Files
metona-ai-desktop/src/hooks/useAgentStream.ts
T
thzxx 97a3d3d53b fix: 全面修复审计问题并优化系统提示词
**崩溃/挂死修复 (5):**
- 统一 TrayManager.isQuitting 变量,修复 Cmd+Q 无法退出
- useAgentStream 闭包过期快照 → 每次 getState()
- Agnes chatStream 添加 AbortSignal.timeout
- SSE JSON.parse 添加 try-catch 保护
- Orchestrator setTools 污染 → save/restore 模式

**功能修复 (14):**
- 上下文压缩实现 (每5轮 COMPRESSING 状态)
- 修复 requestId 硬编码空串
- ConfigService.set() 保留已有 category
- MemoryManager 新增 working 类型搜索
- PromptInjectionDefender 补全 sanitize()
- Ollama: 补全 dynamicReminders + reasoningContent
- openai-format: 所有 assistant 消息保留 reasoningContent
- SSE: finish_reason 时提前 flush tool_calls
- DeepSeek thinking effort 映射注释
- Ollama done_reason load→stop
- RateLimitHook >= 边界修复
- WorkspaceService isValid 首次启动修复
- sessions:archive IPC handler
- 托盘/窗口图标路径生产环境修复

**系统提示词优化:**
- SOUL.md 存在时不显示兜底身份,原文放最前
- 兜底身份改为中文 (MetonaAI 自身描述)
- 用户文本在前,附件内容在后

**文件上传:**
- 非图片文件不再 base64 编码,保留 JSON 结构
- 用户文本优先于文件内容

**UI 修复:**
- 首页 Logo 路径修复 (public/ + 相对路径)
- TokenUsage contextWindow 动态计算 (Provider 感知)
- 切换 Provider 同步 contextWindow
- 托盘图标始终显示 Logo (状态由右键菜单展示)
2026-06-30 23:17:38 +08:00

234 lines
7.7 KiB
TypeScript

