2026-04-09 10:01:23 +08:00
|
|
|
|
"use client"
|
|
|
|
|
|
|
|
|
|
|
|
import { create } from "zustand"
|
|
|
|
|
|
|
|
|
|
|
|
import {
|
|
|
|
|
|
fetchAgentConversations,
|
|
|
|
|
|
fetchAgentMessages,
|
|
|
|
|
|
markAgentMessageRead,
|
|
|
|
|
|
recallAgentMessage,
|
|
|
|
|
|
sendAgentMessage,
|
|
|
|
|
|
uploadAgentConversationAttachment,
|
|
|
|
|
|
uploadAgentConversationImage,
|
|
|
|
|
|
type AgentAsset,
|
|
|
|
|
|
type AgentConversation,
|
|
|
|
|
|
type AgentMessage,
|
|
|
|
|
|
} from "@/lib/api/agent"
|
|
|
|
|
|
import { summarizeIMMessage } from "@/lib/im-message"
|
|
|
|
|
|
import { generateUUID } from "@/lib/utils"
|
|
|
|
|
|
|
|
|
|
|
|
export const agentConversationFilterOptions = [
|
|
|
|
|
|
// { value: "mine", label: "我的" },
|
|
|
|
|
|
{ value: "active", label: "处理中" },
|
|
|
|
|
|
{ value: "pending", label: "待接入" },
|
2026-04-11 14:02:09 +08:00
|
|
|
|
{ value: "ai_serving", label: "AI接待中" },
|
2026-04-09 10:01:23 +08:00
|
|
|
|
{ value: "closed", label: "已关闭" },
|
|
|
|
|
|
] as const
|
|
|
|
|
|
|
|
|
|
|
|
export type AgentConversationFilterKey =
|
|
|
|
|
|
(typeof agentConversationFilterOptions)[number]["value"]
|
|
|
|
|
|
|
|
|
|
|
|
function buildConversationQuery(filter: AgentConversationFilterKey, keyword: string) {
|
|
|
|
|
|
const query: Record<string, string | number | undefined> = {
|
|
|
|
|
|
filter,
|
|
|
|
|
|
keyword: keyword.trim() || undefined,
|
|
|
|
|
|
limit: 100,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return query
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type LoadMessagesOptions = {
|
|
|
|
|
|
forceLoading?: boolean
|
|
|
|
|
|
reset?: boolean
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function ensureArray<T>(value: T[] | null | undefined): T[] {
|
|
|
|
|
|
return Array.isArray(value) ? value : []
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function mergeMessagesByIdAsc(
|
|
|
|
|
|
a: AgentMessage[],
|
|
|
|
|
|
b: AgentMessage[]
|
|
|
|
|
|
): AgentMessage[] {
|
|
|
|
|
|
const byId = new Map<number, AgentMessage>()
|
|
|
|
|
|
for (const m of a) {
|
|
|
|
|
|
byId.set(m.id, m)
|
|
|
|
|
|
}
|
|
|
|
|
|
for (const m of b) {
|
|
|
|
|
|
byId.set(m.id, m)
|
|
|
|
|
|
}
|
|
|
|
|
|
return Array.from(byId.values()).sort((x, y) => x.id - y.id)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function parseCursorId(cursor: string): number {
|
|
|
|
|
|
const n = Number.parseInt(cursor, 10)
|
|
|
|
|
|
return Number.isFinite(n) && n > 0 ? n : 0
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 下一页「更旧」请求应传入的游标:当前已加载列表中的最小 message id(后端用 id < cursor) */
|
|
|
|
|
|
function cursorFromLoadedMessages(messages: AgentMessage[]): string {
|
|
|
|
|
|
if (messages.length === 0) {
|
|
|
|
|
|
return ""
|
|
|
|
|
|
}
|
|
|
|
|
|
return String(Math.min(...messages.map((m) => m.id)))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function minMessageId(messages: AgentMessage[]): number | null {
|
|
|
|
|
|
if (messages.length === 0) {
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
return Math.min(...messages.map((m) => m.id))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 拉「最新一页」做增量合并后,是否仍显示「还有更旧」。
|
|
|
|
|
|
* 若本地已确认没有更旧,且合并后最早一条 id 没有变小,则不能用接口对「最新一页」的 hasMore 再次打开(满页会误报)。
|
|
|
|
|
|
*/
|
|
|
|
|
|
function hasMoreAfterLatestSyncMerge(args: {
|
|
|
|
|
|
previousMessages: AgentMessage[]
|
|
|
|
|
|
previousHasMore: boolean
|
|
|
|
|
|
merged: AgentMessage[]
|
|
|
|
|
|
apiHasMore: boolean
|
|
|
|
|
|
}): boolean {
|
|
|
|
|
|
const prevMin = minMessageId(args.previousMessages)
|
|
|
|
|
|
const mergedMin = minMessageId(args.merged)
|
|
|
|
|
|
|
|
|
|
|
|
if (mergedMin === null) {
|
|
|
|
|
|
return Boolean(args.apiHasMore)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
|
!args.previousHasMore &&
|
|
|
|
|
|
prevMin !== null &&
|
|
|
|
|
|
mergedMin >= prevMin
|
|
|
|
|
|
) {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return args.previousHasMore || Boolean(args.apiHasMore)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type AgentConversationsStore = {
|
|
|
|
|
|
searchKeyword: string
|
|
|
|
|
|
conversationFilter: AgentConversationFilterKey
|
|
|
|
|
|
conversations: AgentConversation[]
|
|
|
|
|
|
conversationsLoading: boolean
|
|
|
|
|
|
conversationsLoaded: boolean
|
|
|
|
|
|
selectedConversationId: number | null
|
|
|
|
|
|
messages: AgentMessage[]
|
|
|
|
|
|
messagesLoading: boolean
|
|
|
|
|
|
messagesLoadingMore: boolean
|
|
|
|
|
|
messagesCursor: string
|
|
|
|
|
|
messagesHasMore: boolean
|
|
|
|
|
|
messagesLoadedConversationId: number | null
|
|
|
|
|
|
sending: boolean
|
|
|
|
|
|
uploadingAsset: boolean
|
|
|
|
|
|
recallingMessageId: number
|
|
|
|
|
|
readingMessageId: number
|
|
|
|
|
|
setSearchKeyword: (keyword: string) => void
|
|
|
|
|
|
setConversationFilter: (filter: AgentConversationFilterKey) => void
|
|
|
|
|
|
setConversationTags: (
|
|
|
|
|
|
conversationId: number,
|
|
|
|
|
|
tags: AgentConversation["tags"]
|
|
|
|
|
|
) => void
|
|
|
|
|
|
loadConversations: () => Promise<void>
|
|
|
|
|
|
selectConversation: (conversationId: number) => Promise<void>
|
|
|
|
|
|
loadMessages: (conversationId: number, options?: LoadMessagesOptions) => Promise<void>
|
|
|
|
|
|
loadOlderMessages: () => Promise<void>
|
|
|
|
|
|
syncLatestMessages: (conversationId: number) => Promise<void>
|
|
|
|
|
|
markSelectedConversationRead: () => Promise<void>
|
|
|
|
|
|
sendMessage: (html: string) => Promise<AgentMessage | null>
|
|
|
|
|
|
uploadImage: (file: File) => Promise<AgentAsset | null>
|
|
|
|
|
|
sendAttachment: (file: File) => Promise<AgentMessage | null>
|
|
|
|
|
|
recallMessage: (messageId: number) => Promise<AgentMessage | null>
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let conversationsRequestSeq = 0
|
|
|
|
|
|
let messagesRequestSeq = 0
|
|
|
|
|
|
|
|
|
|
|
|
export const useAgentConversationsStore = create<AgentConversationsStore>((set, get) => ({
|
|
|
|
|
|
searchKeyword: "",
|
|
|
|
|
|
conversationFilter: "active",
|
|
|
|
|
|
conversations: [],
|
|
|
|
|
|
conversationsLoading: false,
|
|
|
|
|
|
conversationsLoaded: false,
|
|
|
|
|
|
selectedConversationId: null,
|
|
|
|
|
|
messages: [],
|
|
|
|
|
|
messagesLoading: false,
|
|
|
|
|
|
messagesLoadingMore: false,
|
|
|
|
|
|
messagesCursor: "",
|
|
|
|
|
|
messagesHasMore: false,
|
|
|
|
|
|
messagesLoadedConversationId: null,
|
|
|
|
|
|
sending: false,
|
|
|
|
|
|
uploadingAsset: false,
|
|
|
|
|
|
recallingMessageId: 0,
|
|
|
|
|
|
readingMessageId: 0,
|
|
|
|
|
|
|
|
|
|
|
|
setSearchKeyword: (keyword) => {
|
|
|
|
|
|
set({ searchKeyword: keyword })
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
setConversationFilter: (filter) => {
|
|
|
|
|
|
set({ conversationFilter: filter })
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
setConversationTags: (conversationId, tags) => {
|
|
|
|
|
|
set((state) => ({
|
|
|
|
|
|
conversations: state.conversations.map((item) =>
|
|
|
|
|
|
item.id === conversationId
|
|
|
|
|
|
? {
|
|
|
|
|
|
...item,
|
|
|
|
|
|
tags: tags && tags.length > 0 ? tags : [],
|
|
|
|
|
|
}
|
|
|
|
|
|
: item
|
|
|
|
|
|
),
|
|
|
|
|
|
}))
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
loadConversations: async () => {
|
|
|
|
|
|
const requestSeq = ++conversationsRequestSeq
|
|
|
|
|
|
const store = get()
|
|
|
|
|
|
|
|
|
|
|
|
if (!store.conversationsLoaded) {
|
|
|
|
|
|
set({ conversationsLoading: true })
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const data = await fetchAgentConversations(
|
|
|
|
|
|
buildConversationQuery(store.conversationFilter, store.searchKeyword)
|
|
|
|
|
|
)
|
|
|
|
|
|
const conversations = ensureArray(data.results)
|
|
|
|
|
|
|
|
|
|
|
|
if (requestSeq !== conversationsRequestSeq) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const currentSelectedId = get().selectedConversationId
|
|
|
|
|
|
const hasCurrentSelection =
|
|
|
|
|
|
currentSelectedId !== null && conversations.some((item) => item.id === currentSelectedId)
|
|
|
|
|
|
const nextSelectedId = hasCurrentSelection ? currentSelectedId : (conversations[0]?.id ?? null)
|
|
|
|
|
|
const selectionChanged = nextSelectedId !== currentSelectedId
|
|
|
|
|
|
|
|
|
|
|
|
set({
|
|
|
|
|
|
conversations,
|
|
|
|
|
|
conversationsLoaded: true,
|
|
|
|
|
|
conversationsLoading: false,
|
|
|
|
|
|
selectedConversationId: nextSelectedId,
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
if (nextSelectedId === null) {
|
|
|
|
|
|
set({
|
|
|
|
|
|
messages: [],
|
|
|
|
|
|
messagesLoading: false,
|
|
|
|
|
|
messagesLoadingMore: false,
|
|
|
|
|
|
messagesCursor: "",
|
|
|
|
|
|
messagesHasMore: false,
|
|
|
|
|
|
messagesLoadedConversationId: null,
|
|
|
|
|
|
})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (selectionChanged || get().messagesLoadedConversationId === null) {
|
|
|
|
|
|
await get().loadMessages(nextSelectedId, {
|
|
|
|
|
|
forceLoading: true,
|
|
|
|
|
|
reset: true,
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
if (requestSeq === conversationsRequestSeq) {
|
|
|
|
|
|
set({ conversationsLoading: false })
|
|
|
|
|
|
}
|
|
|
|
|
|
throw error
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
selectConversation: async (conversationId) => {
|
|
|
|
|
|
if (get().selectedConversationId === conversationId) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
set({
|
|
|
|
|
|
selectedConversationId: conversationId,
|
|
|
|
|
|
messages: [],
|
|
|
|
|
|
messagesLoading: true,
|
|
|
|
|
|
messagesLoadingMore: false,
|
|
|
|
|
|
messagesCursor: "",
|
|
|
|
|
|
messagesHasMore: false,
|
|
|
|
|
|
messagesLoadedConversationId: null,
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
await get().loadMessages(conversationId, {
|
|
|
|
|
|
forceLoading: true,
|
|
|
|
|
|
reset: true,
|
|
|
|
|
|
})
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
loadMessages: async (conversationId, options = {}) => {
|
|
|
|
|
|
const requestSeq = ++messagesRequestSeq
|
|
|
|
|
|
const store = get()
|
|
|
|
|
|
const shouldShowLoading =
|
|
|
|
|
|
options.forceLoading || store.messagesLoadedConversationId !== conversationId
|
|
|
|
|
|
|
|
|
|
|
|
if (shouldShowLoading) {
|
|
|
|
|
|
set({
|
|
|
|
|
|
messagesLoading: true,
|
|
|
|
|
|
...(options.reset
|
|
|
|
|
|
? {
|
|
|
|
|
|
messages: [],
|
|
|
|
|
|
messagesCursor: "",
|
|
|
|
|
|
messagesHasMore: false,
|
|
|
|
|
|
}
|
|
|
|
|
|
: {}),
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const data = await fetchAgentMessages({
|
|
|
|
|
|
conversationId,
|
|
|
|
|
|
limit: 50,
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
if (requestSeq !== messagesRequestSeq) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (get().selectedConversationId !== conversationId) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const list = ensureArray(data.results)
|
|
|
|
|
|
set({
|
|
|
|
|
|
messages: list,
|
|
|
|
|
|
messagesLoading: false,
|
|
|
|
|
|
messagesLoadedConversationId: conversationId,
|
|
|
|
|
|
messagesCursor:
|
|
|
|
|
|
cursorFromLoadedMessages(list) || (data.cursor ?? ""),
|
|
|
|
|
|
messagesHasMore: Boolean(data.hasMore),
|
|
|
|
|
|
})
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
if (requestSeq === messagesRequestSeq) {
|
|
|
|
|
|
set({ messagesLoading: false })
|
|
|
|
|
|
}
|
|
|
|
|
|
throw error
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
loadOlderMessages: async () => {
|
|
|
|
|
|
const conversationId = get().selectedConversationId
|
|
|
|
|
|
if (!conversationId || get().messagesLoadingMore || !get().messagesHasMore) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
const cursorId = parseCursorId(get().messagesCursor)
|
|
|
|
|
|
if (cursorId <= 0) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
set({ messagesLoadingMore: true })
|
|
|
|
|
|
try {
|
|
|
|
|
|
const data = await fetchAgentMessages({
|
|
|
|
|
|
conversationId,
|
|
|
|
|
|
cursor: cursorId,
|
|
|
|
|
|
limit: 50,
|
|
|
|
|
|
})
|
|
|
|
|
|
if (get().selectedConversationId !== conversationId) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
const incoming = ensureArray(data.results)
|
|
|
|
|
|
set((state) => {
|
|
|
|
|
|
const merged = mergeMessagesByIdAsc(incoming, state.messages)
|
|
|
|
|
|
return {
|
|
|
|
|
|
messages: merged,
|
|
|
|
|
|
messagesCursor:
|
|
|
|
|
|
cursorFromLoadedMessages(merged) ||
|
|
|
|
|
|
(data.cursor ?? state.messagesCursor),
|
|
|
|
|
|
messagesHasMore: Boolean(data.hasMore),
|
|
|
|
|
|
messagesLoadingMore: false,
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
set({ messagesLoadingMore: false })
|
|
|
|
|
|
throw error
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
syncLatestMessages: async (conversationId) => {
|
|
|
|
|
|
if (conversationId <= 0) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
|
|
|
const data = await fetchAgentMessages({
|
|
|
|
|
|
conversationId,
|
|
|
|
|
|
limit: 50,
|
|
|
|
|
|
})
|
|
|
|
|
|
if (get().selectedConversationId !== conversationId) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
const batch = ensureArray(data.results)
|
|
|
|
|
|
if (batch.length === 0) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
const firstId = batch[0]!.id
|
|
|
|
|
|
set((state) => {
|
|
|
|
|
|
const preserved = state.messages.filter((m) => m.id < firstId)
|
|
|
|
|
|
const merged = mergeMessagesByIdAsc(preserved, batch)
|
|
|
|
|
|
return {
|
|
|
|
|
|
messages: merged,
|
|
|
|
|
|
messagesCursor:
|
|
|
|
|
|
cursorFromLoadedMessages(merged) ||
|
|
|
|
|
|
(data.cursor ?? state.messagesCursor),
|
|
|
|
|
|
messagesHasMore: hasMoreAfterLatestSyncMerge({
|
|
|
|
|
|
previousMessages: state.messages,
|
|
|
|
|
|
previousHasMore: state.messagesHasMore,
|
|
|
|
|
|
merged,
|
|
|
|
|
|
apiHasMore: Boolean(data.hasMore),
|
|
|
|
|
|
}),
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
// 实时同步失败不抛给 WS 回调
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
markSelectedConversationRead: async () => {
|
|
|
|
|
|
const store = get()
|
|
|
|
|
|
const conversationId = store.selectedConversationId
|
|
|
|
|
|
const conversation = store.conversations.find((item) => item.id === conversationId)
|
|
|
|
|
|
const lastMessage = store.messages.at(-1)
|
|
|
|
|
|
if (!conversationId || !conversation || !lastMessage) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if (
|
|
|
|
|
|
conversation.agentUnreadCount <= 0 &&
|
|
|
|
|
|
(conversation.agentLastReadMessageId ?? 0) >= lastMessage.id
|
|
|
|
|
|
) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if (store.readingMessageId === lastMessage.id) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
set({ readingMessageId: lastMessage.id })
|
|
|
|
|
|
try {
|
|
|
|
|
|
await markAgentMessageRead(conversationId, lastMessage.id)
|
|
|
|
|
|
set((current) => {
|
|
|
|
|
|
if (current.selectedConversationId !== conversationId) {
|
|
|
|
|
|
return { readingMessageId: 0 }
|
|
|
|
|
|
}
|
|
|
|
|
|
return {
|
|
|
|
|
|
readingMessageId: 0,
|
|
|
|
|
|
messages: current.messages.map((item) =>
|
|
|
|
|
|
item.seqNo <= lastMessage.seqNo
|
|
|
|
|
|
? {
|
|
|
|
|
|
...item,
|
|
|
|
|
|
agentRead: true,
|
|
|
|
|
|
}
|
|
|
|
|
|
: item
|
|
|
|
|
|
),
|
|
|
|
|
|
conversations: current.conversations.map((item) =>
|
|
|
|
|
|
item.id === conversationId
|
|
|
|
|
|
? {
|
|
|
|
|
|
...item,
|
|
|
|
|
|
agentUnreadCount: 0,
|
|
|
|
|
|
agentLastReadMessageId: lastMessage.id,
|
|
|
|
|
|
agentLastReadSeqNo: lastMessage.seqNo,
|
|
|
|
|
|
}
|
|
|
|
|
|
: item
|
|
|
|
|
|
),
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
set({ readingMessageId: 0 })
|
|
|
|
|
|
throw error
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
sendMessage: async (html) => {
|
|
|
|
|
|
const trimmedContent = html.trim()
|
|
|
|
|
|
const { selectedConversationId, sending } = get()
|
|
|
|
|
|
if (!selectedConversationId || !trimmedContent || sending) {
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
set({ sending: true })
|
|
|
|
|
|
try {
|
|
|
|
|
|
const message = await sendAgentMessage({
|
|
|
|
|
|
conversationId: selectedConversationId,
|
|
|
|
|
|
messageType: "html",
|
|
|
|
|
|
content: trimmedContent,
|
|
|
|
|
|
clientMsgId: `agent_${generateUUID()}`,
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
if (get().selectedConversationId === selectedConversationId) {
|
|
|
|
|
|
set((current) => ({
|
|
|
|
|
|
messages: current.messages.some((m) => m.id === message.id)
|
|
|
|
|
|
? current.messages.map((m) => (m.id === message.id ? message : m))
|
|
|
|
|
|
: [...current.messages, message],
|
|
|
|
|
|
conversations: current.conversations.map((item) =>
|
|
|
|
|
|
item.id === selectedConversationId
|
|
|
|
|
|
? {
|
|
|
|
|
|
...item,
|
|
|
|
|
|
lastMessageAt: message.sentAt,
|
|
|
|
|
|
lastActiveAt: message.sentAt,
|
|
|
|
|
|
lastMessageSummary: summarizeIMMessage({
|
|
|
|
|
|
messageType: "html",
|
|
|
|
|
|
content: trimmedContent,
|
|
|
|
|
|
}),
|
|
|
|
|
|
agentUnreadCount: 0,
|
|
|
|
|
|
customerUnreadCount: (item.customerUnreadCount ?? 0) + 1,
|
|
|
|
|
|
agentLastReadMessageId: message.id,
|
|
|
|
|
|
agentLastReadSeqNo: message.seqNo,
|
|
|
|
|
|
}
|
|
|
|
|
|
: item
|
|
|
|
|
|
),
|
|
|
|
|
|
}))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return message
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
set({ sending: false })
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
uploadImage: async (file) => {
|
|
|
|
|
|
const { selectedConversationId, sending, uploadingAsset } = get()
|
|
|
|
|
|
if (!selectedConversationId || sending || uploadingAsset) {
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
set({ uploadingAsset: true })
|
|
|
|
|
|
try {
|
|
|
|
|
|
return await uploadAgentConversationImage(selectedConversationId, file)
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
set({ uploadingAsset: false })
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
sendAttachment: async (file) => {
|
|
|
|
|
|
const { selectedConversationId, sending, uploadingAsset } = get()
|
|
|
|
|
|
if (!selectedConversationId || sending || uploadingAsset) {
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
set({ uploadingAsset: true })
|
|
|
|
|
|
try {
|
|
|
|
|
|
const asset = await uploadAgentConversationAttachment(selectedConversationId, file)
|
|
|
|
|
|
const message = await sendAgentMessage({
|
|
|
|
|
|
conversationId: selectedConversationId,
|
|
|
|
|
|
messageType: "attachment",
|
|
|
|
|
|
content: asset.filename,
|
|
|
|
|
|
payload: JSON.stringify({ assetId: asset.assetId }),
|
|
|
|
|
|
clientMsgId: `agent_attachment_${generateUUID()}`,
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
if (get().selectedConversationId === selectedConversationId) {
|
|
|
|
|
|
set((current) => ({
|
|
|
|
|
|
messages: current.messages.some((m) => m.id === message.id)
|
|
|
|
|
|
? current.messages.map((m) => (m.id === message.id ? message : m))
|
|
|
|
|
|
: [...current.messages, message],
|
|
|
|
|
|
conversations: current.conversations.map((item) =>
|
|
|
|
|
|
item.id === selectedConversationId
|
|
|
|
|
|
? {
|
|
|
|
|
|
...item,
|
|
|
|
|
|
lastMessageAt: message.sentAt,
|
|
|
|
|
|
lastActiveAt: message.sentAt,
|
|
|
|
|
|
lastMessageSummary: summarizeIMMessage(message),
|
|
|
|
|
|
agentUnreadCount: 0,
|
|
|
|
|
|
customerUnreadCount: (item.customerUnreadCount ?? 0) + 1,
|
|
|
|
|
|
agentLastReadMessageId: message.id,
|
|
|
|
|
|
agentLastReadSeqNo: message.seqNo,
|
|
|
|
|
|
}
|
|
|
|
|
|
: item
|
|
|
|
|
|
),
|
|
|
|
|
|
}))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return message
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
set({ uploadingAsset: false })
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
recallMessage: async (messageId) => {
|
|
|
|
|
|
const { selectedConversationId, recallingMessageId } = get()
|
|
|
|
|
|
if (!selectedConversationId || messageId <= 0 || recallingMessageId === messageId) {
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
set({ recallingMessageId: messageId })
|
|
|
|
|
|
try {
|
|
|
|
|
|
const message = await recallAgentMessage(messageId)
|
|
|
|
|
|
if (get().selectedConversationId === selectedConversationId) {
|
|
|
|
|
|
set((current) => {
|
|
|
|
|
|
const nextMessages = current.messages.map((item) =>
|
|
|
|
|
|
item.id === message.id ? message : item
|
|
|
|
|
|
)
|
|
|
|
|
|
const lastActiveMessage = [...nextMessages]
|
|
|
|
|
|
.reverse()
|
|
|
|
|
|
.find((item) => !item.recalledAt && item.sendStatus !== 6)
|
|
|
|
|
|
return {
|
|
|
|
|
|
recallingMessageId: 0,
|
|
|
|
|
|
messages: nextMessages,
|
|
|
|
|
|
conversations: current.conversations.map((item) =>
|
|
|
|
|
|
item.id === selectedConversationId
|
|
|
|
|
|
? {
|
|
|
|
|
|
...item,
|
|
|
|
|
|
lastMessageId: lastActiveMessage?.id ?? 0,
|
|
|
|
|
|
lastMessageAt: lastActiveMessage?.sentAt ?? "",
|
|
|
|
|
|
lastMessageSummary: lastActiveMessage
|
|
|
|
|
|
? summarizeIMMessage(lastActiveMessage)
|
|
|
|
|
|
: "",
|
|
|
|
|
|
}
|
|
|
|
|
|
: item
|
|
|
|
|
|
),
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
} else {
|
|
|
|
|
|
set({ recallingMessageId: 0 })
|
|
|
|
|
|
}
|
|
|
|
|
|
return message
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
set({ recallingMessageId: 0 })
|
|
|
|
|
|
throw error
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
|
|
export const agentConversationSelectors = {
|
|
|
|
|
|
selectedConversation: (state: AgentConversationsStore) =>
|
|
|
|
|
|
state.conversations.find((item) => item.id === state.selectedConversationId) ?? null,
|
|
|
|
|
|
}
|