feat: MetonaAI Desktop 初始项目
- Electron + React + TypeScript 架构 - 三栏布局: Sidebar | ChatPanel | DetailPanel - 9 个内置工具 (文件系统/网络/记忆/命令) - SQLite 持久化 (better-sqlite3) - MUI 暗色/亮色主题系统 - Agent Loop ReAct 状态机引擎 - DeepSeek / Agnes AI / Ollama Provider 适配器 - MCP 协议集成 - 系统托盘 + 全局快捷键 - Tailwind CSS v4 + Tailwind Merge - 修复: Sidebar 缺失 TextField 导入导致黑屏
This commit is contained in:
@@ -0,0 +1,220 @@
|
||||
/**
|
||||
* 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;
|
||||
};
|
||||
|
||||
const store = useAgentStore.getState();
|
||||
|
||||
switch (data.type) {
|
||||
// 思考开始
|
||||
case 'thinking_start':
|
||||
store.setAgentStatus('thinking');
|
||||
store.addTraceStep({
|
||||
iteration: data.iteration ?? store.currentIteration + 1,
|
||||
state: 'THINKING',
|
||||
startedAt: Date.now(),
|
||||
});
|
||||
break;
|
||||
|
||||
// 推理内容增量
|
||||
case 'reasoning_delta':
|
||||
if (data.delta) {
|
||||
const messages = store.messages;
|
||||
const lastMsg = messages[messages.length - 1];
|
||||
if (lastMsg?.role === 'assistant') {
|
||||
// 追加到最后一条 assistant 消息
|
||||
store.updateMessage(lastMsg.id, {
|
||||
reasoningContent: (lastMsg.reasoningContent ?? '') + data.delta,
|
||||
});
|
||||
} else {
|
||||
// 还没有 assistant 消息,先创建一条(仅含思考内容)
|
||||
store.addMessage({
|
||||
id: `msg_${Date.now()}_assistant`,
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
reasoningContent: data.delta,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
// 思考结束
|
||||
case 'thinking_end':
|
||||
if (data.iteration) {
|
||||
store.updateTraceStep(data.iteration, { completedAt: Date.now() });
|
||||
}
|
||||
break;
|
||||
|
||||
// 文本增量
|
||||
case 'text_delta':
|
||||
if (data.delta) {
|
||||
store.setStreaming(true);
|
||||
store.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 messages = store.messages;
|
||||
const lastMsg = messages[messages.length - 1];
|
||||
if (lastMsg?.role === 'assistant') {
|
||||
store.updateMessage(lastMsg.id, {
|
||||
toolCalls: [...(lastMsg.toolCalls ?? []), tc],
|
||||
});
|
||||
}
|
||||
|
||||
store.setAgentStatus('executing');
|
||||
store.addTraceStep({
|
||||
iteration: data.iteration ?? store.currentIteration,
|
||||
state: 'EXECUTING',
|
||||
startedAt: Date.now(),
|
||||
toolCalls: [tc],
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
// 工具执行结果
|
||||
case 'tool_result': {
|
||||
if (data.toolResult) {
|
||||
const messages = store.messages;
|
||||
const lastMsg = messages[messages.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,
|
||||
);
|
||||
store.updateMessage(lastMsg.id, { toolCalls: updatedToolCalls });
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Token 使用统计
|
||||
case 'usage':
|
||||
if (data.usage) {
|
||||
store.updateTokenUsage({
|
||||
inputTokens: data.usage.inputTokens ?? 0,
|
||||
outputTokens: data.usage.outputTokens ?? 0,
|
||||
totalTokens: data.usage.totalTokens ?? 0,
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
// 流结束
|
||||
case 'done':
|
||||
store.setStreaming(false);
|
||||
store.setAgentStatus('idle');
|
||||
// 保存 trace 数据到数据库
|
||||
store.saveTraceData();
|
||||
break;
|
||||
|
||||
// 错误
|
||||
case 'error':
|
||||
store.setStreaming(false);
|
||||
store.setAgentStatus('error');
|
||||
store.addMessage({
|
||||
id: `msg_${Date.now()}_error`,
|
||||
role: 'system',
|
||||
content: `错误: ${data.error?.message ?? '未知错误'}`,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
break;
|
||||
|
||||
// 状态变化
|
||||
case 'state_change':
|
||||
if (data.state) {
|
||||
store.addTraceStep({
|
||||
iteration: data.iteration ?? store.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) => {
|
||||
// 状态变化由主进程推送,UI 已通过 streamEvent 处理
|
||||
});
|
||||
|
||||
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();
|
||||
}, []);
|
||||
}
|
||||
Reference in New Issue
Block a user