feat: 升级至 v0.2.1 — 流式渲染修复、安全增强、工具自动执行

流式渲染修复:
- runId 机制防止 abort 后旧流事件污染新 run
- run lock 防止并发 run 污染引擎状态
- abort race 提前退出工具执行等待
- TERMINATED 状态通过 stateChange 发射
- tool_call_delta 流式参数拼接 + pending 占位替换
- 首轮卡片创建路径统一,traceStep 按 ID 精确匹配
- compressed 事件转发为 toast 通知

安全增强:
- ConfirmationHook 支持持久化自动执行(跨会话)
- 设置面板新增自动执行工具管理 UI
- SandboxManager 双重安全校验 fail-closed
- 审计日志链式哈希防篡改
- PromptInjectionDefender 中文注入标记清理
- scanCode 28 模式 + base64/$() 检测
- validatePath realpathSync 防符号链接逃逸
- code-search 使用 execFile 防命令注入

新增工具:
- file_editor、code_search、task_manager、diff_viewer

其他:
- Agent Loop 加 PARSING/REFLECTING 状态 + 指数退避重试
- MemoryManager TF-IDF 语义检索
- run_command Windows 中文编码修复(chcp 65001)
- 版本号 0.2.0 → 0.2.1
This commit is contained in:
thzxx
2026-07-12 12:54:52 +08:00
parent 469f53b623
commit 3c5aea8fb7
41 changed files with 4972 additions and 423 deletions
+28 -15
View File
@@ -7,6 +7,10 @@
import { create } from 'zustand';
import { useSessionStore } from './session-store';
// L-1: 消息 ID 防碰撞计数器
let _msgIdCounter = 0;
export const genMsgId = (role: string) => `msg_${Date.now()}_${role}_${_msgIdCounter++}`;
// ===== 消息类型 =====
export type MessageRole = 'user' | 'assistant' | 'system' | 'tool';
@@ -85,7 +89,9 @@ interface AgentState {
// 流式
isStreaming: boolean;
streamingContent: string;
// Run 标识(用于过滤旧流事件)
currentRunId: string | null;
// Provider
provider: string;
@@ -101,10 +107,10 @@ interface AgentState {
updateLastAssistantMessage: (delta: string) => void;
setAgentStatus: (status: AgentStatus) => void;
setStreaming: (streaming: boolean) => void;
appendStreamingContent: (delta: string) => void;
clearStreamingContent: () => void;
setCurrentRunId: (runId: string | null) => void;
addTraceStep: (step: TraceStep) => void;
updateTraceStep: (iteration: number, updates: Partial<TraceStep>) => void;
updateTraceStepById: (id: string, updates: Partial<TraceStep>) => void;
updateLastTraceStep: (updates: Partial<TraceStep>) => void;
updateTokenUsage: (usage: Partial<TokenUsage>) => void;
setProvider: (provider: string, model: string) => void;
@@ -125,7 +131,7 @@ export const useAgentStore = create<AgentState>((set, get) => ({
tokenUsage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
traceSteps: [],
isStreaming: false,
streamingContent: '',
currentRunId: null,
provider: '',
model: '',
contextWindow: 0,
@@ -133,7 +139,7 @@ export const useAgentStore = create<AgentState>((set, get) => ({
// ===== Actions =====
setCurrentSession: (id) => {
set({ currentSessionId: id, messages: [], traceSteps: [], currentIteration: 0, tokenUsage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 } });
set({ currentSessionId: id, messages: [], traceSteps: [], currentIteration: 0, tokenUsage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 }, agentStatus: 'idle', isStreaming: false, currentRunId: null });
// 从数据库加载该会话的消息
if (id && window.metona?.sessions?.getMessages) {
@@ -208,7 +214,7 @@ export const useAgentStore = create<AgentState>((set, get) => ({
console.error('[AgentStore]', 'Failed to create session:', err);
set({ agentStatus: 'error', isStreaming: false });
get().addMessage({
id: `msg_${Date.now()}_error`,
id: genMsgId('error'),
role: 'system',
content: `错误: 无法创建会话 — ${(err as Error).message}`,
timestamp: Date.now(),
@@ -218,7 +224,7 @@ export const useAgentStore = create<AgentState>((set, get) => ({
}
const userMessage: ChatMessage = {
id: `msg_${Date.now()}_user`,
id: genMsgId('user'),
role: 'user',
content, // 用户可见内容(纯文本)
timestamp: Date.now(),
@@ -229,7 +235,7 @@ export const useAgentStore = create<AgentState>((set, get) => ({
messages: [...s.messages, userMessage],
agentStatus: 'thinking',
isStreaming: true,
streamingContent: '',
currentRunId: null, // 将在首个 streamEvent 中由 runId 设置
currentIteration: 0,
}));
@@ -283,7 +289,7 @@ export const useAgentStore = create<AgentState>((set, get) => ({
console.error('[AgentStore]', 'IPC sendMessage failed:', err);
set({ agentStatus: 'error', isStreaming: false });
get().addMessage({
id: `msg_${Date.now()}_error`,
id: genMsgId('error'),
role: 'system',
content: `错误: 消息发送失败 — ${(err as Error).message}`,
timestamp: Date.now(),
@@ -302,7 +308,7 @@ export const useAgentStore = create<AgentState>((set, get) => ({
messages[lastIdx] = { ...lastMsg, content: lastMsg.content + delta };
} else {
messages.push({
id: `msg_${Date.now()}_assistant`,
id: genMsgId('assistant'),
role: 'assistant',
content: delta,
timestamp: Date.now(),
@@ -317,10 +323,7 @@ export const useAgentStore = create<AgentState>((set, get) => ({
setStreaming: (streaming) => set({ isStreaming: streaming }),
appendStreamingContent: (delta) =>
set((s) => ({ streamingContent: s.streamingContent + delta })),
clearStreamingContent: () => set({ streamingContent: '' }),
setCurrentRunId: (runId) => set({ currentRunId: runId }),
addTraceStep: (step) =>
set((s) => ({ traceSteps: [...s.traceSteps, step] })),
@@ -332,6 +335,14 @@ export const useAgentStore = create<AgentState>((set, get) => ({
),
})),
// L-2: 按 ID 精确匹配 traceStep(避免 iteration 碰撞)
updateTraceStepById: (id, updates) =>
set((s) => ({
traceSteps: s.traceSteps.map((t) =>
t.id === id ? { ...t, ...updates } : t,
),
})),
updateLastTraceStep: (updates) =>
set((s) => {
if (s.traceSteps.length === 0) return s;
@@ -376,11 +387,13 @@ export const useAgentStore = create<AgentState>((set, get) => ({
tokenUsage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
traceSteps: [],
isStreaming: false,
streamingContent: '',
currentRunId: null,
}),
abort: () => {
const sessionId = get().currentSessionId;
// 不清除 currentRunId — 保留用于过滤旧 run 的延迟终止事件(DONE/TERMINATED
// 旧 run 的 DONE 到达时会匹配 currentRunId,处理后清除;新 run 的 INIT 会覆盖
set({ agentStatus: 'idle', isStreaming: false });
if (sessionId && window.metona?.agent?.abortSession) {
window.metona.agent.abortSession(sessionId).catch((err) => { console.error('[AgentStore]', err); });