重构: AI流式断线保文StreamResult三分支+重试对齐决策a1,推进链落df-nodes
This commit is contained in:
@@ -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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user