|
@@ -11,7 +11,7 @@ import LocalVmWorkspace from '@/components/workspace/LocalVmWorkspace.vue'
|
|
|
import ComputerPanel from '@/components/workspace/ComputerPanel.vue'
|
|
import ComputerPanel from '@/components/workspace/ComputerPanel.vue'
|
|
|
import TaskPlan from '@/components/workspace/TaskPlan.vue'
|
|
import TaskPlan from '@/components/workspace/TaskPlan.vue'
|
|
|
import type { AgentAssignment } from '@shared/agentTasks'
|
|
import type { AgentAssignment } from '@shared/agentTasks'
|
|
|
-import type { WorkspaceTask } from '@shared/workspace'
|
|
|
|
|
|
|
+import type { WorkspaceSendReceipt, WorkspaceTask } from '@shared/workspace'
|
|
|
import { workspaceHasUnread } from '@shared/workspace'
|
|
import { workspaceHasUnread } from '@shared/workspace'
|
|
|
import type { WorkspaceLifecycleAction, WorkspaceLifecyclePreview } from '@shared/workspaceLifecycle'
|
|
import type { WorkspaceLifecycleAction, WorkspaceLifecyclePreview } from '@shared/workspaceLifecycle'
|
|
|
import FloatingResourceSearch from '@/components/app/FloatingResourceSearch.vue'
|
|
import FloatingResourceSearch from '@/components/app/FloatingResourceSearch.vue'
|
|
@@ -643,11 +643,25 @@ async function upload(event: Event) {
|
|
|
;(event.target as HTMLInputElement).value = ''
|
|
;(event.target as HTMLInputElement).value = ''
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+let draftRevision = 0, accountEpoch = 0
|
|
|
|
|
+watch([text, files, mentions], () => { draftRevision++ }, { deep: true, flush: 'sync' })
|
|
|
|
|
+const sendRefreshes = new Map<string, ReturnType<typeof setTimeout>>()
|
|
|
|
|
+function reconcileSend(receipt: WorkspaceSendReceipt, scope: string, id: string, owner: string | undefined) {
|
|
|
|
|
+ clearTimeout(sendRefreshes.get(scope))
|
|
|
|
|
+ sendRefreshes.set(scope, setTimeout(() => {
|
|
|
|
|
+ sendRefreshes.delete(scope)
|
|
|
|
|
+ if (disposed || auth.user?.id !== owner || composerKey.value !== scope) return
|
|
|
|
|
+ if (receipt.cursor !== undefined && transcriptStore.cursor >= receipt.cursor && eventSource?.readyState === EventSource.OPEN) return
|
|
|
|
|
+ void load(id, true).catch(() => { error.value = '消息已发送,正在恢复同步' })
|
|
|
|
|
+ }, receipt.cursor === undefined ? 0 : 2000))
|
|
|
|
|
+}
|
|
|
async function send() {
|
|
async function send() {
|
|
|
const c = active.value
|
|
const c = active.value
|
|
|
if (!c || busy.value || (!text.value.trim() && !files.value.length)) return
|
|
if (!c || busy.value || (!text.value.trim() && !files.value.length)) return
|
|
|
busy.value = true
|
|
busy.value = true
|
|
|
error.value = ''
|
|
error.value = ''
|
|
|
|
|
+ const owner = auth.user?.id, epoch = accountEpoch, revision = draftRevision, scope = composerKey.value, taskId = activeTask.value?.id
|
|
|
|
|
+ const submittedText = text.value, submittedFiles = files.value, submittedMentions = [...mentions.value]
|
|
|
const mode = deliveryMode.value ? 'goal' : 'chat'
|
|
const mode = deliveryMode.value ? 'goal' : 'chat'
|
|
|
const fingerprint = JSON.stringify([c.id, activeTask.value?.id, text.value, mentions.value, files.value.map(f => f.id), mode])
|
|
const fingerprint = JSON.stringify([c.id, activeTask.value?.id, text.value, mentions.value, files.value.map(f => f.id), mode])
|
|
|
if (!pendingRequestId || pendingFingerprint !== fingerprint) {
|
|
if (!pendingRequestId || pendingFingerprint !== fingerprint) {
|
|
@@ -655,7 +669,7 @@ async function send() {
|
|
|
pendingFingerprint = fingerprint
|
|
pendingFingerprint = fingerprint
|
|
|
}
|
|
}
|
|
|
try {
|
|
try {
|
|
|
- await apiRequest(`/api/app/conversations/${c.id}/messages`, {
|
|
|
|
|
|
|
+ const receipt = await apiRequest<WorkspaceSendReceipt>(`/api/app/conversations/${c.id}/messages`, {
|
|
|
method: 'POST',
|
|
method: 'POST',
|
|
|
body: {
|
|
body: {
|
|
|
requestId: pendingRequestId,
|
|
requestId: pendingRequestId,
|
|
@@ -666,19 +680,24 @@ async function send() {
|
|
|
fileIds: files.value.map((f) => f.id),
|
|
fileIds: files.value.map((f) => f.id),
|
|
|
},
|
|
},
|
|
|
})
|
|
})
|
|
|
- text.value = ''
|
|
|
|
|
- files.value = []
|
|
|
|
|
- mentions.value = []
|
|
|
|
|
- pendingRequestId = undefined
|
|
|
|
|
- pendingFingerprint = ''
|
|
|
|
|
- deliveryMode.value = false
|
|
|
|
|
- await load(c.id, true)
|
|
|
|
|
- await refresh()
|
|
|
|
|
|
|
+ if (disposed || accountEpoch !== epoch || auth.user?.id !== owner) return true
|
|
|
|
|
+ busy.value = false
|
|
|
|
|
+ if (draftRevision === revision && composerKey.value === scope && text.value === submittedText && files.value === submittedFiles
|
|
|
|
|
+ && JSON.stringify(mentions.value) === JSON.stringify(submittedMentions)) {
|
|
|
|
|
+ text.value = ''; files.value = []; mentions.value = []
|
|
|
|
|
+ pendingRequestId = undefined; pendingFingerprint = ''; deliveryMode.value = false
|
|
|
|
|
+ }
|
|
|
|
|
+ flushEvents()
|
|
|
|
|
+ transcriptStore.acceptReceipt(receipt)
|
|
|
|
|
+ conversations.value = transcriptStore.conversations
|
|
|
|
|
+ const accepted = transcriptStore.get(c.id, taskId)
|
|
|
|
|
+ if (accepted && composerKey.value === scope) presentDetail(accepted)
|
|
|
|
|
+ reconcileSend(receipt, scope, c.id, owner)
|
|
|
return true
|
|
return true
|
|
|
} catch (e) {
|
|
} catch (e) {
|
|
|
- error.value = e instanceof Error ? e.message : '发送失败'
|
|
|
|
|
|
|
+ if (accountEpoch === epoch) error.value = e instanceof Error ? e.message : '发送失败'
|
|
|
} finally {
|
|
} finally {
|
|
|
- busy.value = false
|
|
|
|
|
|
|
+ if (accountEpoch === epoch) busy.value = false
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -804,6 +823,7 @@ watch([selected,selectedTask], () => {
|
|
|
watch(selected,()=>{dockView.value=undefined})
|
|
watch(selected,()=>{dockView.value=undefined})
|
|
|
watch(() => auth.user?.id, (owner, previous) => {
|
|
watch(() => auth.user?.id, (owner, previous) => {
|
|
|
if (owner === previous) return
|
|
if (owner === previous) return
|
|
|
|
|
+ accountEpoch++; busy.value = false
|
|
|
generation++
|
|
generation++
|
|
|
eventSource?.close(); eventSource = undefined; eventQueue = []
|
|
eventSource?.close(); eventSource = undefined; eventQueue = []
|
|
|
if (eventFrame !== undefined) cancelAnimationFrame(eventFrame)
|
|
if (eventFrame !== undefined) cancelAnimationFrame(eventFrame)
|
|
@@ -826,6 +846,8 @@ onMounted(async () => {
|
|
|
}
|
|
}
|
|
|
})
|
|
})
|
|
|
onBeforeUnmount(() => {
|
|
onBeforeUnmount(() => {
|
|
|
|
|
+ for (const timer of sendRefreshes.values()) clearTimeout(timer)
|
|
|
|
|
+ sendRefreshes.clear()
|
|
|
disposed = true
|
|
disposed = true
|
|
|
eventSource?.close()
|
|
eventSource?.close()
|
|
|
if (eventFrame !== undefined) cancelAnimationFrame(eventFrame)
|
|
if (eventFrame !== undefined) cancelAnimationFrame(eventFrame)
|