|
@@ -1,4 +1,5 @@
|
|
|
<script setup lang="ts">
|
|
<script setup lang="ts">
|
|
|
|
|
+import { useUnreadStore } from '@/stores/unread'
|
|
|
import type { ComputerBackend } from '@shared/managedBrowser'
|
|
import type { ComputerBackend } from '@shared/managedBrowser'
|
|
|
import { WORKSPACE_PATCH_CAPABILITY } from '@shared/workspaceMessagePatch'
|
|
import { WORKSPACE_PATCH_CAPABILITY } from '@shared/workspaceMessagePatch'
|
|
|
import { setApiCsrfToken } from '@/api/client'
|
|
import { setApiCsrfToken } from '@/api/client'
|
|
@@ -376,8 +377,9 @@ async function load(id = selected.value, append = false, signal?: AbortSignal) {
|
|
|
if (!append || atBottom) {
|
|
if (!append || atBottom) {
|
|
|
await nextTick()
|
|
await nextTick()
|
|
|
if (!current()) return
|
|
if (!current()) return
|
|
|
- if (!append) timeline.value?.scrollToBottom('auto')
|
|
|
|
|
- void markRead().catch(e => { if (current()) error.value = e instanceof Error ? e.message : '同步已读失败' })
|
|
|
|
|
|
|
+ if (!append && typeof route.query.unread!=='string') timeline.value?.scrollToBottom('auto')
|
|
|
|
|
+ if(typeof route.query.unread==='string')void locateUnread(route.query.unread)
|
|
|
|
|
+
|
|
|
}
|
|
}
|
|
|
} catch (e) {
|
|
} catch (e) {
|
|
|
if (current() && requestedTask && e instanceof ApiError && e.status === 404) {
|
|
if (current() && requestedTask && e instanceof ApiError && e.status === 404) {
|
|
@@ -396,44 +398,20 @@ async function load(id = selected.value, append = false, signal?: AbortSignal) {
|
|
|
if (own === generation) loading.value = false
|
|
if (own === generation) loading.value = false
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+async function locateUnread(id:string){
|
|
|
|
|
+ for(let i=0;i<100&&active.value?.id===selected.value;i++){
|
|
|
|
|
+ await nextTick();if(timeline.value?.scrollToMessage(id)){const query={...route.query};delete query.unread;await router.replace({query});return}
|
|
|
|
|
+ if(!older.value||loadingOlder.value)return
|
|
|
|
|
+ await loadOlder()
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+const unread=useUnreadStore()
|
|
|
const pendingReads = new Set<string>()
|
|
const pendingReads = new Set<string>()
|
|
|
-async function markRead() {
|
|
|
|
|
- const c = active.value
|
|
|
|
|
- const read = activeTask.value ?? c
|
|
|
|
|
- if (!c || !read || (!workspaceHasUnread(read) && read.readSeq >= read.lastSeq)) return
|
|
|
|
|
- const task = activeTask.value, seq = read.lastSeq, version = read.unreadVersion
|
|
|
|
|
- const key = `${c.id}:${task?.id ?? ''}:${version}:${seq}`
|
|
|
|
|
- if (pendingReads.has(key)) return
|
|
|
|
|
- pendingReads.add(key)
|
|
|
|
|
- const previous = { unread: read.unread, unreadCount: read.unreadCount, readSeq: read.readSeq }
|
|
|
|
|
- const previousConversation = { unread: c.unread, unreadCount: c.unreadCount }
|
|
|
|
|
- read.unread = false;read.unreadCount = 0;read.readSeq = seq
|
|
|
|
|
- if (task) tasks.value = tasks.value.map(t => t.id === task.id ? { ...t, unread: false, unreadCount: 0, readSeq: seq } : t)
|
|
|
|
|
- const publishUnread = () => {
|
|
|
|
|
- c.unread = task ? tasks.value.some(workspaceHasUnread) : read.unread
|
|
|
|
|
- c.unreadCount = c.unread ? 1 : 0
|
|
|
|
|
- const row = conversations.value.find(row => row.id === c.id)
|
|
|
|
|
- if (row && row.unreadVersion === c.unreadVersion) { row.unread = c.unread;row.unreadCount = c.unreadCount }
|
|
|
|
|
- }
|
|
|
|
|
- publishUnread()
|
|
|
|
|
- try {
|
|
|
|
|
- const result = await apiRequest<{conversation: Conversation}>(`/api/app/conversations/${c.id}/read${task ? `?taskId=${task.id}` : ''}`, {
|
|
|
|
|
- method: 'PUT', body: { seq, ...(version === undefined ? {} : {unreadVersion:version}) },
|
|
|
|
|
- })
|
|
|
|
|
- const row = conversations.value.find(row => row.id === c.id)
|
|
|
|
|
- if (row && result.conversation && (row.unreadVersion ?? 0) <= (result.conversation.unreadVersion ?? 0)) {
|
|
|
|
|
- row.unread = result.conversation.unread;row.unreadCount = result.conversation.unreadCount;row.unreadVersion = result.conversation.unreadVersion
|
|
|
|
|
- }
|
|
|
|
|
- } catch (error) {
|
|
|
|
|
- const row = conversations.value.find(row => row.id === c.id)
|
|
|
|
|
- if (row && row.unreadVersion === c.unreadVersion) Object.assign(row, previousConversation)
|
|
|
|
|
- if (active.value?.id === c.id && activeTask.value?.id === task?.id && (activeTask.value ?? active.value)?.unreadVersion === version) {
|
|
|
|
|
- Object.assign(activeTask.value ?? active.value!, previous)
|
|
|
|
|
- if (task) tasks.value = tasks.value.map(t => t.id === task.id ? { ...t, ...previous, unreadCount: previous.unreadCount ?? 0 } : t)
|
|
|
|
|
- publishUnread()
|
|
|
|
|
- }
|
|
|
|
|
- throw error
|
|
|
|
|
- } finally { pendingReads.delete(key) }
|
|
|
|
|
|
|
+async function markVisible(ids:string[]) {
|
|
|
|
|
+ const c=active.value;if(!c||loading.value||computerOpen.value||typeof route.query.unread==='string')return
|
|
|
|
|
+ const key=`${c.id}:${ids.join(',')}`;if(pendingReads.has(key))return
|
|
|
|
|
+ const complete=new Set(messages.value.filter(m=>m.role==='assistant'&&m.status==='complete'&&m.visible!==false).map(m=>m.id))
|
|
|
|
|
+ pendingReads.add(key);try{await unread.visible('bot',c.id,undefined,ids.filter(id=>complete.has(id)))}catch{}finally{pendingReads.delete(key)}
|
|
|
}
|
|
}
|
|
|
async function selectTask(event:Event){
|
|
async function selectTask(event:Event){
|
|
|
const select=event.currentTarget as HTMLSelectElement,value=select.value
|
|
const select=event.currentTarget as HTMLSelectElement,value=select.value
|
|
@@ -503,7 +481,7 @@ function presentDetail(detail: WorkspaceDetail) {
|
|
|
run.value = detail.run; interactions.value = detail.interactions
|
|
run.value = detail.run; interactions.value = detail.interactions
|
|
|
context.value = detail.context; assignments.value = detail.assignments ?? []
|
|
context.value = detail.context; assignments.value = detail.assignments ?? []
|
|
|
older.value = detail.hasOlder ?? false
|
|
older.value = detail.hasOlder ?? false
|
|
|
- if (atBottom) void markRead().catch(() => {})
|
|
|
|
|
|
|
+
|
|
|
}
|
|
}
|
|
|
function flushEvents() {
|
|
function flushEvents() {
|
|
|
eventFrame = undefined
|
|
eventFrame = undefined
|
|
@@ -1025,7 +1003,7 @@ onBeforeUnmount(() => {
|
|
|
/></template>
|
|
/></template>
|
|
|
<div v-show="!twoDesktops" class="conversation-with-computer">
|
|
<div v-show="!twoDesktops" class="conversation-with-computer">
|
|
|
<section class="workspace-chat" aria-label="聊天" @click.capture="openTaskLink">
|
|
<section class="workspace-chat" aria-label="聊天" @click.capture="openTaskLink">
|
|
|
- <MessageTimeline ref="timeline" :identity="`${selected}:${activeTask?.id ?? ''}`" :loading-older="loadingOlder" :messages="uiMessages" :title="activeHeaderTitle"
|
|
|
|
|
|
|
+ <MessageTimeline @visible-messages="markVisible" ref="timeline" :identity="`${selected}:${activeTask?.id ?? ''}`" :loading-older="loadingOlder" :messages="uiMessages" :title="activeHeaderTitle"
|
|
|
:subtitle="active?.kind === 'group' ? `${members.length} 位成员` : ''"
|
|
:subtitle="active?.kind === 'group' ? `${members.length} 位成员` : ''"
|
|
|
:header-avatar-name="activeHeaderAvatarName" :header-avatar="activeHeaderAvatar" :header-avatar-kind="active?.kind === 'group' ? 'team' : 'agent'"
|
|
:header-avatar-name="activeHeaderAvatarName" :header-avatar="activeHeaderAvatar" :header-avatar-kind="active?.kind === 'group' ? 'team' : 'agent'"
|
|
|
:header-avatar-members="activeHeaderAvatarMembers" :header-avatar-state="activeHeaderAgent ? workspaceAvatarState(active, activeHeaderAgent.id) : 'idle'" :header-avatar-activity-key="active?.lastSeq"
|
|
:header-avatar-members="activeHeaderAvatarMembers" :header-avatar-state="activeHeaderAgent ? workspaceAvatarState(active, activeHeaderAgent.id) : 'idle'" :header-avatar-activity-key="active?.lastSeq"
|