feat: v0.7.2 安全收口 · 断链接线 · 观测补洞 — 230 用例扩充与全量回归
CI / 类型检查 + Lint + 单元测试 (push) Failing after 5m43s
CI / 全量测试 (Electron ABI) (push) Failing after 5m25s
CI / 产物编译验证 (push) Successful in 10m1s

P1 修复面收口: /clear 全链路根治(前端清空联动 DB messages+摘要游标+TRACE 快照,
IPC 语义改"操作完成"; 流式中拒绝); web_browser open 补 SSRF 校验(Chromium 旁路关闭,
与 web_fetch/http_request 同源 validateSSRF); MCP 工具结果纳入注入扫描(mcp_* 前缀
按网络来源同级 full 模式, 收敛 resolveScanMode 单点); Trace 落库/入 store 双重瘦身
(tool_result base64/超长字段剥离, metadata 防 MB 级膨胀); 文本附件 512KB 闸门
(file.slice 首段读取+truncated 标志随消息持久化+主进程附件提示感知截断);
单实例锁(requestSingleInstanceLock + second-instance 聚焦已有窗口)

P2 安全纵深: ConfirmationHook 多窗口化(确认请求/超时提示改全窗口广播,
getAllWindows 空时回退 mainWindow, fail-closed 判定升级双通道); mcp_servers.headers
全链路接线(safeParseHeaders 容错解析+SSE/StreamableHTTP requestInit 注入+IPC 逐项
校验+设置页 JSON 输入, 远程 MCP 鉴权头可用)

P3 断链接线: llm:listModels IPC(六家 adapter 动态模型发现首次接线, 配置完整性
前置校验); Ollama pullModel IPC+设置页下载卡片(进度/取消/能力徽标, v0.7.0 死代码
激活); 后台会话运行指示(sessionRunStates 图+Sidebar 状态点, 多会话并发可见);
IR 卫生(移除 THINKING_START/END 死枚举, constraints 标注预留)

P4 质量与文档: i18n 第二阶段(确认弹框/侧栏/状态栏/AgentMonitor/终止原因出层,
外观设置 zh-CN/en-US 切换, ui.locale 持久化, 渲染时求值规避异步注册); README/D1
文档对齐(http_request 风险等级/用例数/实现状态注记); 版本号 0.7.2

测试: 507 → 737 用例(+230, 11 个新文件)。覆盖补齐: context-builder/consolidator/
orchestrator/workspace.service/session-recorder/config-layering/secure-config/
network-proxy + IPC mcp/tasks/memory/app/data 域 + 渲染层 store 与流事件管线纯函数。
测试驱动修复: workspace.appendMemory 中文分区 \b 词边界失效(JS \b 不含 CJK),
固化条目恒追加文件末尾产生重复分区头 → (?=\n|$) 前瞻断言根治

回归: typecheck 双端 0 错误; ESLint 0/0; 系统 Node 687 通过 50 跳过;
Electron ABI 全量 737/737 零跳过
This commit is contained in:
2026-08-30 00:09:25 +08:00
parent 36e4b28d93
commit 26169b7be4
55 changed files with 5673 additions and 434 deletions
+117 -18
View File
@@ -6,6 +6,9 @@
import { create } from 'zustand';
import { useSessionStore } from './session-store';
// v0.7.2 A4: Trace 落库前复用显示层裁剪(剥离 base64/超长字符串),防止
// view_image 截图等超大工具结果(单张 ~6.7MB base64)原样写入 sessions.metadata
import { toDisplayResult } from '../lib/tool-result-display';
// L-1: 消息 ID 防碰撞计数器
let _msgIdCounter = 0;
@@ -41,6 +44,12 @@ export interface AttachmentInfo {
size: number;
preview?: string; // 图片 base64 data URL
textContent?: string; // 文本文件内容
/**
* v0.7.2 A5: 文本附件超过 512KB 上限时被截断。
* 上传入口(ChatInput)读取文件首段内容并附加截断标记,
* 该标志随消息持久化并注入主进程附件提示(LLM 感知内容不完整)。
*/
truncated?: boolean;
}
export interface ChatMessage {
@@ -136,6 +145,16 @@ interface AgentState {
// v0.5.4: 多模态总开关(llm.multimodalEnabled)— 未开启时禁止上传图片
multimodalEnabled: boolean;
/**
* v0.7.2 P3-11: 后台会话运行状态图(sessionId → AgentStatus)。
*
* 消息流/Trace 均按"当前会话"过滤(多会话隔离),用户切走会话后该会话
* 仍在运行的 Agent 对 UI 完全静默。此图由 useAgentStream 的 onStateChange
* 管道全量维护(含非当前会话),供 Sidebar 点亮任意运行中会话的状态点。
* TERMINATED 时条目被移除(值域仅 thinking/executing 有意义,idle 不落图)。
*/
sessionRunStates: Record<string, AgentStatus>;
// Actions
setCurrentSession: (id: string | null) => void;
setMessages: (messages: ChatMessage[]) => void;
@@ -169,8 +188,13 @@ interface AgentState {
setMultimodalEnabled: (enabled: boolean) => void;
setMaxIterations: (max: number) => void;
setCurrentIteration: (n: number) => void;
/**
* v0.7.2 P3-11: 维护后台会话运行状态图。
* status 传入 null 表示该会话运行已结束(TERMINATED / abort),移除条目。
*/
updateSessionRunState: (sessionId: string, status: AgentStatus | null) => void;
saveTraceData: () => void;
clearMessages: () => void;
clearMessages: () => Promise<void>;
abort: () => void;
/**
* P2-11: 编辑重发——截断该用户消息(含)之后的所有消息,重新发送修订内容
@@ -208,6 +232,8 @@ export const useAgentStore = create<AgentState>((set, get) => ({
contextWindow: 0,
// v0.5.4: 多模态总开关(默认关闭,setProvider 时从配置加载)
multimodalEnabled: false,
// v0.7.2 P3-11: 后台会话运行状态图(初始为空)
sessionRunStates: {},
// ===== Actions =====
@@ -394,6 +420,10 @@ export const useAgentStore = create<AgentState>((set, get) => ({
isStreaming: true,
currentRunId: null, // 将在首个 streamEvent 中由 runId 设置
currentIteration: 0,
// v0.7.2 P3-11: 当前会话进入运行态(Sidebar 状态点即时反馈,无需等待首个 stateChange
sessionRunStates: sessionId
? { ...s.sessionRunStates, [sessionId]: 'thinking' }
: s.sessionRunStates,
// 方案 A: 不清空 traceSteps,避免前一条消息的 trace 被永久覆盖
// TraceViewer 按 runId 过滤显示,只展示当前 run 的 steps
// 历史 trace 仍在 DB 中,切换会话回来可恢复
@@ -578,39 +608,108 @@ export const useAgentStore = create<AgentState>((set, get) => ({
setCurrentIteration: (currentIteration) => set({ currentIteration }),
// v0.7.2 P3-11: 后台会话运行状态图维护(status=null 表示运行结束,移除条目)
updateSessionRunState: (sessionId, status) =>
set((s) => {
if (status === null) {
if (!(sessionId in s.sessionRunStates)) return s;
const { [sessionId]: _removed, ...rest } = s.sessionRunStates;
void _removed;
return { sessionRunStates: rest };
}
return { sessionRunStates: { ...s.sessionRunStates, [sessionId]: status } };
}),
saveTraceData: () => {
const { currentSessionId, traceSteps, tokenUsage } = get();
if (currentSessionId && window.metona?.sessions?.saveTrace) {
// v0.7.2 A4 根治: 落库前剥离工具结果中的 base64/超长字段。
// useAgentStream 把原始 toolResult.result(含 view_image dataUrl / web_browser
// 截图 image,单张最大 ~6.7MB)写入 traceStepssaveTrace 原样 JSON.stringify
// 进 sessions.metadata,长会话多图可把单行撑到数十 MB。复用显示层裁剪
// toDisplayResultdataUrl 剥离 + 任意 >8KB 字符串截断),内存态 traceSteps
// 在入 store 时已同步瘦身(见 useAgentStream),此处对 restore 自 DB 的
// 旧格式数据兜底二次裁剪。
const sanitizedSteps = traceSteps.map((step) =>
step.toolCalls?.length
? {
...step,
toolCalls: step.toolCalls.map((tc) => ({
...tc,
result: tc.result != null ? toDisplayResult(tc.result) : tc.result,
})),
}
: step,
);
window.metona.sessions
.saveTrace(currentSessionId, { traceSteps, tokenUsage })
.saveTrace(currentSessionId, { traceSteps: sanitizedSteps, tokenUsage })
.catch((err) => {
console.error('[AgentStore]', err);
});
}
},
clearMessages: () =>
set({
messages: [],
agentStatus: 'idle',
currentIteration: 0,
tokenUsage: {
inputTokens: 0,
outputTokens: 0,
totalTokens: 0,
lastInputTokens: 0,
lastCompressedSaved: 0,
},
traceSteps: [],
isStreaming: false,
currentRunId: null,
}),
// v0.7.2 A1 根治: /clear 全链路修复 —— 此前只清前端 state,数据库消息与
// 滚动摘要游标原样保留,切换会话再切回时消息全部"复活"。
// 现契约:优先调用 sessions:clearMessagesDB 侧同步删除 messages +
// session_summaries 游标 + metadata TRACE 快照),成功后才清本地 state;
// 失败时保持 UI 与 DB 一致(都不清)并 toast 报错;流式进行中拒绝执行
// (与 editAndResend/regenerate 同口径,防止清空与引擎写入竞争)。
clearMessages: async () => {
const { currentSessionId, isStreaming } = get();
if (isStreaming) {
import('@metona-team/metona-toast')
.then((mod) => mod.default.warning('Agent 正在回复中,请先中断再清空会话'))
.catch(() => {});
return;
}
if (currentSessionId && window.metona?.sessions?.clearMessages) {
try {
const r = await window.metona.sessions.clearMessages(currentSessionId);
if (r && r.success === false) {
import('@metona-team/metona-toast')
.then((mod) => mod.default.error(r.error ?? '清空会话失败,请重试'))
.catch(() => {});
return;
}
} catch (err) {
console.error('[AgentStore] clearMessages failed:', err);
import('@metona-team/metona-toast')
.then((mod) => mod.default.error(`清空会话失败:${(err as Error).message}`))
.catch(() => {});
return;
}
}
set((s) => {
// v0.7.2 P3-11: 同步熄灭当前会话的运行指示点
const { [currentSessionId ?? '']: _current, ...restRunStates } = s.sessionRunStates;
void _current;
return {
messages: [],
agentStatus: 'idle',
currentIteration: 0,
tokenUsage: {
inputTokens: 0,
outputTokens: 0,
totalTokens: 0,
lastInputTokens: 0,
lastCompressedSaved: 0,
},
traceSteps: [],
isStreaming: false,
currentRunId: null,
sessionRunStates: currentSessionId ? restRunStates : s.sessionRunStates,
};
});
},
abort: () => {
const sessionId = get().currentSessionId;
// 不清除 currentRunId — 保留用于过滤旧 run 的延迟终止事件(DONE/TERMINATED
// 旧 run 的 DONE 到达时会匹配 currentRunId,处理后清除;新 run 的 INIT 会覆盖
set({ agentStatus: 'idle', isStreaming: false });
// v0.7.2 P3-11: 立即熄灭运行指示点;若中断未生效,后续 THINKING 状态事件会重新点亮(自愈)
if (sessionId) get().updateSessionRunState(sessionId, null);
if (sessionId && window.metona?.agent?.abortSession) {
window.metona.agent.abortSession(sessionId).catch((err) => {
console.error('[AgentStore]', err);