|
@@ -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 type { ChatMessage, ToolCallInfo, SessionListItem, UploadedFile, MessageSegment } from './interface'
|
|
|
import { useUserStore } from '@/stores/user'
|
|
import { useUserStore } from '@/stores/user'
|
|
|
import { useDiscreteApi } from '@/composable/useDiscreteApi'
|
|
import { useDiscreteApi } from '@/composable/useDiscreteApi'
|
|
@@ -40,6 +40,25 @@ interface ParsedTextChunk {
|
|
|
}
|
|
}
|
|
|
type ParsedChunk = ParsedTextChunk | ParsedFileChunk
|
|
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$$ 标记把文本切分为「文本 / 文件」有序片段。
|
|
// 按 $$FILE_DOWNLOAD$$ 标记把文本切分为「文本 / 文件」有序片段。
|
|
|
// 非法/残缺标记会被丢弃(既不产生文件片段,也不保留标记文本)。
|
|
// 非法/残缺标记会被丢弃(既不产生文件片段,也不保留标记文本)。
|
|
|
function splitByFileMarker(text: string): ParsedChunk[] {
|
|
function splitByFileMarker(text: string): ParsedChunk[] {
|
|
@@ -437,7 +456,8 @@ export function useAiChat() {
|
|
|
async function saveLocalMessages() {
|
|
async function saveLocalMessages() {
|
|
|
if (!currentSessionId.value || messages.value.length === 0) return
|
|
if (!currentSessionId.value || messages.value.length === 0) return
|
|
|
const title = getSessionTitle()
|
|
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) {
|
|
async function loadLocalMessages(sessionId: string) {
|