- 待发箱地基: V51 ai_outbox 表 + 手写 OutboxRepo(claim CAS/崩溃残留复位/瞬时失败保留) + outbox.rs 调度器(KV 默认关/Notify 唤醒/会话忙跳过不丢)
- Wake 异步唤醒原语: 三条件(delay/file-appear/process-exit)+纯判定+注入走正典 spawn_agentic_session,默认关
- 上下文预算随模型窗口: model_context.rs 三级优先(池显式>预设表>回退 128K);上界取 1M 而非 128K——max_tokens 仅裁剪预算,卡 128K 等于不治根因
- 文件租约补纯函数与单测(未另造双轨:LEASE 早随 cb260ab 落地) / 跨会话召回补纯函数单测(主体早已落地)
- 子代理: 完整卡已消化 + 子会话压缩层 + P3 批量
- 隧道/中继: 指令离线不丢 + Hello proto 版本 + 心跳与 runbook
- df-mcp: 写工具后落 KV 变更标记(供 GUI 轮询兜底,补独立 stdio 进程无感知)
- 迁移链顶点改单一真值常量 LATEST_SCHEMA_VERSION(修 2 处因 V51 失效的断言)
- 治理: i18n 特殊字符提交门禁(含反引号覆盖 + 10 自测) / 根目录卫生门禁 / 看板归档器 / 单一 lockfile(bun) / swap-install 落 temp/ 且兼容无 Get-FileHash
- 场景护栏: 流式正文可见性 e2e(playwright 免真机事件回放) + 列表续写/外置句柄 node 测试
120 lines
4.6 KiB
TypeScript
120 lines
4.6 KiB
TypeScript
//! 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<string, unknown>
|
|
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<string, unknown>)[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<string> {
|
|
return invoke<string>('ai_read_offloaded_result', { path })
|
|
}
|
|
|
|
/**
|
|
* 外置结果展开状态机:识别句柄 + 懒加载读回原文(同一 path 只读一次,读回后缓存)。
|
|
* getResult 惰性取值,响应式来源不在此固化(工具卡按 tc、审计按当前展开行)。
|
|
*/
|
|
export function useOffloadedResult(getResult: () => unknown) {
|
|
const handle: ComputedRef<OffloadHandle | null> = computed(() => parseOffloadHandle(getResult()))
|
|
const expanded = ref(false)
|
|
const loading = ref(false)
|
|
const error = ref('')
|
|
const resolved = ref<string | null>(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 }
|
|
}
|