新增: 无人值守P0任务边界全链
- 任务级授权边界判定与审批旁路单点插桩 - boundary_json 随会话落库(V45 可逆迁移) - 六路非正常终局统一安全停靠与托盘通知 - 每五轮进度事件与会话头部进度条 - 前端边界配置卡侧栏徽标及 UX 校验补齐
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
//! useAiEvents() 返回 shape 保持不变(stores/ai.ts spread 形状不变 → 组件零改动)。
|
||||
|
||||
import { listen, emit } from '@tauri-apps/api/event'
|
||||
import { t } from '@/i18n/i18n-helpers'
|
||||
import { aiApi } from '@/api'
|
||||
import { useAppSettingsStore } from '@/stores/appSettings'
|
||||
import { state } from '@/stores/ai'
|
||||
@@ -19,9 +20,11 @@ import { nextMsgId, setConvState, convStates, switchingConvs, getConvStreamState
|
||||
import { resetStreamWatchdog, clearAllStreamWatchdogs, clearResumeAttempts } from './useAiStream'
|
||||
import { setStreaming } from './streamingGuard'
|
||||
import { scheduleConversationsRefresh } from './useAiConversations'
|
||||
import { pendingTaskParked, pendingTaskProgress } from './useAiPendingState'
|
||||
import { clearConvBoundaryCache } from './useTaskBoundary'
|
||||
import { handleStreamingEvent } from './useAiStreamingEvents'
|
||||
import { handleToolEvent } from './useAiToolEvents'
|
||||
import { handleLifecycleEvent } from './useAiLifecycleEvents'
|
||||
import { handleLifecycleEvent, recordTaskParked } from './useAiLifecycleEvents'
|
||||
import type { AiChatEvent, MessageId } from '@/api/types'
|
||||
|
||||
// 跨域共享 helper re-export(原为 useAiEvents 导出,拆分后从 aiShared/叶子透传,消费方 import 路径不变)
|
||||
@@ -40,9 +43,12 @@ let _startPromise: Promise<void> | null = null
|
||||
|
||||
const appSettings = useAppSettingsStore()
|
||||
|
||||
// 看门狗不重置的事件集合(审批等待/完成/错误/达 max/路径授权/求助由各自 case 内 clear)。
|
||||
// 看门狗不重置的事件集合(审批等待/完成/错误/达 max/路径授权/求助/停靠由各自 case 内 clear)。
|
||||
// 模块级 Set 复用,避免每事件(delta/token 高频)新建数组字面量做 includes。
|
||||
const NO_RESET_WATCHDOG = new Set<AiChatEvent['type']>(['AiApprovalRequired', 'AiCompleted', 'AiError', 'AiMaxRoundsReached', 'AiDirAuthRequired', 'AiHelpRequired'])
|
||||
const NO_RESET_WATCHDOG = new Set<AiChatEvent['type']>(['AiApprovalRequired', 'AiCompleted', 'AiError', 'AiMaxRoundsReached', 'AiDirAuthRequired', 'AiHelpRequired', 'AiTaskParked'])
|
||||
|
||||
// 后台会话完成 toast 的摘要截长(轻提醒只够判断「做完了/值不值得切回去看」)
|
||||
const AGENT_FINISHED_TOAST_SUMMARY_MAX = 40
|
||||
|
||||
/** 全局审批类事件:跨会话放行,不因非当前会话被过滤。审批挂起是等用户决策的跨会话资产,
|
||||
* 丢弃后用户在切回该会话前没有任何感知与处理入口;归口仍按 conversation_id 写 per-conv
|
||||
@@ -73,6 +79,8 @@ function handleAgentFinishedEvent(event: AiChatEvent): boolean {
|
||||
status: AgentFinishedStatus
|
||||
rounds: number
|
||||
tokens: number
|
||||
rejected_tools?: string[] | null
|
||||
stop_kind?: string | null
|
||||
}
|
||||
const convId = event.conversation_id
|
||||
if (!convId || !ev.agent_id) return true
|
||||
@@ -82,13 +90,73 @@ function handleAgentFinishedEvent(event: AiChatEvent): boolean {
|
||||
status: ev.status,
|
||||
rounds: ev.rounds,
|
||||
tokens: ev.tokens,
|
||||
// 后端 Option→null/缺省,卡片语义统一为缺省=零拒绝
|
||||
rejected_tools: ev.rejected_tools ?? undefined,
|
||||
// 结构化终止原因(可选,旧后端不发):存卡供卡面状态词优先消费,替代 summary 启发式
|
||||
stop_kind: ev.stop_kind ?? undefined,
|
||||
})
|
||||
// P3-6:后台会话完成轻提醒(当前会话卡已在消息流可见,不弹)。
|
||||
// composable 无组件上下文,复用 ai-toast 事件总线先例(ai-tool-slow-toast),AiChat listen 弹本地 toast;
|
||||
// 多个子代理连发时 toast 单槽自然合并,无风暴面。
|
||||
if (convId !== state.activeConversationId) {
|
||||
// 未知 status 兜底按 completed 语气(防 t 回显键名);failed 用 warning 色与完成区分
|
||||
const statusKey = ev.status === 'failed' ? 'failed' : ev.status === 'truncated' ? 'truncated' : 'completed'
|
||||
const collapsed = ev.summary.replace(/\s+/g, ' ').trim()
|
||||
const summary = collapsed.length > AGENT_FINISHED_TOAST_SUMMARY_MAX
|
||||
? collapsed.slice(0, AGENT_FINISHED_TOAST_SUMMARY_MAX) + '…'
|
||||
: collapsed
|
||||
void emit('ai-toast', {
|
||||
msg: t('ai.agentFinishedToast', { status: t(`ai.agentFinished.${statusKey}`), summary }),
|
||||
type: ev.status === 'failed' ? 'warning' : 'info',
|
||||
})
|
||||
}
|
||||
// 子代理执行是该会话的真实活跃信号(父 loop 在 spawn_agent 内等待):此前该事件穿透到
|
||||
// 活跃事件分支会重置看门狗,现提前归集后显式补上,保持既有不误判停滞的语义。
|
||||
resetStreamWatchdog(convId)
|
||||
return true
|
||||
}
|
||||
|
||||
/** 无人值守停靠域:消费 AiTaskParked(per-conv 落卡 + 后台会话收敛,挂起家族终局)。
|
||||
* 返回 true=已处理。仅拦截后台会话:当前会话放行到 lifecycle 域统一收尾(看门狗/流式复位
|
||||
* 在 cleanupTerminatedConversation,后台分支此处的收敛集合与其对齐但不重复触发两次)。
|
||||
* 停靠卡必须 per-conv 落卡:多会话并发各自可停靠,后台会话的卡丢了用户切回前无从得知。 */
|
||||
function handleTaskParkedEvent(event: AiChatEvent): boolean {
|
||||
if (event.type !== 'AiTaskParked') return false
|
||||
const convId = event.conversation_id || state.activeConversationId
|
||||
if (convId && convId !== state.activeConversationId) {
|
||||
recordTaskParked(convId, event.reason, event.stop_kind ?? undefined)
|
||||
// 对齐 AiCompleted 家族后台分支的收敛:删状态项回不在生成、清 per-conv 流式态/去重记录/
|
||||
// 挂起审批集合/自动续跑计数/进度快照,再刷新侧栏(防抖调度)
|
||||
convStates.delete(convId)
|
||||
clearConvStreamState(convId)
|
||||
clearLastDelta(convId)
|
||||
discardConvPendingApprovals(convId)
|
||||
clearResumeAttempts(convId || undefined)
|
||||
pendingTaskProgress.delete(convId)
|
||||
scheduleConversationsRefresh()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/** 无人值守进度域:消费 AiTaskProgress(per-conv 快照,静默期「在干活」可见)。
|
||||
* 返回 true=已处理。isCurrent 守卫之前归集(同停靠卡范式):后台会话的进度也要落 Map,
|
||||
* 切回仍可见。进度事件即活体信号,顺带 resetStreamWatchdog(子代理长跑/重入退避期间
|
||||
* 后端心跳之外的第二重存活证明,防看门狗误判停滞)。 */
|
||||
function handleTaskProgressEvent(event: AiChatEvent): boolean {
|
||||
if (event.type !== 'AiTaskProgress') return false
|
||||
const convId = event.conversation_id || state.activeConversationId
|
||||
if (!convId) return true
|
||||
pendingTaskProgress.set(convId, {
|
||||
round: event.round,
|
||||
maxRounds: event.max_rounds,
|
||||
tokensUsed: event.tokens_used,
|
||||
updatedAt: Date.now(),
|
||||
})
|
||||
resetStreamWatchdog(convId)
|
||||
return true
|
||||
}
|
||||
|
||||
/** 跨端用户消息域:miniapp→桌面 user 气泡同步(仅处理 AiUserMessage)。
|
||||
* 去重:末条已是同 content 的 user 则跳过(防本地乐观 push + 事件回灌双气泡)。 */
|
||||
function handleUserMessageEvent(event: AiChatEvent): boolean {
|
||||
@@ -128,6 +196,10 @@ export function handleEvent(event: AiChatEvent) {
|
||||
if (handleUserMessageEvent(event)) return
|
||||
// S1 子代理完成卡归集:在 isCurrent 守卫之前处理(per-conv 落卡,后台会话不丢卡)
|
||||
if (handleAgentFinishedEvent(event)) return
|
||||
// 无人值守停靠卡归集:在 isCurrent 守卫之前处理(per-conv 落卡,后台会话不丢卡)
|
||||
if (handleTaskParkedEvent(event)) return
|
||||
// 无人值守进度快照归集:在 isCurrent 守卫之前处理(per-conv 落 Map,后台会话不丢进度)
|
||||
if (handleTaskProgressEvent(event)) return
|
||||
// 事件不属于当前展示对话(生成中切走了)→ 不污染当前视图,仅完成/错误/求助时刷新侧边栏;
|
||||
// 全局审批类事件例外(isGlobalApprovalEvent),放行到下方按域分派
|
||||
const isCurrent = !convId || convId === state.activeConversationId
|
||||
@@ -145,6 +217,8 @@ export function handleEvent(event: AiChatEvent) {
|
||||
clearLastDelta(convId || '')
|
||||
discardConvPendingApprovals(convId || '')
|
||||
clearResumeAttempts(convId || undefined)
|
||||
// 终态即清进度快照(事件即逝态,会话已终止不留陈旧「进行中」)
|
||||
pendingTaskProgress.delete(convId || '')
|
||||
// FE1:后台会话终止只刷新列表(防抖调度,避免与 AiCompleted 收尾的 notify 双拉)。
|
||||
scheduleConversationsRefresh()
|
||||
// 后台会话终止也触发队列续发/清队(完成续发;错误清该会话队列)
|
||||
@@ -168,8 +242,10 @@ export function handleEvent(event: AiChatEvent) {
|
||||
// 无目标会话(事件缺 convId 且当前无活跃会话):流式 delta 无处累积,跳过写防 null 键无意义写入
|
||||
if (!convId && !state.activeConversationId && event.type === 'AiTextDelta') return
|
||||
// 活跃事件(delta/工具/新轮/审批结果)到达即标记该会话生成中(enum 兜底写入口;
|
||||
// 后端 AiConvStateChanged{generating} 已先到时同值 set 幂等覆盖,无副作用)
|
||||
if (convId && event.type !== 'AiCompleted' && event.type !== 'AiError' && event.type !== 'AiHelpRequired') {
|
||||
// 后端 AiConvStateChanged{generating} 已先到时同值 set 幂等覆盖,无副作用)。
|
||||
// 终态家族(AiCompleted/AiError/AiHelpRequired/AiTaskParked)除外:标记生成中会与
|
||||
// 收尾的删项收敛打架(停靠后端已复位 generating,不能被兜底写回)
|
||||
if (convId && event.type !== 'AiCompleted' && event.type !== 'AiError' && event.type !== 'AiHelpRequired' && event.type !== 'AiTaskParked') {
|
||||
setConvState(convId, 'generating')
|
||||
}
|
||||
// 流式看门狗:活跃事件重置;审批等待/完成/错误在 case 内 clear(per-conv,各会话独立计时)
|
||||
@@ -206,6 +282,10 @@ export async function startListener() {
|
||||
// S1:会话删除连带清其完成卡(Map 不持已删会话条目;存在即清,不限当前视图)
|
||||
if (deleted) {
|
||||
clearConvAgentFinished(deleted)
|
||||
// 无人值守三件随会话同亡:停靠卡挂起 + 进度快照 + 边界镜像(边界生命周期=会话存续,设计 §二)
|
||||
pendingTaskParked.delete(deleted)
|
||||
pendingTaskProgress.delete(deleted)
|
||||
clearConvBoundaryCache(deleted)
|
||||
}
|
||||
})
|
||||
_unlistenApprovalClear = await listen('ai-approval-clear-timers', () => { clearAllApprovalTimers() })
|
||||
|
||||
Reference in New Issue
Block a user