feat: harden support, localize knowledge and add operator availability [skip ci]
Source-only publication of reviewed commit 64e96a7f423673dc29ad56485be9fc79bce8af1c.
Passed in isolation: focused frontend/backend tests, race checks, TypeScript typecheck, lint, Go compilation, Flowgram build, and deterministic SDK build/tests.
Not accepted as a production release: Next.js export/native assembly remains blocked by environment memory limits; browser interaction and MySQL staging acceptance remain unverified. No production server, database/schema, runtime configuration, deployment, release tag, or workflow execution was performed.
Expected source tree: 0a4a7528e9.
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
// Modified in the independent hardening fork; see docs/hardening/README.md.
|
||||
// Modified for AgentDeskFixes: guard private state, captured recipients, and stale async completions.
|
||||
"use client"
|
||||
|
||||
import { create } from "zustand"
|
||||
@@ -104,9 +106,9 @@ type AgentConversationsStore = {
|
||||
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>
|
||||
sendMessage: (html: string, conversationId?: number) => Promise<AgentMessage | null>
|
||||
uploadImage: (file: File, conversationId?: number) => Promise<AgentAsset | null>
|
||||
sendAttachment: (file: File, conversationId?: number) => Promise<AgentMessage | null>
|
||||
recallMessage: (messageId: number) => Promise<AgentMessage | null>
|
||||
applyRealtimeMessageCreated: (message: AgentMessage) => void
|
||||
applyRealtimeAgentActivity: (payload: AgentActivityEventPayload) => void
|
||||
@@ -119,6 +121,22 @@ type AgentConversationsStore = {
|
||||
|
||||
let conversationsRequestSeq = 0
|
||||
let messagesRequestSeq = 0
|
||||
let sessionGeneration = 0
|
||||
|
||||
function captureSessionScope() {
|
||||
const generation = sessionGeneration
|
||||
const userId = readSession()?.user.id
|
||||
return () =>
|
||||
generation === sessionGeneration && Boolean(userId) && readSession()?.user.id === userId
|
||||
}
|
||||
|
||||
/** Invalidates every outstanding request before removing private dashboard data. */
|
||||
export function resetAgentConversations() {
|
||||
sessionGeneration += 1
|
||||
conversationsRequestSeq += 1
|
||||
messagesRequestSeq += 1
|
||||
useAgentConversationsStore.setState(useAgentConversationsStore.getInitialState(), true)
|
||||
}
|
||||
|
||||
export const useAgentConversationsStore = create<AgentConversationsStore>((set, get) => ({
|
||||
searchKeyword: "",
|
||||
@@ -167,6 +185,8 @@ export const useAgentConversationsStore = create<AgentConversationsStore>((set,
|
||||
},
|
||||
|
||||
loadConversations: async () => {
|
||||
const isCurrentSession = captureSessionScope()
|
||||
if (!isCurrentSession()) return
|
||||
const requestSeq = ++conversationsRequestSeq
|
||||
const store = get()
|
||||
|
||||
@@ -180,7 +200,7 @@ export const useAgentConversationsStore = create<AgentConversationsStore>((set,
|
||||
)
|
||||
const conversations = ensureArray(data.results)
|
||||
|
||||
if (requestSeq !== conversationsRequestSeq) {
|
||||
if (requestSeq !== conversationsRequestSeq || !isCurrentSession()) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -216,7 +236,7 @@ export const useAgentConversationsStore = create<AgentConversationsStore>((set,
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
if (requestSeq === conversationsRequestSeq) {
|
||||
if (requestSeq === conversationsRequestSeq && isCurrentSession()) {
|
||||
set({ conversationsLoading: false })
|
||||
}
|
||||
throw error
|
||||
@@ -246,10 +266,14 @@ export const useAgentConversationsStore = create<AgentConversationsStore>((set,
|
||||
},
|
||||
|
||||
loadMessages: async (conversationId, options = {}) => {
|
||||
const isCurrentSession = captureSessionScope()
|
||||
if (!isCurrentSession() || get().selectedConversationId !== conversationId) return
|
||||
const requestSeq = ++messagesRequestSeq
|
||||
const store = get()
|
||||
const shouldShowLoading =
|
||||
options.forceLoading || store.messagesLoadedConversationId !== conversationId
|
||||
// A fresh snapshot supersedes any older page request in the prior generation.
|
||||
set({ messagesLoadingMore: false })
|
||||
|
||||
if (shouldShowLoading) {
|
||||
set({
|
||||
@@ -270,7 +294,7 @@ export const useAgentConversationsStore = create<AgentConversationsStore>((set,
|
||||
limit: 50,
|
||||
})
|
||||
|
||||
if (requestSeq !== messagesRequestSeq) {
|
||||
if (requestSeq !== messagesRequestSeq || !isCurrentSession()) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -288,7 +312,7 @@ export const useAgentConversationsStore = create<AgentConversationsStore>((set,
|
||||
messagesHasMore: Boolean(data.hasMore),
|
||||
})
|
||||
} catch (error) {
|
||||
if (requestSeq === messagesRequestSeq) {
|
||||
if (requestSeq === messagesRequestSeq && isCurrentSession()) {
|
||||
set({ messagesLoading: false })
|
||||
}
|
||||
throw error
|
||||
@@ -296,6 +320,9 @@ export const useAgentConversationsStore = create<AgentConversationsStore>((set,
|
||||
},
|
||||
|
||||
loadOlderMessages: async () => {
|
||||
const isCurrentSession = captureSessionScope()
|
||||
if (!isCurrentSession()) return
|
||||
const selectionGeneration = messagesRequestSeq
|
||||
const conversationId = get().selectedConversationId
|
||||
if (!conversationId || get().messagesLoadingMore || !get().messagesHasMore) {
|
||||
return
|
||||
@@ -312,7 +339,7 @@ export const useAgentConversationsStore = create<AgentConversationsStore>((set,
|
||||
cursor: cursorId,
|
||||
limit: 50,
|
||||
})
|
||||
if (get().selectedConversationId !== conversationId) {
|
||||
if (!isCurrentSession() || selectionGeneration !== messagesRequestSeq || get().selectedConversationId !== conversationId) {
|
||||
return
|
||||
}
|
||||
const incoming = ensureArray(data.results)
|
||||
@@ -328,12 +355,17 @@ export const useAgentConversationsStore = create<AgentConversationsStore>((set,
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
set({ messagesLoadingMore: false })
|
||||
if (isCurrentSession() && selectionGeneration === messagesRequestSeq && get().selectedConversationId === conversationId) {
|
||||
set({ messagesLoadingMore: false })
|
||||
}
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
syncLatestMessages: async (conversationId) => {
|
||||
const isCurrentSession = captureSessionScope()
|
||||
if (!isCurrentSession()) return
|
||||
const selectionGeneration = messagesRequestSeq
|
||||
if (conversationId <= 0) {
|
||||
return
|
||||
}
|
||||
@@ -342,7 +374,7 @@ export const useAgentConversationsStore = create<AgentConversationsStore>((set,
|
||||
conversationId,
|
||||
limit: 50,
|
||||
})
|
||||
if (get().selectedConversationId !== conversationId) {
|
||||
if (!isCurrentSession() || selectionGeneration !== messagesRequestSeq || get().selectedConversationId !== conversationId) {
|
||||
return
|
||||
}
|
||||
const batch = ensureArray(data.results)
|
||||
@@ -370,6 +402,8 @@ export const useAgentConversationsStore = create<AgentConversationsStore>((set,
|
||||
},
|
||||
|
||||
markSelectedConversationRead: async () => {
|
||||
const isCurrentSession = captureSessionScope()
|
||||
if (!isCurrentSession()) return
|
||||
const store = get()
|
||||
const conversationId = store.selectedConversationId
|
||||
const conversation = store.conversations.find((item) => item.id === conversationId)
|
||||
@@ -390,6 +424,7 @@ export const useAgentConversationsStore = create<AgentConversationsStore>((set,
|
||||
set({ readingMessageId: lastMessage.id })
|
||||
try {
|
||||
await markAgentMessageRead(conversationId, lastMessage.id)
|
||||
if (!isCurrentSession() || get().readingMessageId !== lastMessage.id) return
|
||||
set((current) => {
|
||||
if (current.selectedConversationId !== conversationId) {
|
||||
return { readingMessageId: 0 }
|
||||
@@ -414,7 +449,9 @@ export const useAgentConversationsStore = create<AgentConversationsStore>((set,
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
set({ readingMessageId: 0 })
|
||||
if (isCurrentSession() && get().readingMessageId === lastMessage.id) {
|
||||
set({ readingMessageId: 0 })
|
||||
}
|
||||
throw error
|
||||
}
|
||||
},
|
||||
@@ -513,7 +550,10 @@ export const useAgentConversationsStore = create<AgentConversationsStore>((set,
|
||||
},
|
||||
|
||||
resyncRealtimeData: async (conversationId) => {
|
||||
const isCurrentSession = captureSessionScope()
|
||||
if (!isCurrentSession()) return
|
||||
await get().loadConversations()
|
||||
if (!isCurrentSession()) return
|
||||
const selectedConversationId = get().selectedConversationId
|
||||
const targetConversationId = conversationId ?? selectedConversationId
|
||||
if (targetConversationId && selectedConversationId === targetConversationId) {
|
||||
@@ -524,10 +564,15 @@ export const useAgentConversationsStore = create<AgentConversationsStore>((set,
|
||||
}
|
||||
},
|
||||
|
||||
sendMessage: async (html) => {
|
||||
sendMessage: async (html, recipientId) => {
|
||||
const isCurrentSession = captureSessionScope()
|
||||
const trimmedContent = html.trim()
|
||||
const { selectedConversationId, sending } = get()
|
||||
if (!selectedConversationId || !trimmedContent || sending) {
|
||||
if (
|
||||
!isCurrentSession() || !selectedConversationId ||
|
||||
(recipientId !== undefined && recipientId !== selectedConversationId) ||
|
||||
!trimmedContent || sending || get().uploadingAsset
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -540,7 +585,7 @@ export const useAgentConversationsStore = create<AgentConversationsStore>((set,
|
||||
clientMsgId: `agent_${generateUUID()}`,
|
||||
})
|
||||
|
||||
if (get().selectedConversationId === selectedConversationId) {
|
||||
if (isCurrentSession() && get().selectedConversationId === selectedConversationId) {
|
||||
set((current) => ({
|
||||
messages: current.messages.some((m) => m.id === message.id)
|
||||
? current.messages.map((m) => (m.id === message.id ? message : m))
|
||||
@@ -561,33 +606,45 @@ export const useAgentConversationsStore = create<AgentConversationsStore>((set,
|
||||
|
||||
return message
|
||||
} finally {
|
||||
set({ sending: false })
|
||||
if (isCurrentSession()) set({ sending: false })
|
||||
}
|
||||
},
|
||||
|
||||
uploadImage: async (file) => {
|
||||
uploadImage: async (file, recipientId) => {
|
||||
const isCurrentSession = captureSessionScope()
|
||||
const { selectedConversationId, sending, uploadingAsset } = get()
|
||||
if (!selectedConversationId || sending || uploadingAsset) {
|
||||
if (
|
||||
!isCurrentSession() || !selectedConversationId ||
|
||||
(recipientId !== undefined && recipientId !== selectedConversationId) ||
|
||||
sending || uploadingAsset
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
set({ uploadingAsset: true })
|
||||
try {
|
||||
return await uploadAgentConversationImage(selectedConversationId, file)
|
||||
const asset = await uploadAgentConversationImage(selectedConversationId, file)
|
||||
return isCurrentSession() ? asset : null
|
||||
} finally {
|
||||
set({ uploadingAsset: false })
|
||||
if (isCurrentSession()) set({ uploadingAsset: false })
|
||||
}
|
||||
},
|
||||
|
||||
sendAttachment: async (file) => {
|
||||
sendAttachment: async (file, recipientId) => {
|
||||
const isCurrentSession = captureSessionScope()
|
||||
const { selectedConversationId, sending, uploadingAsset } = get()
|
||||
if (!selectedConversationId || sending || uploadingAsset) {
|
||||
if (
|
||||
!isCurrentSession() || !selectedConversationId ||
|
||||
(recipientId !== undefined && recipientId !== selectedConversationId) ||
|
||||
sending || uploadingAsset
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
set({ uploadingAsset: true })
|
||||
try {
|
||||
const asset = await uploadAgentConversationAttachment(selectedConversationId, file)
|
||||
if (!isCurrentSession()) return null
|
||||
const message = await sendAgentMessage({
|
||||
conversationId: selectedConversationId,
|
||||
messageType: "attachment",
|
||||
@@ -596,7 +653,7 @@ export const useAgentConversationsStore = create<AgentConversationsStore>((set,
|
||||
clientMsgId: `agent_attachment_${generateUUID()}`,
|
||||
})
|
||||
|
||||
if (get().selectedConversationId === selectedConversationId) {
|
||||
if (isCurrentSession() && get().selectedConversationId === selectedConversationId) {
|
||||
set((current) => ({
|
||||
messages: current.messages.some((m) => m.id === message.id)
|
||||
? current.messages.map((m) => (m.id === message.id ? message : m))
|
||||
@@ -617,20 +674,22 @@ export const useAgentConversationsStore = create<AgentConversationsStore>((set,
|
||||
|
||||
return message
|
||||
} finally {
|
||||
set({ uploadingAsset: false })
|
||||
if (isCurrentSession()) set({ uploadingAsset: false })
|
||||
}
|
||||
},
|
||||
|
||||
recallMessage: async (messageId) => {
|
||||
const isCurrentSession = captureSessionScope()
|
||||
const { selectedConversationId, recallingMessageId } = get()
|
||||
if (!selectedConversationId || messageId <= 0 || recallingMessageId === messageId) {
|
||||
if (!isCurrentSession() || !selectedConversationId || messageId <= 0 || recallingMessageId === messageId) {
|
||||
return null
|
||||
}
|
||||
|
||||
set({ recallingMessageId: messageId })
|
||||
try {
|
||||
const message = await recallAgentMessage(messageId)
|
||||
if (get().selectedConversationId === selectedConversationId) {
|
||||
if (!isCurrentSession()) return null
|
||||
if (isCurrentSession() && get().selectedConversationId === selectedConversationId) {
|
||||
set((current) => {
|
||||
const nextMessages = current.messages.map((item) =>
|
||||
item.id === message.id ? message : item
|
||||
@@ -656,11 +715,11 @@ export const useAgentConversationsStore = create<AgentConversationsStore>((set,
|
||||
}
|
||||
})
|
||||
} else {
|
||||
set({ recallingMessageId: 0 })
|
||||
if (isCurrentSession() && get().recallingMessageId === messageId) set({ recallingMessageId: 0 })
|
||||
}
|
||||
return message
|
||||
} catch (error) {
|
||||
set({ recallingMessageId: 0 })
|
||||
if (isCurrentSession() && get().recallingMessageId === messageId) set({ recallingMessageId: 0 })
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user