diff --git a/src/composables/ai/aiShared.ts b/src/composables/ai/aiShared.ts index 4dc80dd..8be95ea 100644 --- a/src/composables/ai/aiShared.ts +++ b/src/composables/ai/aiShared.ts @@ -12,12 +12,10 @@ import { reactive } from 'vue' import { aiApi } from '@/api' -import { state } from '@/stores/ai' import { useAppSettingsStore } from '@/stores/appSettings' import { t } from '@/i18n/i18n-helpers' -import type { AiToolCallInfo, ConvState } from '@/api/types' +import type { AiMessage, AiToolCallInfo, ConvState } from '@/api/types' -const appSettings = useAppSettingsStore() /** * 解析 AI 回复语言(useAiSend.sendMessage / useAiContext.compressContext 共用)。 @@ -29,12 +27,34 @@ const appSettings = useAppSettingsStore() * 语义相同仅写法不同,提取至此共用。 */ export function resolveAiLang(): string { + const appSettings = useAppSettingsStore() const raw = appSettings.get('df-ai-language', 'auto') return raw === 'auto' ? appSettings.get('df-language', 'zh-CN') : raw } +// ── Lazy messages ref(破 stores/ai ↔ aiShared 循环依赖) ── +// +// stores/ai.ts import aiShared.ts,而 aiShared.ts 若在模块顶层 import +// stores/ai.ts 的 `state`,Vite 打包后可能因循环引用导致 TDZ 崩溃 +// (ReferenceError: Cannot access 'u' before initialization)。 +// 策略:stores/ai.ts 在 state 就绪后主动注入 messages 数组引用, +// aiShared 函数在运行时通过 getMessages() 惰性获取,不再模块顶 import。 +let _messages: AiMessage[] | null = null + +/** + * @internal 由 stores/ai.ts 在 state 创建后调用,注入响应式 messages 引用。 + * 本模块内 findToolCall / startApprovalTimer 通过 getMessages() 访问。 + */ +export function __bindMessages(m: AiMessage[]): void { + _messages = m +} + +function getMessages(): AiMessage[] { + return _messages ?? [] +} + /** 客户端消息自增 id(全局唯一,多个 composable 共用 nextMsgId) */ let _msgCounter = 0 @@ -57,8 +77,9 @@ export function nextMsgId(): number { * 片读到已不存在的对象。反向扫描零额外状态、改动最小且行为完全一致。 */ export function findToolCall(id: string): AiToolCallInfo | undefined { - for (let i = state.messages.length - 1; i >= 0; i--) { - const toolCalls = state.messages[i].toolCalls + const messages = getMessages() + for (let i = messages.length - 1; i >= 0; i--) { + const toolCalls = messages[i].toolCalls if (!toolCalls) continue // 同一消息内工具调用通常很少,沿用正向扫描;命中即返回 for (let j = 0; j < toolCalls.length; j++) { @@ -83,6 +104,7 @@ const APPROVAL_TIMEOUT_DEFAULT_MS = 900_000 // 15 分钟 * (已在跑的计时器沿用旧值,符合「不影响已发起审批」的预期)。 */ function getApprovalTimeoutMs(): number { + const appSettings = useAppSettingsStore() const raw = appSettings.get(APPROVAL_TIMEOUT_KEY, APPROVAL_TIMEOUT_DEFAULT_MS) // 防御:负数 / NaN 视为默认值;0 保留(语义=不限时) if (typeof raw !== 'number' || !Number.isFinite(raw) || raw < 0) { @@ -121,7 +143,7 @@ export function startApprovalTimer( denyP.catch(e => { console.error('[AI] 审批超时自动拒绝 IPC 未送达:', e) }) - state.messages.push({ + getMessages().push({ id: `approval-timeout-${nextMsgId()}`, role: 'assistant', content: t('ai.approvalTimeout', { toolName }), diff --git a/src/stores/ai.ts b/src/stores/ai.ts index c28ed66..a675e22 100644 --- a/src/stores/ai.ts +++ b/src/stores/ai.ts @@ -52,7 +52,7 @@ import { useAiEvents } from '@/composables/ai/useAiEvents' import { useAiStream } from '@/composables/ai/useAiStream' // 批4 双轨收口:getConvState 从 aiShared 取(下沉破环,避免 stores/ai↔useAiEvents 反向环)。 // F-09 per-conv streaming/currentText(DEC-07a 父④):convStreamStates Map + 辅助函数亦从 aiShared 取。 -import { getConvState, getConvStreamState, setConvStreaming, setConvCurrentText } from '@/composables/ai/aiShared' +import { getConvState, getConvStreamState, setConvStreaming, setConvCurrentText, __bindMessages } from '@/composables/ai/aiShared' import { useAiSend, initDrainQueueListener } from '@/composables/ai/useAiSend' import { useAiApproval } from '@/composables/ai/useAiApproval' import { useAiConversations } from '@/composables/ai/useAiConversations' @@ -190,6 +190,8 @@ export const state = reactive(_stateBase) as typeof _stateBase & { } // 回填 stateRef:accessor 闭包读此引用(经 reactive 包装后的代理对象,activeConversationId 变化可追踪)。 stateRef = state +// 注入 messages 引用到 aiShared 共享层(破循环依赖,避免 Vite 打包 TDZ)。 +__bindMessages(state.messages) // 旁注:此处不再保留 type-only 导出(AiChatEvent 等),因组件直接从 api/types import。 // 若有外部模块仍从本文件 import 这些类型,下方 re-export 兜底: