//! D2a 工具结果外置句柄的前端识别与读回。 //! //! 后端审计写路径对超 8KB 的工具结果落盘到 {app_data}/tool-results/**,DB 只留句柄 JSON: //! {"__offloaded__":true,"path":"...","size_bytes":N,"tool":"...","head":"...","hint":"..."} //! 前端遇此句柄时显示「结果已外置」+ 展开;展开经 IPC ai_read_offloaded_result 按 path 读回原文。 //! //! 单一真值来源:句柄识别/读回/IPC 参数名在此收口,工具卡与审计详情共用,不各自实现一份。 //! 两个来源形态:工具卡 tc.result 是完整句柄(JSON 合法);审计 result_brief 被后端截断到 //! 160 字符(JSON 非法、尾部省略号),故识别分完整解析与截断正则兜底两路。 import { computed, ref, type ComputedRef } from 'vue' import { invoke } from '@tauri-apps/api/core' /** 句柄标记字段(与后端 tool_result_offload::HANDLE_MARK 同字面量,改动须同步) */ export const OFFLOAD_MARK = '__offloaded__' /** 外置句柄(后端 json! 落库;此处只取前端展示/读回需要的字段) */ export interface OffloadHandle { path: string sizeBytes?: number tool?: string head?: string } /** 从截断句柄文本里宽松提取 path(JSON 已非法,不能 JSON.parse)。 */ function extractPathTolerant(s: string): string | null { const m = /"path"\s*:\s*"((?:[^"\\]|\\.)*)"/.exec(s) if (!m) return null // JSON 字符串反转义(Windows 路径含 \\ 转义;路径中不会出现 \n/\t 等序列) return m[1].replace(/\\(.)/g, '$1') } /** 对象形态 → 句柄(标记须为 true 且含非空 path,否则 null)。 */ function handleFromObject(obj: unknown): OffloadHandle | null { if (!obj || typeof obj !== 'object' || Array.isArray(obj)) return null const o = obj as Record if (o[OFFLOAD_MARK] !== true) return null const path = o.path if (typeof path !== 'string' || !path) return null return { path, sizeBytes: typeof o.size_bytes === 'number' ? o.size_bytes : undefined, tool: typeof o.tool === 'string' ? o.tool : undefined, head: typeof o.head === 'string' ? o.head : undefined, } } /** result 是否带外置标记(不要求 path 可解析;供列表单元格/头部摘要轻量判据)。 */ export function hasOffloadMark(result: unknown): boolean { if (typeof result === 'string') return /"__offloaded__"\s*:\s*true/.test(result) if (result && typeof result === 'object' && !Array.isArray(result)) { return (result as Record)[OFFLOAD_MARK] === true } return false } /** * 识别外置句柄:非句柄 / 无 path → null(调用方按普通结果渲染)。 * 完整句柄走 JSON 解析;截断句柄(审计 result_brief)JSON 非法 → 标记 + 正则抽 path 兜底。 */ export function parseOffloadHandle(result: unknown): OffloadHandle | null { if (!hasOffloadMark(result)) return null if (typeof result === 'string') { try { return handleFromObject(JSON.parse(result)) } catch { const path = extractPathTolerant(result) return path ? { path } : null } } return handleFromObject(result) } /** 按 path 读回外置原文(后端 IPC ai_read_offloaded_result;文件缺失返带语义错误串)。 */ export function readOffloadedResult(path: string): Promise { return invoke('ai_read_offloaded_result', { path }) } /** * 外置结果展开状态机:识别句柄 + 懒加载读回原文(同一 path 只读一次,读回后缓存)。 * getResult 惰性取值,响应式来源不在此固化(工具卡按 tc、审计按当前展开行)。 */ export function useOffloadedResult(getResult: () => unknown) { const handle: ComputedRef = computed(() => parseOffloadHandle(getResult())) const expanded = ref(false) const loading = ref(false) const error = ref('') const resolved = ref(null) async function toggle() { if (expanded.value) { expanded.value = false return } expanded.value = true const h = handle.value // 已读回 / 读回中 / 无句柄 → 仅切换展开态,不重复走 IPC if (!h || resolved.value != null || loading.value) return loading.value = true error.value = '' try { resolved.value = await readOffloadedResult(h.path) } catch (e) { error.value = String(e) } finally { loading.value = false } } /** 展开对象切换(审计单行详情复用同一实例)时清态,防串上一行的读回结果/错误。 */ function reset() { expanded.value = false loading.value = false error.value = '' resolved.value = null } return { handle, expanded, loading, error, resolved, toggle, reset } }