Files
metona-ai-desktop/electron/main.ts
T
thzxx aa13a98f63 feat: 升级至 v0.2.2 — 工作空间路径注入、Provider 切换修复、编码与样式优化
功能增强:
- System Prompt 注入当前工作空间路径到动态区(LLM 可使用绝对路径调用工具)
- ContextBuilder.buildSystemPrompt 新增 workspacePath 参数

Provider 切换修复:
- reloadAdapter 返回 boolean,失败时 sendMessage 中止发送
- 切换 Provider 时自动清空 apiKey(不同 Provider key 不通用)
- apiKey 为空时显示警告提示
- Provider 切换事件加 sessionId 字段

编码修复:
- run_command 使用 encoding: 'buffer' 获取原始字节
- 新增 decodeBuffer 智能解码:UTF-8 严格 → GBK → UTF-8 宽松
- 修复不响应 chcp 的 Windows 命令中文乱码问题

样式优化:
- 代码块底色改用主题变量(灰底主题色文字,双主题适配)
- 表格标题 th/td 底色和文字色改用主题变量
- CodeBlock 组件 pre 背景从 secondary.main 改为 background.default
- 设置面板已自动执行工具行 opacity 0.8→0.15(淡绿底,文字清晰)

版本号 0.2.1 → 0.2.2
2026-07-12 13:46:09 +08:00

