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:
thzxx
2026-06-27 21:33:27 +08:00
commit 1d185db6b3
109 changed files with 39155 additions and 0 deletions
+332
View File
@@ -0,0 +1,332 @@
/**
* Zustand Store — Agent 状态管理
*
* 管理 Agent 的实时状态:当前会话、消息流、流式输出、工具调用状态、Token 统计。
*/
import { create } from 'zustand';
import { useSessionStore } from './session-store';
// ===== 消息类型 =====
export type MessageRole = 'user' | 'assistant' | 'system' | 'tool';
export interface ToolCallInfo {
id: string;
name: string;
args: Record<string, unknown>;
status: 'pending' | 'executing' | 'success' | 'error' | 'blocked';
result?: unknown;
durationMs?: number;
error?: string;
}
export interface AttachmentInfo {
id: string;
name: string;
type: 'image' | 'text' | 'other';
size: number;
preview?: string; // 图片 base64 data URL
textContent?: string; // 文本文件内容
}
export interface ChatMessage {
id: string;
role: MessageRole;
content: string;
timestamp: number;
iteration?: number;
toolCalls?: ToolCallInfo[];
reasoningContent?: string;
attachments?: AttachmentInfo[];
}
// ===== Agent 状态 =====
export type AgentStatus = 'idle' | 'thinking' | 'executing' | 'error';
// ===== Token 统计 =====
export interface TokenUsage {
inputTokens: number;
outputTokens: number;
totalTokens: number;
}
// ===== Trace 步骤 =====
export interface TraceStep {
iteration: number;
state: string;
startedAt: number;
completedAt?: number;
thought?: string;
toolCalls?: ToolCallInfo[];
tokenUsage?: { promptTokens: number; completionTokens: number; totalTokens: number };
}
// ===== Store 状态 =====
interface AgentState {
// 会话
currentSessionId: string | null;
messages: ChatMessage[];
agentStatus: AgentStatus;
currentIteration: number;
maxIterations: number;
// Token 统计
tokenUsage: TokenUsage;
// Trace
traceSteps: TraceStep[];
// 流式
isStreaming: boolean;
streamingContent: string;
// Provider
provider: string;
model: string;
// Actions
setCurrentSession: (id: string | null) => void;
setMessages: (messages: ChatMessage[]) => void;
addMessage: (message: ChatMessage) => void;
updateMessage: (id: string, updates: Partial<ChatMessage>) => void;
sendMessage: (content: string, images?: Array<{ url: string; detail?: 'low' | 'high' | 'auto' }>, attachments?: AttachmentInfo[]) => void;
updateLastAssistantMessage: (delta: string) => void;
setAgentStatus: (status: AgentStatus) => void;
setStreaming: (streaming: boolean) => void;
appendStreamingContent: (delta: string) => void;
clearStreamingContent: () => void;
addTraceStep: (step: TraceStep) => void;
updateTraceStep: (iteration: number, updates: Partial<TraceStep>) => void;
updateTokenUsage: (usage: Partial<TokenUsage>) => void;
setProvider: (provider: string, model: string) => void;
setMaxIterations: (max: number) => void;
saveTraceData: () => void;
clearMessages: () => void;
abort: () => void;
}
export const useAgentStore = create<AgentState>((set, get) => ({
// ===== 初始状态 =====
currentSessionId: null,
messages: [],
agentStatus: 'idle',
currentIteration: 0,
maxIterations: 20,
tokenUsage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
traceSteps: [],
isStreaming: false,
streamingContent: '',
provider: '',
model: '',
// ===== Actions =====
setCurrentSession: (id) => {
set({ currentSessionId: id, messages: [], traceSteps: [], tokenUsage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 } });
// 从数据库加载该会话的消息
if (id && window.metona?.sessions?.getMessages) {
window.metona.sessions.getMessages(id).then((msgs) => {
const messages = (msgs as Array<{
id: string; role: string; content: string;
reasoningContent?: string; toolCalls?: unknown[];
attachments?: Array<{ id: string; name: string; type: string; size: number; preview?: string; textContent?: string }>;
timestamp: number;
}>).map((m) => ({
id: m.id,
role: m.role as ChatMessage['role'],
content: m.content,
reasoningContent: m.reasoningContent,
toolCalls: m.toolCalls as ToolCallInfo[] | undefined,
attachments: m.attachments as AttachmentInfo[] | undefined,
timestamp: m.timestamp,
}));
set({ messages });
}).catch(() => {});
}
// 从数据库加载该会话的 trace 步骤和 token 用量
if (id && window.metona?.sessions?.getTrace) {
window.metona.sessions.getTrace(id).then((data) => {
if (data) {
if (data.traceSteps) set({ traceSteps: data.traceSteps as TraceStep[] });
if (data.tokenUsage) set({ tokenUsage: data.tokenUsage as TokenUsage });
}
}).catch(() => {});
}
},
setMessages: (messages) => set({ messages }),
addMessage: (message) =>
set((s) => ({ messages: [...s.messages, message] })),
updateMessage: (id, updates) =>
set((s) => ({
messages: s.messages.map((m) =>
m.id === id ? { ...m, ...updates } : m,
),
})),
sendMessage: async (content: string, images?: Array<{ url: string; detail?: 'low' | 'high' | 'auto' }>, attachments?: AttachmentInfo[]) => {
let sessionId = get().currentSessionId;
// 没有当前会话时自动创建
if (!sessionId && window.metona?.sessions?.create) {
try {
const session = await window.metona.sessions.create() as { id: string; title: string; createdAt: number; updatedAt: number; messageCount: number; pinned: boolean; archived: boolean };
useSessionStore.getState().addSession({
id: session.id, title: session.title, createdAt: session.createdAt,
updatedAt: session.updatedAt, messageCount: session.messageCount,
pinned: session.pinned, archived: session.archived,
});
sessionId = session.id;
set({ currentSessionId: sessionId });
} catch {
// 创建失败时静默
}
}
const userMessage: ChatMessage = {
id: `msg_${Date.now()}_user`,
role: 'user',
content, // 用户可见内容(纯文本)
timestamp: Date.now(),
attachments: attachments && attachments.length > 0 ? attachments : undefined,
};
set((s) => ({
messages: [...s.messages, userMessage],
agentStatus: 'thinking',
isStreaming: true,
streamingContent: '',
}));
// 自动更新会话标题为用户第一条消息
if (sessionId && window.metona?.sessions?.getMessages) {
window.metona.sessions.getMessages(sessionId).then((msgs) => {
if (msgs.length <= 1) {
// 这是第一条消息,更新标题
const title = content.length > 30 ? content.slice(0, 30) + '...' : content;
window.metona?.sessions?.rename(sessionId, title);
useSessionStore.getState().updateSession(sessionId, { title });
}
}).catch(() => {});
}
if (sessionId && window.metona?.agent?.sendMessage) {
// 构建发给 LLM 的 content:纯用户文本 + 文件 JSON(无文字描述)
let llmContent = content;
const images: Array<{ url: string; detail?: 'low' | 'high' | 'auto' }> = [];
if (attachments && attachments.length > 0) {
const parts: string[] = [];
for (const att of attachments) {
if (att.type === 'image' && att.preview) {
// 图片:直接加入 images 数组
images.push({ url: att.preview, detail: 'auto' });
} else {
// 所有文件:JSON 格式,Base64 编码内容
const ext = att.name.split('.').pop() ?? 'unknown';
let base64Content = '';
if (att.textContent) {
base64Content = btoa(unescape(encodeURIComponent(att.textContent)));
} else if (att.preview) {
// 二进制文件从 data URL 提取 base64
base64Content = att.preview.split(',')[1] ?? '';
}
parts.push(JSON.stringify({
file_name: att.name,
file_type: ext,
context_encode: 'Base64',
context: base64Content,
}));
}
}
llmContent = parts.join('\n') + (content ? '\n\n' + content : '');
}
const messageWithImages = {
...userMessage,
content: llmContent,
images: images.length > 0 ? images : undefined,
};
window.metona.agent.sendMessage(messageWithImages, sessionId).catch(() => {});
}
},
updateLastAssistantMessage: (delta: string) => {
set((s) => {
const messages = [...s.messages];
const lastMsg = messages[messages.length - 1];
if (lastMsg?.role === 'assistant') {
lastMsg.content += delta;
} else {
messages.push({
id: `msg_${Date.now()}_assistant`,
role: 'assistant',
content: delta,
timestamp: Date.now(),
});
}
return { messages };
});
},
setAgentStatus: (status) => set({ agentStatus: status }),
setStreaming: (streaming) => set({ isStreaming: streaming }),
appendStreamingContent: (delta) =>
set((s) => ({ streamingContent: s.streamingContent + delta })),
clearStreamingContent: () => set({ streamingContent: '' }),
addTraceStep: (step) =>
set((s) => ({ traceSteps: [...s.traceSteps, step] })),
updateTraceStep: (iteration, updates) =>
set((s) => ({
traceSteps: s.traceSteps.map((t) =>
t.iteration === iteration ? { ...t, ...updates } : t,
),
})),
updateTokenUsage: (usage) =>
set((s) => ({ tokenUsage: { ...s.tokenUsage, ...usage } })),
setProvider: (provider, model) => set({ provider, model }),
setMaxIterations: (max) => set({ maxIterations: max }),
saveTraceData: () => {
const { currentSessionId, traceSteps, tokenUsage } = get();
if (currentSessionId && window.metona?.sessions?.saveTrace) {
window.metona.sessions.saveTrace(currentSessionId, { traceSteps, tokenUsage }).catch(() => {});
}
},
clearMessages: () =>
set({
messages: [],
agentStatus: 'idle',
currentIteration: 0,
tokenUsage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
traceSteps: [],
isStreaming: false,
streamingContent: '',
}),
abort: () => {
const sessionId = get().currentSessionId;
set({ agentStatus: 'idle', isStreaming: false });
if (sessionId && window.metona?.agent?.abortSession) {
window.metona.agent.abortSession(sessionId).catch(() => {});
}
},
}));