fix: preserve interaction replies across chat recovery

This commit is contained in:
2026-10-29 17:53:33 +08:00
parent fbe273e90a
commit 52681a8451
38 changed files with 1623 additions and 314 deletions
+39 -4
View File
@@ -1,5 +1,6 @@
import type { Event as OpencodeEvent, Part } from "@opencode-ai/sdk/v2";
import { containsForbiddenPersistentContent } from "../utils/persistencePolicy.js";
import { logger } from "../logger.js";
import { type QuestionAnswers } from "../runtime/opencode.js";
@@ -61,8 +62,8 @@ const isDevelopmentDebugLoggingEnabled = process.env.NODE_ENV === "development";
const toolLabels: Record<string, string> = {
clear_scada_analysis: "清除 SCADA 地图分析",
memory_manager: "记忆写入",
render_scada_analysis: "渲染 SCADA 地图分析",
memory_manager: "记忆写入",
session_search: "历史会话检索",
skill_manager: "流程沉淀",
};
@@ -356,6 +357,40 @@ export const normalizeToolStatus = (status: string) => {
return "running";
};
/** Browser-visible tool inputs: retain useful values without credentials or large payloads. */
export const sanitizeToolProgressInput = (params: Record<string, unknown>): Record<string, unknown> => {
let remaining = 16_000;
let nodes = 512;
const visit = (value: unknown, depth: number): unknown => {
if (remaining <= 0 || nodes-- <= 0) return "[truncated]";
if (typeof value === "string") {
if (containsForbiddenPersistentContent(value)) return "[redacted]";
const limit = Math.min(remaining, 4_000);
remaining -= Math.min(value.length, limit);
return value.length > limit ? `${value.slice(0, limit)}… [truncated]` : value;
}
if (value === null || typeof value === "boolean" || typeof value === "number") return value;
if (depth >= 5) return "[truncated]";
if (Array.isArray(value)) {
const items = value.slice(0, 64).map((item) => visit(item, depth + 1));
if (value.length > 64) items.push("[truncated]");
return items;
}
if (value && typeof value === "object") {
const entries = Object.entries(value).slice(0, 64).map(([rawKey, item]) => {
const key = rawKey.slice(0, 100);
remaining -= key.length;
return [key, /authorization|cookie|password|secret|token|api[_-]?key|credential|headers?/i.test(key)
? "[redacted]" : visit(item, depth + 1)];
});
if (Object.keys(value).length > 64) entries.push(["…", "[truncated]"]);
return Object.fromEntries(entries);
}
return "[unsupported]";
};
return visit(params, 0) as Record<string, unknown>;
};
const formatProgressValue = (value: unknown): string => {
if (typeof value === "string") {
return value.length > 120 ? `${value.slice(0, 117)}...` : value;
@@ -435,11 +470,11 @@ export const buildToolProgressDetail = (
error?: string,
) => {
const toolName = toolLabels[tool] ?? tool;
const reasonText = reason ? `;说明:${reason}` : "";
const paramsText = `;关键参数:${summarizeToolParams(params)}`;
const reasonText = reason ? `;说明:${containsForbiddenPersistentContent(reason) ? "[redacted]" : reason}` : "";
const paramsText = `;关键参数:${summarizeToolParams(sanitizeToolProgressInput(params))}`;
if (status === "error") {
const errorText = error ? `;错误:${error}` : "";
const errorText = error ? `;错误:${containsForbiddenPersistentContent(error) ? "[redacted]" : error}` : "";
return `${toolName} 调用失败${reasonText}${paramsText}${errorText}`;
}
if (status === "completed") {