From 8720b9424d798e8bfb7dab89d43d5a7cf7a98d9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BB=9D=E5=B0=98?= <237809796@qq.com> Date: Mon, 15 Jun 2026 05:25:38 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84+=E4=BF=AE=E5=A4=8D:=20ARC-06?= =?UTF-8?q?=20composables/ai=20=E5=BE=AA=E7=8E=AF=E4=BE=9D=E8=B5=96?= =?UTF-8?q?=E7=A0=B4=E7=8E=AF=20+=20B-34=20=E5=A4=9A=E9=80=89=E5=AE=A1?= =?UTF-8?q?=E6=89=B9=20select=5Ftype=20=E5=9B=9E=E5=BD=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ARC-06: 提 aiShared.ts 下沉 nextMsgId+_msgCounter(叶子模块),useAiEvents/useAiStream/useAiSend/useAiWindow import 源改 aiShared,useAiStream 不再 import useAiEvents 环消除。watchdog 三函数留 useAiStream(持计时器状态非纯函数)。行为零变化。 - B-34(另一会话走查发现 P0): stores/project.ts:273 selectType→select_type(Tauri 2 IPC 不转 camelCase),对齐后端 workflow.rs:211。F-260615-01 多选审批回归修复——前端传 selectType 后端收 None 归一化 Single 致多选被拒。删错误注释。 批1 wwllb4ith(ARC-06)+主代理(B-34),vue-tsc 0 err --- src/composables/ai/aiShared.ts | 18 ++++++++++++++++++ src/composables/ai/useAiEvents.ts | 10 ++-------- src/composables/ai/useAiSend.ts | 2 +- src/composables/ai/useAiStream.ts | 8 ++++++-- src/composables/ai/useAiWindow.ts | 4 ++-- src/stores/project.ts | 2 +- 6 files changed, 30 insertions(+), 14 deletions(-) create mode 100644 src/composables/ai/aiShared.ts diff --git a/src/composables/ai/aiShared.ts b/src/composables/ai/aiShared.ts new file mode 100644 index 0000000..d027497 --- /dev/null +++ b/src/composables/ai/aiShared.ts @@ -0,0 +1,18 @@ +//! AI composables 共享层 — 多 composable 共用的模块级状态与纯函数 +//! +//! 存在意义:打破 useAiEvents ↔ useAiStream 之间的循环依赖。 +//! - useAiStream.onStreamTimeout 调用 nextMsgId +//! - useAiEvents.handleEvent 调用 useAiStream 的 resetStreamWatchdog/clearStreamWatchdog +//! 把两者互相依赖的 nextMsgId(全局消息 id 自增计数器,无其他依赖的纯函数 + 模块级状态) +//! 下沉到本模块,使 useAiStream 改为依赖 aiShared 而非 useAiEvents,环即消除。 +//! +//! 下沉范围(严格限定为「无依赖、被多个 composable 共用」的成员): +//! - nextMsgId / _msgCounter:events/stream/send/window 四个 composable 共用同一计数器 + +/** 客户端消息自增 id(全局唯一,多个 composable 共用 nextMsgId) */ +let _msgCounter = 0 + +/** 全局消息 id 自增(供 events/stream/send/window 各 composable 共享同一计数器) */ +export function nextMsgId(): number { + return ++_msgCounter +} diff --git a/src/composables/ai/useAiEvents.ts b/src/composables/ai/useAiEvents.ts index 3c21e0b..cd32cd9 100644 --- a/src/composables/ai/useAiEvents.ts +++ b/src/composables/ai/useAiEvents.ts @@ -3,18 +3,18 @@ //! 模块级私有状态(不进 reactive): //! - _unlistenAiEvent / _unlistenConvChanged: 已注册的 unlistener //! - _startPromise: startListener 并发去重(防 onMounted 与 sendMessage 首发竞态重复注册) -//! - _msgCounter: 客户端消息自增 id(全局唯一,多个 composable 共用 nextMsgId) //! //! 耦合: //! - handleEvent 调 useAiConversations.loadConversations、useAiSend.drainQueue、 //! useAiStream.{resetStreamWatchdog,clearStreamWatchdog}、本模块 flushCurrentText/findToolCall/notifyConversationChanged -//! - onStreamTimeout(useAiStream) 调本模块 nextMsgId +//! - nextMsgId 已下沉到 aiShared(原为本模块导出,useAiStream 亦依赖之构成循环依赖,故抽出) import { listen, emit } from '@tauri-apps/api/event' import { aiApi } from '../../api' import { useAppSettingsStore } from '../../stores/appSettings' import { state } from '../../stores/ai' import i18n from '../../i18n' +import { nextMsgId } from './aiShared' import { resetStreamWatchdog, clearStreamWatchdog } from './useAiStream' import { drainQueue } from './useAiSend' import { loadConversations } from './useAiConversations' @@ -29,15 +29,9 @@ let _unlistenConvChanged: (() => void) | null = null // startListener 并发去重:防 onMounted 与 sendMessage 首发竞态下重复注册 listener // (两回调写同一 state.currentText → 流式文字双倍) let _startPromise: Promise | null = null -let _msgCounter = 0 const appSettings = useAppSettingsStore() -/** 全局消息 id 自增(供 events/stream/send/window 各 composable 共享同一计数器) */ -export function nextMsgId(): number { - return ++_msgCounter -} - /** 通知会话列表发生变化(供 newConversation/deleteConversation/rename/archive 等触发刷新侧栏) */ export function notifyConversationChanged() { emit('ai-conversation-changed', {}) diff --git a/src/composables/ai/useAiSend.ts b/src/composables/ai/useAiSend.ts index bb243a6..691d9f3 100644 --- a/src/composables/ai/useAiSend.ts +++ b/src/composables/ai/useAiSend.ts @@ -14,7 +14,7 @@ import { useAppSettingsStore } from '../../stores/appSettings' import { state } from '../../stores/ai' import { resetStreamWatchdog, clearStreamWatchdog } from './useAiStream' import { startListener } from './useAiEvents' -import { nextMsgId } from './useAiEvents' +import { nextMsgId } from './aiShared' const appSettings = useAppSettingsStore() diff --git a/src/composables/ai/useAiStream.ts b/src/composables/ai/useAiStream.ts index f849089..0af4916 100644 --- a/src/composables/ai/useAiStream.ts +++ b/src/composables/ai/useAiStream.ts @@ -7,9 +7,12 @@ //! - sendMessage 启动生成时 resetStreamWatchdog() 启动计时 //! - handleEvent 每个活跃事件(delta/工具/新轮/审批结果)重置;审批等待/完成/错误 clear //! - 超时回调 onStreamTimeout 直接改 state 并补一条错误消息 +//! +//! 依赖:nextMsgId 取自 aiShared(原从 useAiEvents 取,构成 useAiEvents ↔ useAiStream +//! 循环依赖;下沉到 aiShared 后本模块不再 import useAiEvents,环消除) import { state } from '../../stores/ai' -import { nextMsgId } from './useAiEvents' +import { nextMsgId } from './aiShared' /// ≥ 后端 STREAM_IDLE_TIMEOUT(120s, ai.rs:848)+余量; /// 前端若短于后端,慢首 token 会被前端先误杀 @@ -26,7 +29,8 @@ export function onStreamTimeout() { // 文案区分:若已有 completed 工具调用,说明工具执行完后续回复被中断(常见为审批后 // 续生成卡住),引导用户继续;否则按纯流式中断处理。 // 数据源:state.messages[].toolCalls[].status,反向扫尾部提前退出(O(1) 均,同 findToolCall); - // 不复用 useAiEvents.findToolCall 避免反向 import 循环依赖(useAiEvents 已 import 本模块) + // 不复用 useAiEvents.findToolCall:本模块不依赖 useAiEvents(已下沉 nextMsgId 到 aiShared 破环), + // 且此处需遍历查找任一 completed 卡片而非按 id 定位,语义不同 let hasCompletedTool = false for (let i = state.messages.length - 1; i >= 0; i--) { const toolCalls = state.messages[i].toolCalls diff --git a/src/composables/ai/useAiWindow.ts b/src/composables/ai/useAiWindow.ts index db35b07..feacbe1 100644 --- a/src/composables/ai/useAiWindow.ts +++ b/src/composables/ai/useAiWindow.ts @@ -5,11 +5,11 @@ //! //! 耦合: //! - detachPanel/resumeInDetached 调 useAiConversations.switchConversation -//! - resumeInDetached 调 useAiEvents.nextMsgId 占位 ai 气泡 +//! - resumeInDetached 调 aiShared.nextMsgId 占位 ai 气泡 //! - reattachPanel/detachPanel 调 useAiPanel.persistUiState import { state } from '../../stores/ai' -import { nextMsgId } from './useAiEvents' +import { nextMsgId } from './aiShared' import { switchConversation } from './useAiConversations' import { persistUiState } from './useAiPanel' diff --git a/src/stores/project.ts b/src/stores/project.ts index d787005..065b8b6 100644 --- a/src/stores/project.ts +++ b/src/stores/project.ts @@ -270,7 +270,7 @@ function createStore() { comment, options: state.pendingApproval.options ?? [], decisions: decisions ?? [], - selectType: state.pendingApproval.select_type ?? 'single', // Tauri camelCase→snake_case 自动转 select_type + select_type: state.pendingApproval.select_type ?? 'single', // B-260615-34:Tauri 2 IPC 不转 camelCase,须 snake_case 对齐后端 workflow.rs:211(同 invoke 其余字段 execution_id/node_id/decisions 均已 snake_case) }) // 清除待审批状态