//! 发送与队列管理 — sendMessage/approveToolCall + 队列(drain/cancel/clear) + stopChat //! //! 模块级私有: //! - 无(队列存在 state.queue 中,看门狗/审批状态机依赖 events 与 stream) //! //! 耦合: //! - sendMessage 调 useAiEvents.startListener(useAiEvents 导出但不在解构集中暴露给组件, //! 故 startListener 同时经 useAiEvents 导出,sendMessage 直接 import 调用) //! - sendMessage 调 useAiStream.resetStreamWatchdog //! - drainQueue 在 sendMessage 完成后由 handleEvent(AiCompleted) 调用 — 故 drainQueue 必须为模块级 export import { aiApi } from '../../api' import { useAppSettingsStore } from '../../stores/appSettings' import { state } from '../../stores/ai' import { resetStreamWatchdog, clearStreamWatchdog } from './useAiStream' import { startListener, findToolCall } from './useAiEvents' import { nextMsgId } from './aiShared' const appSettings = useAppSettingsStore() /// 待发送队列上限(超过抛错提示用户) const QUEUE_LIMIT = 10 /** 取出队首并发送(AiCompleted 触发,此时 streaming 已 false) */ export function drainQueue() { if (state.queue.length === 0) return const next = state.queue.shift()! void sendMessage(next.text, next.skill) } /** 发送消息:生成中入队,否则推送 user+air 气泡并触发后端流式 */ async function sendMessage(text: string, skill?: string) { if (!text.trim()) return // 生成中:进入待发送队列,当前对话完成后(AiCompleted)由 drainQueue 自动续发 if (state.streaming) { if (state.queue.length >= QUEUE_LIMIT) { throw new Error(`待发送队列已满(最多 ${QUEUE_LIMIT} 条)`) } state.queue.push({ text: text.trim(), skill: skill || undefined }) return } const userMsgId = `user-${nextMsgId()}` state.messages.push({ id: userMsgId, role: 'user', content: text.trim(), timestamp: Date.now(), }) const aiMsgId = `ai-${nextMsgId()}` state.messages.push({ id: aiMsgId, role: 'assistant', content: '', timestamp: Date.now(), }) state.streaming = true state.currentText = '' resetStreamWatchdog() // 启动流式看门狗,无数据超时兜底 await startListener() const raw = appSettings.get('df-ai-language', 'auto') const lang = raw === 'auto' ? appSettings.get('df-language', 'zh-CN') : raw try { await aiApi.sendMessage(text.trim(), lang, skill) } catch (e) { // IPC 失败(spawn 前/provider 配置错等):回滚 streaming 防光标卡死 + 移除 user 消息与空气泡占位; // 重新抛出由 handleSend 回填输入框,用户可重试 state.streaming = false clearStreamWatchdog() // 清 :61 启动的看门狗,防 130s 后 onStreamTimeout 误推错误气泡 state.messages = state.messages.filter(m => m.id !== aiMsgId && m.id !== userMsgId) throw e } } /** 工具审批:乐观置 running,IPC 失败时改 completed+错误文案(不回滚避免卡按钮) */ async function approveToolCall(toolCallId: string, approved: boolean) { // 乐观置运行中,禁用审批按钮防重复点击(后端事件回来后转 completed/rejected) // 复用 findToolCall(反向扫描)避免重复实现查找逻辑 const tc = findToolCall(toolCallId) if (tc) tc.status = 'running' // 重启看门狗覆盖审批执行→续生成窗口(useAiEvents.ts:162 AiApprovalRequired 已 clear, // 审批态无心跳兜底;approve 后后端要跑工具+续生成,期间任何后端异常不回则按钮永久 running。 // 复用通用 watchdog 而非加审批专用超时:复用同一收尾路径(onStreamTimeout 兜底复位 streaming), // 避免新增独立计时器与状态分支) resetStreamWatchdog() try { await aiApi.approve(toolCallId, approved) } catch (e) { // IPC 未送达:用户已点过按钮,清看门狗(避免 130s 后误触发假错误)再回滚到失败态 clearStreamWatchdog() state.queue = [] // B-32:IPC 失败即对话结束,清队列防生成中入队消息静默丢失 // 仅 IPC 真失败(审批已处理/网络断)走到这里:后端工具失败已改走 emit completed,不进此分支 // 不回滚 pending_approval(会卡死按钮),改设 completed + 错误提示,让用户知晓失败 console.error('[AI] 审批操作未送达后端:', e) if (tc) { tc.status = 'completed' tc.result = `审批操作未送达后端:${e instanceof Error ? e.message : String(e)}` } throw e } } /** 取消队列中指定位置的消息 */ function cancelQueued(index: number) { state.queue.splice(index, 1) } /** 清空整个待发送队列 */ function clearQueue() { state.queue = [] } /** 停止当前生成:本地先复位 streaming,再发停止信号。 * 不依赖后端 AiCompleted 收尾——审批态看门狗已 clear(useAiEvents),若 AiCompleted 竞态丢失则 streaming 永久 true 卡死,故本地兜底。 */ async function stopChat() { state.streaming = false // 本地先复位,不依赖后端 AiCompleted(审批态看门狗已 clear,竞态丢失则卡死) clearStreamWatchdog() // 清残留看门狗 state.queue = [] // B-32:用户主动停止,清队列防生成中入队的消息被静默丢弃 await aiApi.stopChat() } export function useAiSend() { return { sendMessage, approveToolCall, drainQueue, cancelQueued, clearQueue, stopChat, } }