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