410 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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 { existsSync, readFileSync, writeFileSync } from 'fs';
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 { MemoryConsolidator } from './harness/memory/consolidator';
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, WebFetchTool,
MemoryStoreTool, MemorySearchTool,
RunCommandTool,
WebBrowserTool, cleanupBrowser,
DelegateTaskTool,
// v0.2.0 新增工具
FileEditorTool,
CodeSearchTool,
TaskManagerTool,
DiffViewerTool,
} from './harness/tools/built-in';
import { AuditLogHook, MemoryTriggerHook, PermissionCheckHook, RateLimitHook } from './harness/hooks';
import { ConfirmationHook } from './harness/hooks/confirmation-hook';
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';
import { TaskOrchestrator } from './harness/orchestration/orchestrator';
import { UpdateService } from './services/update.service';
// ===== 步骤 1: 初始化日志系统(SYS 层)=====
log.transports.file.level = 'info';
log.transports.console.level = 'debug';
// ===== 工作空间路径独立存储(解决 DB 在 workspace 内的鸡生蛋问题)=====
const WORKSPACE_CONFIG_FILE = join(app.getPath('userData'), 'workspace-config.json');
function readWorkspacePathFromFile(): string | null {
try {
if (existsSync(WORKSPACE_CONFIG_FILE)) {
const data = JSON.parse(readFileSync(WORKSPACE_CONFIG_FILE, 'utf-8'));
return data.workspacePath ?? null;
}
} catch {
// 忽略读取错误
}
return null;
}
function writeWorkspacePathToFile(workspacePath: string): void {
try {
writeFileSync(WORKSPACE_CONFIG_FILE, JSON.stringify({ workspacePath }, null, 2), 'utf-8');
} catch (err) {
log.error('Failed to write workspace config file:', err);
}
}
export { readWorkspacePathFromFile, writeWorkspacePathToFile };
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 savedWorkspacePath = readWorkspacePathFromFile();
const workspaceService = new WorkspaceService(savedWorkspacePath ?? undefined);
const workspaceInfo = workspaceService.initialize();
// 持久化工作空间路径(供下次启动读取)
if (savedWorkspacePath !== workspaceInfo.path) {
writeWorkspacePathToFile(workspaceInfo.path);
}
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 createAdapter = () => {
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.');
}
if (!apiKey && provider !== 'ollama') {
throw new Error(`API key is required for provider "${provider || 'unknown'}". Please set it in Settings.`);
}
const adapterConfig = { provider, baseURL, apiKey, defaultModel: model };
switch (provider) {
case 'agnes': return new AgnesAdapter(adapterConfig);
case 'ollama': return new OllamaAdapter(adapterConfig);
default: return new DeepSeekAdapter(adapterConfig);
}
};
const adapter = createAdapter();
// ===== 步骤 5: 工作空间文件 + System Prompt =====
const contextBuilder = new ContextBuilder();
// ===== v0.2.0: 安全模块(必须在工具注册之前,便于 RunCommandTool 注入 SandboxManager=====
const policyEngine = new PolicyEngine();
const sandboxManager = new SandboxManager({
allowedPaths: [workspaceInfo.path],
networkPolicy: 'allowlist',
});
const promptDefender = new PromptInjectionDefender();
const outputValidator = new OutputValidator();
// v0.2.0: ConfirmationHook(提前创建,mainWindow 创建后再注入)
// 注入 ConfigService 以支持持久化自动执行设置
const confirmationHook = new ConfirmationHook(null, configService);
// ===== 步骤 6: 注册内置工具 =====
const toolRegistry = new ToolRegistry();
toolRegistry.registerBuiltin(new ReadFileTool());
toolRegistry.registerBuiltin(new WriteFileTool());
toolRegistry.registerBuiltin(new ListDirectoryTool());
toolRegistry.registerBuiltin(new SearchFilesTool());
// v0.2.0: 新增文件工具
toolRegistry.registerBuiltin(new FileEditorTool());
toolRegistry.registerBuiltin(new CodeSearchTool());
toolRegistry.registerBuiltin(new DiffViewerTool());
// WebFetchTool 先于 WebSearchTool 构造,注入为依赖
const webFetchTool = new WebFetchTool();
toolRegistry.registerBuiltin(webFetchTool);
toolRegistry.registerBuiltin(new WebSearchTool(configService, webFetchTool));
toolRegistry.registerBuiltin(new MemoryStoreTool(memoryManager));
toolRegistry.registerBuiltin(new MemorySearchTool(memoryManager));
// v0.2.0: RunCommandTool 注入 SandboxManager
const runCommandTool = new RunCommandTool();
runCommandTool.setSandboxManager(sandboxManager);
toolRegistry.registerBuiltin(runCommandTool);
// v0.2.0: 任务管理工具
toolRegistry.registerBuiltin(new TaskManagerTool(() => db));
// 注册 Web Browser 统一浏览器工具
toolRegistry.registerBuiltin(new WebBrowserTool());
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);
});
// ===== Hooks =====
// v0.2.0: ConfirmationHook 注入到 preToolHooks 管道
// 注意:setToolDefs 延迟到 DelegateTaskTool 注册后调用,确保包含所有工具的风险等级
const preToolHooks = [
new PermissionCheckHook(policyEngine),
new RateLimitHook(20),
confirmationHook,
];
const postToolHooks = [
new AuditLogHook(auditService),
new MemoryTriggerHook(memoryManager),
];
// ===== Agent Loop =====
// TODO: Re-read agent config on each runStream call or when config changes
const ollamaNumCtx = configService.get<number>('ollama.numCtx');
const agentMaxIter = configService.get<number>('agent.maxIterations');
const agentTimeout = configService.get<number>('agent.totalTimeoutMs');
const agentThinkingEnabled = configService.get<boolean>('agent.enableThinking');
const agentThinkingEffort = configService.get<string>('agent.thinkingEffort') as 'low' | 'medium' | 'high' | 'max' | null;
const agentLoop = new AgentLoopEngine(
{
maxIterations: agentMaxIter ?? 20,
totalTimeoutMs: agentTimeout ?? 600_000,
contextLength: ollamaNumCtx ?? undefined,
thinkingEnabled: agentThinkingEnabled ?? true,
thinkingEffort: agentThinkingEffort ?? 'high',
},
adapter, toolRegistry, preToolHooks, postToolHooks,
);
agentLoop.setTools(toolRegistry.listTools());
agentLoop.setWorkspacePath(workspaceInfo.path);
// ===== Memory Consolidator(会话结束 AI 提取重要记忆到 MEMORY.md=====
const memoryConsolidator = new MemoryConsolidator(adapter, workspaceService);
// ===== Task Orchestrator(子任务委派)=====
const orchestrator = new TaskOrchestrator(
agentLoop, toolRegistry, preToolHooks, postToolHooks,
{
thinkingEnabled: agentThinkingEnabled ?? true,
thinkingEffort: agentThinkingEffort ?? 'high',
contextLength: ollamaNumCtx ?? undefined,
},
);
toolRegistry.registerBuiltin(new DelegateTaskTool(orchestrator));
// 重新设置工具列表,包含新注册的 delegate_task
agentLoop.setTools(toolRegistry.listTools());
// v0.2.0: 在所有工具(包括 DelegateTaskTool)注册完成后,刷新 ConfirmationHook 的工具定义缓存
confirmationHook.setToolDefs(toolRegistry.listAllTools());
// ===== 热重载 Adapter 回调(设置变更时触发)=====
let lastProvider = configService.get<string>('llm.provider') ?? '';
const reloadAdapter = (): boolean => {
try {
const newAdapter = createAdapter();
agentLoop.setAdapter(newAdapter);
memoryConsolidator.setAdapter(newAdapter);
// Provider 切换时同步 contextLength:仅 Ollama 使用 numCtx 作为有效上下文窗口
const provider = configService.get<string>('llm.provider') ?? '';
if (provider === 'ollama') {
const numCtx = configService.get<number>('ollama.numCtx');
agentLoop.updateConfig({ contextLength: numCtx ?? undefined });
} else {
agentLoop.updateConfig({ contextLength: undefined });
}
log.info(`[CONFIG] Adapter reloaded: provider=${provider}`);
// 通知渲染进程 Provider 已切换(UI 显示 Toast + 系统消息)
if (mainWindow && !mainWindow.isDestroyed()) {
if (lastProvider && lastProvider !== provider) {
mainWindow.webContents.send('agent:providerSwitched', {
from: lastProvider,
to: provider,
reason: 'config_changed',
sessionId: '',
});
}
mainWindow.webContents.send('toast:show', {
type: 'success',
message: `Provider 已切换: ${lastProvider || '未知'}${provider}`,
});
}
lastProvider = provider;
return true;
} catch (err) {
log.error(`[CONFIG] Failed to reload adapter: ${(err as Error).message}`);
// 通知渲染进程 Provider 切换失败(UI 显示错误 Toast)
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('toast:show', {
type: 'error',
message: `Provider 切换失败: ${(err as Error).message}`,
});
}
return false;
}
};
// ===== 窗口管理 =====
windowManager = new WindowManager();
const mainWindow = windowManager.createWindow({
id: 'main',
workspacePath: workspaceInfo.path,
title: 'MetonaAI Desktop',
});
// v0.2.0: 为 ConfirmationHook 注入主窗口(用于向渲染进程发送确认请求)
confirmationHook.setMainWindow(mainWindow);
// ===== 系统托盘 =====
const resourcesPath = join(__dirname, '../../assets');
trayManager = new TrayManager(resourcesPath);
trayManager.initialize(mainWindow);
// ===== 全局快捷键 =====
windowManager.registerGlobalShortcuts();
// ===== Agent 状态同步到托盘 =====
agentLoop.on('stateChange', (data: { previous: string; current: string; state?: string }) => {
const statusMap: Record<string, 'idle' | 'thinking' | 'executing' | 'error'> = {
INIT: 'idle', THINKING: 'thinking', PARSING: 'thinking',
EXECUTING: 'executing', OBSERVING: 'thinking', REFLECTING: 'thinking',
COMPRESSING: 'thinking', TERMINATED: 'idle',
};
const stateValue = (data.current || data.state) ?? '';
trayManager?.setStatus(statusMap[stateValue] ?? '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, createAdapter,
promptDefender, outputValidator,
confirmationHook, memoryConsolidator,
);
// TODO: Initialize UpdateService for auto-update functionality
// const updateService = new UpdateService();
// updateService.initialize(mainWindow);
// ===== 应用生命周期 =====
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 (err) { log.error('[Shutdown] MCP shutdown failed:', err); }
cleanupBrowser();
if (databaseService) { databaseService.close(); databaseService = null; }
});
// ===== 应用日志级别配置 =====
const logLevel = configService.get<string>('logging.level') as 'error' | 'warn' | 'info' | 'debug' | 'verbose' | 'silly' | null;
if (logLevel) {
log.transports.file.level = logLevel;
log.transports.console.level = logLevel;
log.info(`Log level applied: ${logLevel}`);
}
// ===== 恢复工具启用/禁用状态 =====
for (const toolDef of toolRegistry.listAllTools()) {
const stored = configService.get<boolean>(`tools.${toolDef.name}.enabled`);
if (stored === false) {
toolRegistry.setToolEnabled(toolDef.name, false);
log.info(`Tool restored as disabled: ${toolDef.name}`);
}
}
log.info(`MetonaAI Desktop initialized [Provider: ${configService.get<string>('llm.provider') ?? 'none'}, Model: ${configService.get<string>('llm.model') ?? 'none'}, Tools: ${toolRegistry.size}]`);
}
app.whenReady().then(initialize);
app.on('web-contents-created', (_, contents) => {
contents.setWindowOpenHandler(({ url }) => { shell.openExternal(url); return { action: 'deny' }; });
});