修复: 设置页面崩溃(模块循环依赖致初始化失败)

共享层模块顶层导入状态仓库,而状态仓库又导入共享层符号,形成
循环引用。打包后运行时触发 TDZ 崩溃,设置页面无法打开。

修复方案:

- 共享层删除模块顶层的状态仓库导入和全局 store 实例
- 改为由状态仓库在状态就绪后主动注入消息数组引用
- 共享层函数运行时惰性获取消息引用,不再编译期依赖
- store 实例改为函数内局部获取(运行时 store 已就绪)

零行为变更,纯依赖方向调整。
This commit is contained in:
2026-06-28 23:39:48 +08:00
parent 996f1d9e5f
commit cc3ef5bc1d
2 changed files with 31 additions and 7 deletions

View File

@@ -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<string>('df-ai-language', 'auto')
return raw === 'auto'
? appSettings.get<string>('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<number>(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 }),

View File

@@ -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 兜底: