feat: v0.7.2 安全收口 · 断链接线 · 观测补洞 — 230 用例扩充与全量回归
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:
+117
-18
@@ -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)写入 traceSteps;saveTrace 原样 JSON.stringify
|
||||
// 进 sessions.metadata,长会话多图可把单行撑到数十 MB。复用显示层裁剪
|
||||
// toDisplayResult(dataUrl 剥离 + 任意 >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:clearMessages(DB 侧同步删除 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);
|
||||
|
||||
Reference in New Issue
Block a user