Răsfoiți Sursa

fix:解决响应式messages无法存入IndexedDB的问题,添加深度解响应式

Jerry Liang 1 lună în urmă
părinte
comite
cb79a535ce
1 a modificat fișierele cu 22 adăugiri și 2 ștergeri
  1. 22 2
      src/layout/header/Actions/AiChat/useAiChat.ts

+ 22 - 2
src/layout/header/Actions/AiChat/useAiChat.ts

@@ -1,4 +1,4 @@
-import { ref, toRaw } from 'vue'
+import { ref, toRaw, isRef, unref } from 'vue'
 import type { ChatMessage, ToolCallInfo, SessionListItem, UploadedFile, MessageSegment } from './interface'
 import { useUserStore } from '@/stores/user'
 import { useDiscreteApi } from '@/composable/useDiscreteApi'
@@ -40,6 +40,25 @@ interface ParsedTextChunk {
 }
 type ParsedChunk = ParsedTextChunk | ParsedFileChunk
 
+// 深度普通化:把 Vue 响应式对象(含所有嵌套层级)递归还原为纯 JS 对象,
+// 使其可被 structured clone(IndexedDB.put)。相比 toRaw 只解一层、
+// 或 JSON 往返会丢失共享引用,这里用 WeakMap 记录已拷贝对象,
+// 保证如 tool 在 toolCalls 与 segments 中的“共享同一引用”在拷贝后依然成立,
+// 从而读取会话、Vue 重新响应式化后,tool 状态同步语义得以保留。
+// 同时兼容循环引用。
+function deepPlain<T>(input: T, seen: WeakMap<object, any> = new WeakMap()): T {
+  const raw: any = toRaw(isRef(input) ? unref(input) : input)
+  if (raw === null || typeof raw !== 'object') return raw as T
+  if (seen.has(raw)) return seen.get(raw)
+
+  const out: any = Array.isArray(raw) ? [] : {}
+  seen.set(raw, out) // 先登记再递归,命中共享/循环引用时复用同一份拷贝
+  for (const key of Object.keys(raw)) {
+    out[key] = deepPlain(raw[key], seen)
+  }
+  return out as T
+}
+
 // 按 $$FILE_DOWNLOAD$$ 标记把文本切分为「文本 / 文件」有序片段。
 // 非法/残缺标记会被丢弃(既不产生文件片段,也不保留标记文本)。
 function splitByFileMarker(text: string): ParsedChunk[] {
@@ -437,7 +456,8 @@ export function useAiChat() {
   async function saveLocalMessages() {
     if (!currentSessionId.value || messages.value.length === 0) return
     const title = getSessionTitle()
-    await chatDB.saveSession(currentSessionId.value, title, toRaw(messages.value))
+    // 深度普通化:递归解响应式并保留共享引用,避免 IndexedDB 结构化克隆失败
+    await chatDB.saveSession(currentSessionId.value, title, deepPlain(messages.value))
   }
 
   async function loadLocalMessages(sessionId: string) {