重构: AI流式断线保文StreamResult三分支+重试对齐决策a1,推进链落df-nodes

This commit is contained in:
lxy
2026-06-16 19:11:19 +08:00
parent 7d5cd4c89a
commit ba7f35552b
30 changed files with 1325 additions and 283 deletions
+83 -5
View File
@@ -1,13 +1,21 @@
//! AI composables 共享层 — 多 composable 共用的模块级状态与纯函数
//!
//! 存在意义:打破 useAiEvents ↔ useAiStream 之间的循环依赖。
//! - useAiStream.onStreamTimeout 调用 nextMsgId
//! - useAiEvents.handleEvent 调用 useAiStream 的 resetStreamWatchdog/clearStreamWatchdog
//! 把两者互相依赖的 nextMsgId(全局消息 id 自增计数器,无其他依赖的纯函数 + 模块级状态)
//! 下沉到本模块,使 useAiStream 改为依赖 aiShared 而非 useAiEvents,环即消除。
//! 存在意义:打破 composable 间循环依赖。
//! - 原环1(useAiEvents ↔ useAiStream):nextMsgId 下沉到本模块消除
//! - 原环2(useAiEvents ↔ useAiSend):findToolCall + 审批计时器 下沉到本模块消除
//!
//! 下沉范围(严格限定为「无依赖、被多个 composable 共用」的成员):
//! - nextMsgId / _msgCounter:events/stream/send/window 四个 composable 共用同一计数器
//! - findToolCall:events/send 两个 composable 共用的纯查找函数(仅读 state.messages)
//! - 审批计时器(startApprovalTimer/clearApprovalTimer/clearAllApprovalTimers):
//! events 模块触发(start/clear/allClear),send 模块也需导出给组件,故下沉到共享层
import { aiApi } from '@/api'
import { state } from '@/stores/ai'
import i18n from '@/i18n'
import type { AiToolCallInfo } from '@/api/types'
const t = ((i18n as any).global.t as (k: string, named?: Record<string, unknown>) => string).bind((i18n as any).global)
/** 客户端消息自增 id(全局唯一,多个 composable 共用 nextMsgId) */
let _msgCounter = 0
@@ -16,3 +24,73 @@ let _msgCounter = 0
export function nextMsgId(): number {
return ++_msgCounter
}
/**
* 在全部消息中查找指定 id 的工具调用卡片(用于状态流转)
*
* 扫描策略:从尾部反向遍历并提前退出。tool_result/approval/completed
* 通常对应最近触发的 tool_use(agent 顺序执行工具并尽快回填结果),
* 反向扫描使其命中落在尾部 → 平均 O(1)、最坏 O(n)。(原正向全量扫描为 O(n²),
* 每条 tool_result 都从头扫整条历史。)
*
* 注:未引入 Map<id, tc> 索引。state.messages 在 newConversation /
* switchConversation / deleteConversation 等处被整体替换(且这些操作散落在
* useAiEvents 之外),独立索引极易与响应式数组失配形成"陈旧引用"——状态卡
* 片读到已不存在的对象。反向扫描零额外状态、改动最小且行为完全一致。
*/
export function findToolCall(id: string): AiToolCallInfo | undefined {
for (let i = state.messages.length - 1; i >= 0; i--) {
const toolCalls = state.messages[i].toolCalls
if (!toolCalls) continue
// 同一消息内工具调用通常很少,沿用正向扫描;命中即返回
for (let j = 0; j < toolCalls.length; j++) {
if (toolCalls[j].id === id) return toolCalls[j]
}
}
return undefined
}
// ── 审批超时计时器(原 useAiSend,下沉打破 events↔send 循环依赖) ──
/// 审批等待超时阈值(ms) — 用户 5 分钟内不处理则自动拒绝
const APPROVAL_TIMEOUT_MS = 5 * 60 * 1000
/** toolCallId → 审批超时计时器 */
const _approvalTimers = new Map<string, ReturnType<typeof setTimeout>>()
/**
* 启动审批超时计时器(幂等:同 id 重复启动不重建)。
* 到点回调:调 ai_approve(id, false) 自动拒绝 + push 系统错误消息。
*/
export function startApprovalTimer(toolCallId: string, toolName: string): void {
if (_approvalTimers.has(toolCallId)) return
const timer = setTimeout(() => {
_approvalTimers.delete(toolCallId)
aiApi.approve(toolCallId, false).catch(e => {
console.error('[AI] 审批超时自动拒绝 IPC 未送达:', e)
})
state.messages.push({
id: `approval-timeout-${nextMsgId()}`,
role: 'assistant',
content: t('ai.approvalTimeout', { toolName }),
isError: true,
timestamp: Date.now(),
})
}, APPROVAL_TIMEOUT_MS)
_approvalTimers.set(toolCallId, timer)
}
/** 清除单个审批计时器 */
export function clearApprovalTimer(toolCallId: string): void {
const timer = _approvalTimers.get(toolCallId)
if (timer) {
clearTimeout(timer)
_approvalTimers.delete(toolCallId)
}
}
/** 清除全部审批计时器(整流超时收尾/组件卸载时调用) */
export function clearAllApprovalTimers(): void {
for (const timer of _approvalTimers.values()) clearTimeout(timer)
_approvalTimers.clear()
}