/**
* useAgentStream — Agent 流式事件监听 Hook
*
* 监听 window.metona.agent.onStreamEvent,将事件分发到 Zustand Store。
* 负责流式文本追加、工具调用状态更新、思考内容处理。
*/
import { useEffect, useRef } from 'react';
import { useAgentStore, type ToolCallInfo } from '@renderer/stores/agent-store';
import { useSessionStore } from '@renderer/stores/session-store';
/**
* Agent 流式事件监听 Hook
*
* 在 App 根组件调用一次,自动监听当前会话的流式事件。
*/
export function useAgentStream(): void {
const cleanupRef = useRef<(() => void) | null>(null);
useEffect(() => {
// 检查 IPC 桥是否可用
if (!window.metona?.agent?.onStreamEvent) return;
const unsubscribe = window.metona.agent.onStreamEvent((event: unknown) => {
const data = event as {
type?: string;
requestId?: string;
sessionId?: string;
iteration?: number;
delta?: string;
content?: string;
toolCall?: { id: string; name: string; args: Record<string, unknown> };
toolResult?: { toolCallId: string; success: boolean; result?: unknown; error?: string; durationMs?: number };
usage?: { inputTokens?: number; outputTokens?: number; totalTokens?: number };
error?: { code: string; message: string };
state?: string;
};
// 每次都从 store 读取最新状态(避免闭包捕获过期快照)
const getStore = () => useAgentStore.getState();
// 每次收到迭代号时同步更新
if (data.iteration != null && data.iteration !== getStore().currentIteration) {
console.log(`[useAgentStream] iteration: ${getStore().currentIteration}${data.iteration} (event: ${data.type})`);
getStore().setCurrentIteration(data.iteration);
}
switch (data.type) {
// 思考开始
case 'thinking_start':
getStore().setAgentStatus('thinking');
getStore().addTraceStep({
iteration: data.iteration ?? getStore().currentIteration + 1,
state: 'THINKING',
startedAt: Date.now(),
});
break;
// 推理内容增量
case 'reasoning_delta':
if (data.delta) {
const messages = getStore().messages;
const lastMsg = messages[messages.length - 1];
if (lastMsg?.role === 'assistant') {
// 追加到最后一条 assistant 消息
getStore().updateMessage(lastMsg.id, {
reasoningContent: (lastMsg.reasoningContent ?? '') + data.delta,
});
} else {
// 还没有 assistant 消息,先创建一条(仅含思考内容)
getStore().addMessage({
id: `msg_${Date.now()}_assistant`,
role: 'assistant',
content: '',
reasoningContent: data.delta,
timestamp: Date.now(),
});
}
}
break;
// 思考结束
case 'thinking_end':
if (data.iteration) {
getStore().updateTraceStep(data.iteration, { completedAt: Date.now() });
}
break;
// 文本增量
case 'text_delta':
if (data.delta) {
getStore().setStreaming(true);
getStore().updateLastAssistantMessage(data.delta);
}
break;
// 工具调用完成
case 'tool_call_complete':
if (data.toolCall) {
const tc: ToolCallInfo = {
id: data.toolCall.id,
name: data.toolCall.name,
args: data.toolCall.args,
status: 'pending',
};
// 追加到当前 assistant 消息
const msgs = getStore().messages;
const lastAssistant = msgs[msgs.length - 1];
if (lastAssistant?.role === 'assistant') {
getStore().updateMessage(lastAssistant.id, {
toolCalls: [...(lastAssistant.toolCalls ?? []), tc],
});
}
getStore().setAgentStatus('executing');
const curIter = getStore().currentIteration;
getStore().addTraceStep({
iteration: data.iteration ?? curIter,
state: 'EXECUTING',
startedAt: Date.now(),
toolCalls: [tc],
});
}
break;
// 工具执行结果
case 'tool_result': {
if (data.toolResult) {
const msgs = getStore().messages;
const lastMsg = msgs[msgs.length - 1];
if (lastMsg?.toolCalls) {
const updatedToolCalls = lastMsg.toolCalls.map((tc) =>
tc.id === data.toolResult!.toolCallId
? {
...tc,
status: data.toolResult!.success ? 'success' as const : 'error' as const,
result: data.toolResult!.result,
error: data.toolResult!.error,
durationMs: data.toolResult!.durationMs,
}
: tc,
);
getStore().updateMessage(lastMsg.id, { toolCalls: updatedToolCalls });
}
}
break;
}
// Token 使用统计
case 'usage':
if (data.usage) {
getStore().updateTokenUsage({
inputTokens: data.usage.inputTokens ?? 0,
outputTokens: data.usage.outputTokens ?? 0,
totalTokens: data.usage.totalTokens ?? 0,
});
}
break;
// 流结束
case 'done':
getStore().setStreaming(false);
getStore().setAgentStatus('idle');
getStore().saveTraceData();
break;
// 错误
case 'error':
getStore().setStreaming(false);
getStore().setAgentStatus('error');
getStore().addMessage({
id: `msg_${Date.now()}_error`,
role: 'system',
content: `错误: ${data.error?.message ?? '未知错误'}`,
timestamp: Date.now(),
});
break;
// 状态变化
case 'state_change':
if (data.state) {
getStore().addTraceStep({
iteration: data.iteration ?? getStore().currentIteration,
state: data.state,
startedAt: Date.now(),
});
}
break;
}
});
cleanupRef.current = unsubscribe;
return () => {
cleanupRef.current?.();
};
}, []);
// 监听状态变化事件(含迭代号)
useEffect(() => {
if (!window.metona?.agent?.onStateChange) return;
const unsubscribe = window.metona.agent.onStateChange((state: unknown) => {
const data = state as { sessionId?: string; iteration?: number; state?: string; previous?: string; current?: string };
if (data.iteration != null) {
const store = useAgentStore.getState();
if (data.iteration !== store.currentIteration) {
store.setCurrentIteration(data.iteration);
}
}
});
return () => unsubscribe();
}, []);
// 监听 Provider 切换通知
useEffect(() => {
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`,
role: 'system',
content: `Provider 已切换: ${from ?? '未知'}${to ?? '未知'}${reason ? ` (${reason})` : ''}`,
timestamp: Date.now(),
});
});
return () => unsubscribe();
}, []);
}