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:
+167
-28
@@ -11,7 +11,7 @@
|
||||
*/
|
||||
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useAgentStore, type ToolCallInfo, type AgentStatus } from '@renderer/stores/agent-store';
|
||||
import { useAgentStore, genMsgId, type ToolCallInfo, type AgentStatus } from '@renderer/stores/agent-store';
|
||||
|
||||
/**
|
||||
* Agent 流式事件监听 Hook
|
||||
@@ -40,6 +40,26 @@ export function useAgentStream(): void {
|
||||
state?: string;
|
||||
};
|
||||
|
||||
// 会话过滤:忽略非当前会话的事件(防止切换会话后旧事件污染)
|
||||
if (data.sessionId && data.sessionId !== useAgentStore.getState().currentSessionId) return;
|
||||
|
||||
// H-6/C-4: runId 过滤 — 忽略旧 run 的事件(abort 后重发场景)
|
||||
// 修复 abort 后旧 DONE 污染新 run:currentRunId=null 时忽略终止事件
|
||||
const currentState = useAgentStore.getState();
|
||||
if (data.runId) {
|
||||
if (currentState.currentRunId) {
|
||||
// currentRunId 已设置:只接受匹配的事件
|
||||
if (data.runId !== currentState.currentRunId) return;
|
||||
} else {
|
||||
// currentRunId 未设置:
|
||||
// done/error 是终止事件 — currentRunId 为 null 说明已被 abort 或尚未开始,
|
||||
// 忽略旧 run 的延迟终止事件,避免错误地 setStreaming(false)
|
||||
if (data.type === 'done' || data.type === 'error') return;
|
||||
// 首个活跃事件,设置 currentRunId
|
||||
currentState.setCurrentRunId(data.runId);
|
||||
}
|
||||
}
|
||||
|
||||
// 每次都从 store 读取最新状态(避免闭包捕获过期快照)
|
||||
const getStore = () => useAgentStore.getState();
|
||||
|
||||
@@ -47,6 +67,33 @@ export function useAgentStream(): void {
|
||||
// 推理内容增量
|
||||
case 'reasoning_delta':
|
||||
if (data.delta) {
|
||||
// C-1: 如果 delta 属于新迭代,先创建新卡片(不依赖 stateChange 到达顺序)
|
||||
if (data.iteration != null) {
|
||||
const msgs = getStore().messages;
|
||||
const last = msgs[msgs.length - 1];
|
||||
const needsNewCard = !last || last.role !== 'assistant' ||
|
||||
(last.iteration != null && last.iteration !== data.iteration);
|
||||
if (needsNewCard) {
|
||||
getStore().addMessage({
|
||||
id: genMsgId('assistant'),
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
reasoningContent: data.delta,
|
||||
timestamp: Date.now(),
|
||||
iteration: data.iteration,
|
||||
});
|
||||
if (data.iteration !== getStore().currentIteration) {
|
||||
getStore().setCurrentIteration(data.iteration);
|
||||
}
|
||||
// 同步更新当前 Trace 步骤的 thought 字段
|
||||
const ts = getStore().traceSteps;
|
||||
const step = ts[ts.length - 1];
|
||||
if (step) {
|
||||
getStore().updateLastTraceStep({ thought: (step.thought ?? '') + data.delta });
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
const messages = getStore().messages;
|
||||
const lastMsg = messages[messages.length - 1];
|
||||
if (lastMsg?.role === 'assistant') {
|
||||
@@ -57,7 +104,7 @@ export function useAgentStream(): void {
|
||||
} else {
|
||||
// 还没有 assistant 消息,先创建一条(仅含思考内容)
|
||||
getStore().addMessage({
|
||||
id: `msg_${Date.now()}_assistant`,
|
||||
id: genMsgId('assistant'),
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
reasoningContent: data.delta,
|
||||
@@ -80,7 +127,27 @@ export function useAgentStream(): void {
|
||||
// 文本增量
|
||||
case 'text_delta':
|
||||
if (data.delta) {
|
||||
getStore().setStreaming(true);
|
||||
// C-1: 如果 delta 属于新迭代,先创建新卡片(不依赖 stateChange 到达顺序)
|
||||
if (data.iteration != null) {
|
||||
const msgs = getStore().messages;
|
||||
const last = msgs[msgs.length - 1];
|
||||
const needsNewCard = !last || last.role !== 'assistant' ||
|
||||
(last.iteration != null && last.iteration !== data.iteration);
|
||||
if (needsNewCard) {
|
||||
getStore().addMessage({
|
||||
id: genMsgId('assistant'),
|
||||
role: 'assistant',
|
||||
content: data.delta,
|
||||
timestamp: Date.now(),
|
||||
iteration: data.iteration,
|
||||
});
|
||||
if (data.iteration !== getStore().currentIteration) {
|
||||
getStore().setCurrentIteration(data.iteration);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
// isStreaming 已在 sendMessage 时设置为 true,无需每个 delta 重复设置
|
||||
getStore().updateLastAssistantMessage(data.delta);
|
||||
|
||||
// 无 reasoning 模式下,将文本内容也记入 Trace thought(便于追踪)
|
||||
@@ -97,6 +164,30 @@ export function useAgentStream(): void {
|
||||
}
|
||||
break;
|
||||
|
||||
// M-1: 工具调用增量(流式参数拼接)— 仅更新 UI 占位,完整调用由 tool_call_complete 处理
|
||||
case 'tool_call_delta':
|
||||
if (data.toolCallDelta) {
|
||||
const msgs = getStore().messages;
|
||||
const lastAssistant = msgs[msgs.length - 1];
|
||||
const { index, name } = data.toolCallDelta;
|
||||
// 如果最后一条 assistant 消息还没有该 index 的占位工具调用,添加一个 pending 占位
|
||||
if (lastAssistant?.role === 'assistant' && name) {
|
||||
const existing = (lastAssistant.toolCalls ?? []).find((_, i) => i === index);
|
||||
if (!existing) {
|
||||
const placeholder: ToolCallInfo = {
|
||||
id: `tc_pending_${index}`,
|
||||
name,
|
||||
args: {},
|
||||
status: 'pending',
|
||||
};
|
||||
getStore().updateMessage(lastAssistant.id, {
|
||||
toolCalls: [...(lastAssistant.toolCalls ?? []), placeholder],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
// 工具调用完成
|
||||
case 'tool_call_complete':
|
||||
if (data.toolCall) {
|
||||
@@ -107,13 +198,22 @@ export function useAgentStream(): void {
|
||||
status: 'executing',
|
||||
};
|
||||
|
||||
// 追加到当前 assistant 消息
|
||||
// 追加到当前 assistant 消息(替换同 index 的 pending 占位)
|
||||
const msgs = getStore().messages;
|
||||
const lastAssistant = msgs[msgs.length - 1];
|
||||
if (lastAssistant?.role === 'assistant') {
|
||||
getStore().updateMessage(lastAssistant.id, {
|
||||
toolCalls: [...(lastAssistant.toolCalls ?? []), tc],
|
||||
});
|
||||
const existingTcs = lastAssistant.toolCalls ?? [];
|
||||
// 检查是否有同 index 的 pending 占位需要替换
|
||||
const pendingIdx = existingTcs.findIndex((t) => t.id.startsWith('tc_pending_'));
|
||||
if (pendingIdx >= 0) {
|
||||
const updated = [...existingTcs];
|
||||
updated[pendingIdx] = tc;
|
||||
getStore().updateMessage(lastAssistant.id, { toolCalls: updated });
|
||||
} else {
|
||||
getStore().updateMessage(lastAssistant.id, {
|
||||
toolCalls: [...existingTcs, tc],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 更新当前 Trace 步骤(添加工具调用信息)
|
||||
@@ -165,7 +265,8 @@ export function useAgentStream(): void {
|
||||
}
|
||||
: tc,
|
||||
);
|
||||
getStore().updateTraceStep(trace.iteration, { toolCalls: updatedTraceToolCalls });
|
||||
// L-2: 按 ID 精确匹配 traceStep(避免 iteration 碰撞)
|
||||
getStore().updateTraceStepById(trace.id, { toolCalls: updatedTraceToolCalls });
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -197,7 +298,11 @@ export function useAgentStream(): void {
|
||||
// 流结束
|
||||
case 'done':
|
||||
getStore().setStreaming(false);
|
||||
getStore().setAgentStatus('idle');
|
||||
getStore().setCurrentRunId(null);
|
||||
// 不覆盖 error 状态 — error handler 已设置 agentStatus='error'
|
||||
if (getStore().agentStatus !== 'error') {
|
||||
getStore().setAgentStatus('idle');
|
||||
}
|
||||
// 标记最后一个 Trace 步骤为已完成
|
||||
getStore().updateLastTraceStep({ completedAt: Date.now() });
|
||||
getStore().saveTraceData();
|
||||
@@ -206,10 +311,11 @@ export function useAgentStream(): void {
|
||||
// 错误
|
||||
case 'error':
|
||||
getStore().setStreaming(false);
|
||||
getStore().setCurrentRunId(null);
|
||||
getStore().setAgentStatus('error');
|
||||
getStore().updateLastTraceStep({ completedAt: Date.now() });
|
||||
getStore().addMessage({
|
||||
id: `msg_${Date.now()}_error`,
|
||||
id: genMsgId('error'),
|
||||
role: 'system',
|
||||
content: `错误: ${data.error?.message ?? '未知错误'}`,
|
||||
timestamp: Date.now(),
|
||||
@@ -236,31 +342,60 @@ export function useAgentStream(): void {
|
||||
state?: string;
|
||||
previous?: string;
|
||||
current?: string;
|
||||
runId?: string;
|
||||
};
|
||||
|
||||
const store = useAgentStore.getState();
|
||||
|
||||
// 会话过滤:忽略非当前会话的状态变化
|
||||
if (data.sessionId && data.sessionId !== store.currentSessionId) return;
|
||||
|
||||
// H-6/C-4: runId 过滤 — 忽略旧 run 的状态变化
|
||||
// 修复 abort 后旧 TERMINATED 污染:currentRunId=null 时只有 INIT 设置 currentRunId,其他忽略
|
||||
if (data.runId) {
|
||||
if (store.currentRunId) {
|
||||
// currentRunId 已设置:只接受匹配的状态变化
|
||||
if (data.runId !== store.currentRunId) return;
|
||||
} else {
|
||||
// currentRunId 未设置:
|
||||
// 只有 INIT 是新 run 的第一个状态,设置 currentRunId;
|
||||
// 其他状态(如旧 run 的 TERMINATED)忽略,避免污染
|
||||
if (data.state === 'INIT') {
|
||||
store.setCurrentRunId(data.runId);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- 迭代号更新 + 新消息卡片创建 ---
|
||||
if (data.iteration != null && data.iteration !== store.currentIteration) {
|
||||
const prevIteration = store.currentIteration;
|
||||
store.setCurrentIteration(data.iteration);
|
||||
|
||||
// M-7: 统一所有迭代的卡片创建路径(包括首轮)
|
||||
// 迭代号增大 → 新一轮 ReAct 迭代开始,创建新的 assistant 消息卡片
|
||||
// 仅当上一轮迭代已结束(prevIteration > 0)且上一条 assistant 消息有内容时
|
||||
if (data.iteration > prevIteration && prevIteration > 0) {
|
||||
// 如果最后一条已经是当前迭代的 assistant 卡片(delta handler 提前创建),不重复
|
||||
if (data.iteration > prevIteration) {
|
||||
const messages = store.messages;
|
||||
const lastMsg = messages[messages.length - 1];
|
||||
if (
|
||||
lastMsg?.role === 'assistant' &&
|
||||
(lastMsg.content || lastMsg.toolCalls?.length || lastMsg.reasoningContent)
|
||||
) {
|
||||
store.addMessage({
|
||||
id: `msg_${Date.now()}_assistant`,
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
timestamp: Date.now(),
|
||||
iteration: data.iteration,
|
||||
});
|
||||
const isAlreadyCurrentIteration = lastMsg?.role === 'assistant' && lastMsg.iteration === data.iteration;
|
||||
|
||||
if (!isAlreadyCurrentIteration) {
|
||||
// 首轮不要求上一轮有内容(上一轮是用户消息)
|
||||
const isFirstIteration = prevIteration === 0;
|
||||
const prevHasContent = lastMsg?.role === 'assistant' &&
|
||||
(lastMsg.content || lastMsg.toolCalls?.length || lastMsg.reasoningContent);
|
||||
|
||||
if (isFirstIteration || prevHasContent) {
|
||||
store.addMessage({
|
||||
id: genMsgId('assistant'),
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
timestamp: Date.now(),
|
||||
iteration: data.iteration,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -303,13 +438,14 @@ export function useAgentStream(): void {
|
||||
}
|
||||
|
||||
// --- Agent 状态映射 ---
|
||||
// INIT 不映射 — 避免 engine 的 transitionTo(INIT) 覆盖 sendMessage 设置的 'thinking' 状态
|
||||
const stateToStatus: Record<string, AgentStatus> = {
|
||||
THINKING: 'thinking',
|
||||
EXECUTING: 'executing',
|
||||
PARSING: 'thinking',
|
||||
OBSERVING: 'thinking',
|
||||
OBSERVING: 'thinking', // L-4: 观察工具结果更接近"思考"而非"执行"
|
||||
REFLECTING: 'thinking',
|
||||
COMPRESSING: 'thinking',
|
||||
INIT: 'thinking',
|
||||
TERMINATED: 'idle',
|
||||
};
|
||||
const newStatus = stateToStatus[data.state];
|
||||
@@ -327,9 +463,12 @@ export function useAgentStream(): void {
|
||||
if (!window.metona?.agent?.onProviderSwitched) return;
|
||||
|
||||
const unsubscribe = window.metona.agent.onProviderSwitched((data: unknown) => {
|
||||
const { from, to, reason } = data as { from?: string; to?: string; reason?: string };
|
||||
useAgentStore.getState().addMessage({
|
||||
id: `msg_${Date.now()}_system`,
|
||||
const { from, to, reason, sessionId } = data as { from?: string; to?: string; reason?: string; sessionId?: string };
|
||||
// L-3: 仅在当前会话中显示 Provider 切换消息
|
||||
const store = useAgentStore.getState();
|
||||
if (sessionId && store.currentSessionId && sessionId !== store.currentSessionId) return;
|
||||
store.addMessage({
|
||||
id: genMsgId('system'),
|
||||
role: 'system',
|
||||
content: `Provider 已切换: ${from ?? '未知'} → ${to ?? '未知'}${reason ? ` (${reason})` : ''}`,
|
||||
timestamp: Date.now(),
|
||||
|
||||
Reference in New Issue
Block a user