重构: P0前端架构(useAiSend/useAiEvents/useToolCard拆分+status union全)
squash合并: - P0-1 useAiSend审批抽离(useAiApproval) - P0-2 useAiEvents switch拆3域handler - P0-3 useToolCard formatToolResult策略表 - P0-4 status union(Idea+Task+Project+Workflow+AiToolCall)
This commit is contained in:
@@ -4,6 +4,14 @@
|
||||
//! - _unlistenAiEvent / _unlistenConvChanged: 已注册的 unlistener
|
||||
//! - _startPromise: startListener 并发去重(防 onMounted 与 sendMessage 首发竞态重复注册)
|
||||
//!
|
||||
//! handleEvent 结构(fe-arch P0-2 拆分后):
|
||||
//! - 外围逻辑(convId 同步/路由/看门狗)留在 handleEvent
|
||||
//! - 14 个 case 按域分派到 3 个 handler:
|
||||
//! - handleStreamingEvent:AiTextDelta/AiAgentRound/AiHeartbeat/AiStreamRetry/AiMaxRoundsReached
|
||||
//! - handleToolEvent:AiToolCallStarted/AiToolCallCompleted/AiToolAutoApproved/AiApprovalRequired/AiApprovalResult
|
||||
//! - handleLifecycleEvent:AiCompleted/AiError
|
||||
//! 各 handler 共享模块级 helper(state/aiShared/审批计时器/工具慢执行计时器)。
|
||||
//!
|
||||
//! 耦合:
|
||||
//! - handleEvent 调 useAiConversations.loadConversations、
|
||||
//! useAiStream.{resetStreamWatchdog,clearStreamWatchdog}、本模块 flushCurrentText/findToolCall/notifyConversationChanged
|
||||
@@ -115,36 +123,28 @@ function isShowTokenUsage(): boolean {
|
||||
return appSettings.get<boolean>('df-show-token-usage', false)
|
||||
}
|
||||
|
||||
/** 后端事件分发:按 conversation_id 路由,流式累积文本,工具状态流转,看门狗联动 */
|
||||
export function handleEvent(event: AiChatEvent) {
|
||||
const convId = event.conversation_id
|
||||
// 首次收到事件时同步当前对话 id(后端自动建对话的场景)
|
||||
// 同步写 appSettings(SQLite):刷新页面后 loadConversations 据此恢复上次会话
|
||||
if (convId && !state.activeConversationId) {
|
||||
state.activeConversationId = convId
|
||||
void appSettings.set('df-ai-active-conv', convId)
|
||||
}
|
||||
// 事件不属于当前展示对话(生成中切走了)→ 不污染当前视图,仅完成/错误时刷新侧边栏
|
||||
const isCurrent = !convId || convId === state.activeConversationId
|
||||
if (!isCurrent) {
|
||||
if (event.type === 'AiCompleted' || event.type === 'AiError') {
|
||||
state.generatingConvId = null
|
||||
void loadConversations()
|
||||
}
|
||||
return
|
||||
}
|
||||
// 标记正在生成的对话(完成/错误事件除外)
|
||||
if (convId && event.type !== 'AiCompleted' && event.type !== 'AiError') {
|
||||
state.generatingConvId = convId
|
||||
}
|
||||
// 流式看门狗:活跃事件(delta/工具/新轮/审批结果)重置;审批等待/完成/错误在 case 内 clear
|
||||
if (!NO_RESET_WATCHDOG.has(event.type)) {
|
||||
resetStreamWatchdog()
|
||||
}
|
||||
// ─── 事件域 handler(从原 handleEvent switch 抽出,按域分组)────────────────────────────
|
||||
//
|
||||
// 拆分策略:策略 C(抽 case 体为函数)+ 按域分组(streaming/tool/lifecycle)。
|
||||
// handleEvent 保留外围逻辑(convId 同步/路由/看门狗),switch 改为按 event.type 分派到
|
||||
// 对应域 handler。各 handler 共享模块级 helper(state/aiShared/审批计时器/工具慢执行计时器)。
|
||||
// 零行为变更:逻辑等价搬运,case 体一字未改。
|
||||
//
|
||||
// 域分组:
|
||||
// - streaming 流式:AiTextDelta / AiAgentRound / AiHeartbeat / AiStreamRetry / AiMaxRoundsReached
|
||||
// - tool 工具:AiToolCallStarted / AiToolCallCompleted / AiToolAutoApproved / AiApprovalRequired / AiApprovalResult
|
||||
// - lifecycle 生命周期:AiCompleted / AiError
|
||||
//
|
||||
// 返回值:true=已处理(命中域内 case);false=未命中(供 handleEvent 兜底/未来扩展)
|
||||
// 例外:AiMaxRoundsReached 在 streaming 域(语义属流式中断信号),但 lifecycle 域收尾时
|
||||
// 需置 pendingMaxRounds=false —— 该 ref 为模块级,跨域共享无碍。
|
||||
|
||||
/** streaming 域:流式文本累积/新轮/心跳/重试/max 轮暂停 */
|
||||
function handleStreamingEvent(event: AiChatEvent): boolean {
|
||||
switch (event.type) {
|
||||
case 'AiTextDelta':
|
||||
state.currentText += event.delta
|
||||
break
|
||||
return true
|
||||
|
||||
case 'AiAgentRound': {
|
||||
// Agent 循环新一轮:保存当前文本到上一条 assistant 消息,新建空 assistant 消息
|
||||
@@ -162,12 +162,12 @@ export function handleEvent(event: AiChatEvent) {
|
||||
// 此时实际轮次尚未推进(run_agentic_loop 入口 iteration=0),不覆盖,避免审批通过瞬间
|
||||
// 进度条 2→0 闪烁;下一轮真正的 round>0 事件到达时再更新。
|
||||
if (event.round > 0) state.agentRound = event.round
|
||||
break
|
||||
return true
|
||||
}
|
||||
|
||||
case 'AiHeartbeat':
|
||||
// 心跳事件:仅维持看门狗(已在上方 reset),无需额外处理;显式 case 防 switch 穿透
|
||||
break
|
||||
return true
|
||||
|
||||
case 'AiStreamRetry': {
|
||||
// UX-260618-15: 流前失败重试中——后端 emit AiStreamRetry 携带 attempt/max_attempts。
|
||||
@@ -188,7 +188,7 @@ export function handleEvent(event: AiChatEvent) {
|
||||
timestamp: Date.now(),
|
||||
} as AiMessage)
|
||||
}
|
||||
break
|
||||
return true
|
||||
}
|
||||
|
||||
case 'AiMaxRoundsReached':
|
||||
@@ -196,8 +196,16 @@ export function handleEvent(event: AiChatEvent) {
|
||||
// 后端已 save 落库,这里仅翻 pendingMaxRounds=true 驱动 UI 卡片;看门狗已由
|
||||
// NO_RESET_WATCHDOG 跳过 reset(达 max 不计整流超时)。
|
||||
pendingMaxRounds.value = true
|
||||
break
|
||||
return true
|
||||
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/** tool 域:工具卡片状态流转/会话级信任/审批 */
|
||||
function handleToolEvent(event: AiChatEvent): boolean {
|
||||
switch (event.type) {
|
||||
case 'AiToolCallStarted': {
|
||||
const info: AiToolCallInfo = {
|
||||
id: event.id,
|
||||
@@ -207,7 +215,7 @@ export function handleEvent(event: AiChatEvent) {
|
||||
}
|
||||
// B-260616-21: 同 tool_call_id 重复 emit Started 时(GLM anthropic_compat id 不稳),
|
||||
// 仅挂计时器不重复 push(对齐 startToolSlowTimer 守卫风格),防残留 running 空卡(0 行·N KB)。
|
||||
// 详 docs/02-架构设计/B-260616-21排查方案-2026-06-16.md 方案①治标(后端治本待取证)。
|
||||
// 详 docs/02-架构设计/已编号方案/B-260616-21排查方案-2026-06-16.md 方案①治标(后端治本待取证)。
|
||||
if (!findToolCall(event.id)) {
|
||||
const lastMsg = state.messages[state.messages.length - 1]
|
||||
if (lastMsg && lastMsg.role === 'assistant') {
|
||||
@@ -217,7 +225,7 @@ export function handleEvent(event: AiChatEvent) {
|
||||
}
|
||||
// B-260616-12: 工具开始执行即挂慢执行计时器(超时仅 toast,不动 running 态)
|
||||
startToolSlowTimer(event.id, event.name)
|
||||
break
|
||||
return true
|
||||
}
|
||||
|
||||
case 'AiToolCallCompleted': {
|
||||
@@ -230,7 +238,7 @@ export function handleEvent(event: AiChatEvent) {
|
||||
clearToolSlowTimer(event.id)
|
||||
// AE-2025-06: 工具结束(无论审批通过后执行还是被拒),清审批超时计时器
|
||||
clearApprovalTimer(event.id)
|
||||
break
|
||||
return true
|
||||
}
|
||||
|
||||
case 'AiToolAutoApproved': {
|
||||
@@ -238,7 +246,7 @@ export function handleEvent(event: AiChatEvent) {
|
||||
// 仅经事件总线桥接到 AiChat.vue 弹本地 toast(composable 无组件上下文,与
|
||||
// ai-tool-slow-toast 同款中转模式)。主窗口与分离窗口各挂一份 AiChat,各自消费。
|
||||
void emit('ai-tool-auto-approved-toast', { tool: event.tool, dir: event.dir })
|
||||
break
|
||||
return true
|
||||
}
|
||||
|
||||
case 'AiApprovalRequired': {
|
||||
@@ -261,7 +269,7 @@ export function handleEvent(event: AiChatEvent) {
|
||||
clearToolSlowTimer(event.id)
|
||||
// AE-2025-06: 审批等待开始→启动审批超时计时器(5min 不处理自动拒绝)
|
||||
startApprovalTimer(event.id, event.name)
|
||||
break
|
||||
return true
|
||||
}
|
||||
|
||||
case 'AiApprovalResult': {
|
||||
@@ -281,9 +289,17 @@ export function handleEvent(event: AiChatEvent) {
|
||||
// B-260616-12: 审批通过→工具重新进入执行态,重启慢执行计时器
|
||||
startToolSlowTimer(event.id, findToolCall(event.id)?.name || '')
|
||||
}
|
||||
break
|
||||
return true
|
||||
}
|
||||
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/** lifecycle 域:整轮收尾(AiCompleted)与异常中断(AiError) */
|
||||
function handleLifecycleEvent(event: AiChatEvent): boolean {
|
||||
switch (event.type) {
|
||||
case 'AiCompleted': {
|
||||
clearStreamWatchdog()
|
||||
clearAllToolSlowTimers() // B-260616-12: 整轮结束清全部工具慢执行计时器与已提示集合
|
||||
@@ -326,7 +342,7 @@ export function handleEvent(event: AiChatEvent) {
|
||||
notifyConversationChanged()
|
||||
// 队列续发:当前完成后自动发下一条(经事件总线桥接,避免 import useAiSend 构成循环依赖)
|
||||
emit('ai-drain-queue', {})
|
||||
break
|
||||
return true
|
||||
}
|
||||
|
||||
case 'AiError': {
|
||||
@@ -355,11 +371,46 @@ export function handleEvent(event: AiChatEvent) {
|
||||
errorType: event.error_type,
|
||||
timestamp: Date.now(),
|
||||
} as AiMessage)
|
||||
break
|
||||
return true
|
||||
}
|
||||
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/** 后端事件分发:按 conversation_id 路由,流式累积文本,工具状态流转,看门狗联动 */
|
||||
export function handleEvent(event: AiChatEvent) {
|
||||
const convId = event.conversation_id
|
||||
// 首次收到事件时同步当前对话 id(后端自动建对话的场景)
|
||||
// 同步写 appSettings(SQLite):刷新页面后 loadConversations 据此恢复上次会话
|
||||
if (convId && !state.activeConversationId) {
|
||||
state.activeConversationId = convId
|
||||
void appSettings.set('df-ai-active-conv', convId)
|
||||
}
|
||||
// 事件不属于当前展示对话(生成中切走了)→ 不污染当前视图,仅完成/错误时刷新侧边栏
|
||||
const isCurrent = !convId || convId === state.activeConversationId
|
||||
if (!isCurrent) {
|
||||
if (event.type === 'AiCompleted' || event.type === 'AiError') {
|
||||
state.generatingConvId = null
|
||||
void loadConversations()
|
||||
}
|
||||
return
|
||||
}
|
||||
// 标记正在生成的对话(完成/错误事件除外)
|
||||
if (convId && event.type !== 'AiCompleted' && event.type !== 'AiError') {
|
||||
state.generatingConvId = convId
|
||||
}
|
||||
// 流式看门狗:活跃事件(delta/工具/新轮/审批结果)重置;审批等待/完成/错误在 case 内 clear
|
||||
if (!NO_RESET_WATCHDOG.has(event.type)) {
|
||||
resetStreamWatchdog()
|
||||
}
|
||||
// 按域分派(streaming → tool → lifecycle);未命中任一域 → 无操作(保留原 switch 无 default 的语义)
|
||||
if (handleStreamingEvent(event)) return
|
||||
if (handleToolEvent(event)) return
|
||||
void handleLifecycleEvent(event)
|
||||
}
|
||||
|
||||
/** 启动事件监听(幂等 + 并发去重,onMounted 与 sendMessage 首发竞态不会重复注册) */
|
||||
export async function startListener() {
|
||||
// 已注册 → 直接复用(幂等;sendMessage 每次调用不重复注册)
|
||||
|
||||
Reference in New Issue
Block a user