**崩溃/挂死修复 (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 (状态由右键菜单展示)
238 lines
9.1 KiB
TypeScript
238 lines
9.1 KiB
TypeScript
/**
|
|
* MetonaAI Desktop — Electron 主进程入口
|
|
*
|
|
* 启动流程(按架构规范强制顺序):
|
|
* 1. 初始化日志系统
|
|
* 2. 选择/创建工作空间 → 校验必需文件(缺失自动创建)
|
|
* 3. 连接 SQLite → 执行 schema 迁移 → 加载配置
|
|
* 4. 初始化 Provider Adapter
|
|
* 5. 加载 4 个磁盘文件 → 构建 System Prompt
|
|
* 6. 注册内置工具 + 连接 MCP Servers
|
|
* 7. 启动 React UI → Agent 就绪
|
|
*
|
|
* @see docs/MetonaAI-Desktop 架构与交互设计.html — 启动流程
|
|
* @see docs/MetonaAI-Desktop UI UX 设计集成方案.html — 窗口管理
|
|
*/
|
|
|
|
import { app, shell, Menu } from 'electron';
|
|
import { join } from 'path';
|
|
import { electronApp, optimizer } from '@electron-toolkit/utils';
|
|
import log from 'electron-log';
|
|
import { DatabaseService } from './services/database.service';
|
|
import { SessionService } from './services/session.service';
|
|
import { ConfigService } from './services/config.service';
|
|
import { WorkspaceService } from './services/workspace.service';
|
|
import { AuditService } from './services/audit.service';
|
|
import { SessionRecorder } from './services/session-recorder.service';
|
|
import { TrayManager } from './services/tray-manager.service';
|
|
import { WindowManager } from './services/window-manager.service';
|
|
import { MCPManager } from './services/mcp-manager.service';
|
|
import { ContextBuilder } from './harness/prompts/context-builder';
|
|
import { MemoryManager } from './harness/memory/manager';
|
|
import { registerAllIPCHandlers } from './ipc/handlers';
|
|
import { AgentLoopEngine } from './harness/agent-loop';
|
|
import { ToolRegistry } from './harness/tools/registry';
|
|
import { DeepSeekAdapter } from './harness/adapters/deepseek.adapter';
|
|
import { AgnesAdapter } from './harness/adapters/agnes-ai.adapter';
|
|
import { OllamaAdapter } from './harness/adapters/ollama.adapter';
|
|
import {
|
|
ReadFileTool, WriteFileTool, ListDirectoryTool, SearchFilesTool,
|
|
WebSearchTool, WebExtractTool,
|
|
MemoryStoreTool, MemorySearchTool,
|
|
RunCommandTool,
|
|
} from './harness/tools/built-in';
|
|
import { AuditLogHook, MemoryTriggerHook, PermissionCheckHook, RateLimitHook } from './harness/hooks';
|
|
import { PolicyEngine } from './harness/sandbox/permissions';
|
|
import { SandboxManager } from './harness/sandbox/sandbox';
|
|
import { PromptInjectionDefender } from './harness/security/prompt-injection-defense';
|
|
import { OutputValidator } from './harness/verification/output-validator';
|
|
|
|
// ===== 步骤 1: 初始化日志系统(SYS 层)=====
|
|
log.transports.file.level = 'info';
|
|
log.transports.console.level = 'debug';
|
|
|
|
let databaseService: DatabaseService | null = null;
|
|
let trayManager: TrayManager | null = null;
|
|
let windowManager: WindowManager | null = null;
|
|
|
|
async function initialize(): Promise<void> {
|
|
log.info('MetonaAI Desktop starting...');
|
|
electronApp.setAppUserModelId('com.metona.ai-desktop');
|
|
|
|
// 移除原生菜单栏
|
|
Menu.setApplicationMenu(null);
|
|
|
|
app.on('browser-window-created', (_, window) => {
|
|
optimizer.watchWindowShortcuts(window);
|
|
});
|
|
|
|
// ===== 步骤 2: 工作空间 =====
|
|
const workspaceService = new WorkspaceService();
|
|
const workspaceInfo = workspaceService.initialize();
|
|
log.info(`Workspace: ${workspaceInfo.path} (missing: ${workspaceInfo.missingFiles.join(', ') || 'none'})`);
|
|
|
|
// ===== 步骤 3: SQLite =====
|
|
databaseService = new DatabaseService(workspaceInfo.path);
|
|
databaseService.initialize();
|
|
const db = databaseService.getDB();
|
|
|
|
const sessionService = new SessionService(() => db);
|
|
const configService = new ConfigService(() => db);
|
|
|
|
// ===== 日志服务 =====
|
|
const auditService = new AuditService(() => db);
|
|
const sessionRecorder = new SessionRecorder(workspaceInfo.path);
|
|
|
|
// ===== 记忆系统 =====
|
|
const memoryManager = new MemoryManager(() => db);
|
|
memoryManager.initialize();
|
|
|
|
// ===== 步骤 4: Provider Adapter =====
|
|
const provider = configService.get<string>('llm.provider') ?? '';
|
|
const model = configService.get<string>('llm.model') ?? '';
|
|
const apiKey = configService.get<string>('llm.apiKey') ?? '';
|
|
const baseURL = configService.get<string>('llm.baseURL') ?? '';
|
|
|
|
if (!provider || !baseURL || !model) {
|
|
log.warn('LLM not configured. Please set provider, baseURL, and model in Settings.');
|
|
}
|
|
|
|
const adapterConfig = { provider, baseURL, apiKey, defaultModel: model };
|
|
let adapter;
|
|
switch (provider) {
|
|
case 'agnes': adapter = new AgnesAdapter(adapterConfig); break;
|
|
case 'ollama': adapter = new OllamaAdapter(adapterConfig); break;
|
|
default: adapter = new DeepSeekAdapter(adapterConfig); break;
|
|
}
|
|
|
|
// ===== 步骤 5: 工作空间文件 + System Prompt =====
|
|
const contextBuilder = new ContextBuilder();
|
|
|
|
// ===== 步骤 6: 注册 9 个内置工具 =====
|
|
const toolRegistry = new ToolRegistry();
|
|
toolRegistry.registerBuiltin(new ReadFileTool());
|
|
toolRegistry.registerBuiltin(new WriteFileTool());
|
|
toolRegistry.registerBuiltin(new ListDirectoryTool());
|
|
toolRegistry.registerBuiltin(new SearchFilesTool());
|
|
toolRegistry.registerBuiltin(new WebSearchTool());
|
|
toolRegistry.registerBuiltin(new WebExtractTool());
|
|
toolRegistry.registerBuiltin(new MemoryStoreTool(memoryManager));
|
|
toolRegistry.registerBuiltin(new MemorySearchTool(memoryManager));
|
|
toolRegistry.registerBuiltin(new RunCommandTool());
|
|
log.info(`Registered ${toolRegistry.size} built-in tools`);
|
|
|
|
// ===== MCP Manager =====
|
|
const mcpManager = new MCPManager(() => db, toolRegistry);
|
|
mcpManager.initialize().catch((err) => {
|
|
log.warn('MCP Manager initialization error:', err);
|
|
});
|
|
|
|
// ===== 安全模块 =====
|
|
const policyEngine = new PolicyEngine();
|
|
const sandboxManager = new SandboxManager({
|
|
allowedPaths: [workspaceInfo.path],
|
|
networkPolicy: 'allowlist',
|
|
});
|
|
const promptDefender = new PromptInjectionDefender();
|
|
const outputValidator = new OutputValidator();
|
|
|
|
// ===== Hooks =====
|
|
const preToolHooks = [
|
|
new PermissionCheckHook(policyEngine),
|
|
new RateLimitHook(20),
|
|
];
|
|
const postToolHooks = [
|
|
new AuditLogHook(auditService),
|
|
new MemoryTriggerHook(memoryManager),
|
|
];
|
|
|
|
// ===== Agent Loop =====
|
|
const ollamaNumCtx = configService.get<number>('ollama.numCtx');
|
|
const agentMaxIter = configService.get<number>('agent.maxIterations');
|
|
const agentTimeout = configService.get<number>('agent.totalTimeoutMs');
|
|
const agentLoop = new AgentLoopEngine(
|
|
{
|
|
maxIterations: agentMaxIter ?? 20,
|
|
totalTimeoutMs: agentTimeout ?? 600_000,
|
|
contextLength: ollamaNumCtx ?? undefined,
|
|
},
|
|
adapter, toolRegistry, preToolHooks, postToolHooks,
|
|
);
|
|
agentLoop.setTools(toolRegistry.listTools());
|
|
agentLoop.setWorkspacePath(workspaceInfo.path);
|
|
|
|
// ===== 窗口管理 =====
|
|
windowManager = new WindowManager();
|
|
const mainWindow = windowManager.createWindow({
|
|
id: 'main',
|
|
workspacePath: workspaceInfo.path,
|
|
title: 'MetonaAI Desktop',
|
|
});
|
|
|
|
// ===== 系统托盘 =====
|
|
const resourcesPath = join(__dirname, '../../assets');
|
|
trayManager = new TrayManager(resourcesPath);
|
|
trayManager.initialize(mainWindow);
|
|
|
|
// ===== 全局快捷键 =====
|
|
windowManager.registerGlobalShortcuts();
|
|
|
|
// ===== Agent 状态同步到托盘 =====
|
|
agentLoop.on('stateChange', (data: { previous: string; current: string }) => {
|
|
const statusMap: Record<string, 'idle' | 'thinking' | 'executing' | 'error'> = {
|
|
INIT: 'idle', THINKING: 'thinking', PARSING: 'thinking',
|
|
EXECUTING: 'executing', OBSERVING: 'executing', REFLECTING: 'thinking',
|
|
COMPRESSING: 'thinking', TERMINATED: 'idle',
|
|
};
|
|
trayManager?.setStatus(statusMap[data.current] ?? 'idle');
|
|
});
|
|
|
|
// ===== Agent 完成时发送系统通知 =====
|
|
agentLoop.on('complete', (data: { sessionId: string; durationMs: number }) => {
|
|
trayManager?.sendNotification(
|
|
'MetonaAI — 任务完成',
|
|
`Agent 已完成任务 (${(data.durationMs / 1000).toFixed(1)}s)`,
|
|
() => windowManager?.focusWindow(),
|
|
);
|
|
});
|
|
|
|
// ===== 注册 IPC =====
|
|
registerAllIPCHandlers(
|
|
mainWindow, sessionService, configService, workspaceService,
|
|
contextBuilder, agentLoop, toolRegistry, auditService,
|
|
sessionRecorder, memoryManager, mcpManager,
|
|
);
|
|
|
|
// ===== 应用生命周期 =====
|
|
app.on('window-all-closed', () => {
|
|
// macOS: 保持应用运行(托盘模式)
|
|
});
|
|
|
|
app.on('activate', () => {
|
|
if (windowManager && windowManager.count === 0) {
|
|
windowManager.createWindow({ id: 'main', workspacePath: workspaceInfo.path });
|
|
} else {
|
|
windowManager?.focusWindow();
|
|
}
|
|
});
|
|
|
|
app.on('before-quit', async () => {
|
|
// 标记为正在退出,允许窗口关闭(两处 isQuitting 统一设置)
|
|
(global as Record<string, unknown>).isQuitting = true;
|
|
TrayManager.markQuitting();
|
|
windowManager?.unregisterGlobalShortcuts();
|
|
trayManager?.destroy();
|
|
windowManager?.closeAll();
|
|
try { await mcpManager.shutdown(); } catch {}
|
|
if (databaseService) { databaseService.close(); databaseService = null; }
|
|
});
|
|
|
|
log.info(`MetonaAI Desktop initialized [Provider: ${provider}, Model: ${model}, Tools: ${toolRegistry.size}]`);
|
|
}
|
|
|
|
app.whenReady().then(initialize);
|
|
|
|
app.on('web-contents-created', (_, contents) => {
|
|
contents.setWindowOpenHandler(({ url }) => { shell.openExternal(url); return { action: 'deny' }; });
|
|
});
|