Files
metona-ai-desktop/electron/main.ts
T
thzxx 9ed4837950 fix: 上下文压缩阈值与摘要截断修复
1. engine: 压缩阈值改用 contextLength ?? contextWindow, Ollama 小 numCtx 时正确触发压缩

2. main: reloadAdapter 在 provider 切换时同步 contextLength, 非 Ollama 清除避免残留

3. engine: compressMessages 移除每条消息 500 字符截断, 摘要请求是独立 API 调用无需截断

4. delegate-task: 修复 items 缺少 description 的 TS2741 预存错误
2026-07-07 21:22:24 +08:00

343 lines
13 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 { 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 { 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,
} 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';
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();
// ===== 步骤 6: 注册内置工具 =====
const toolRegistry = new ToolRegistry();
toolRegistry.registerBuiltin(new ReadFileTool());
toolRegistry.registerBuiltin(new WriteFileTool());
toolRegistry.registerBuiltin(new ListDirectoryTool());
toolRegistry.registerBuiltin(new SearchFilesTool());
// 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));
toolRegistry.registerBuiltin(new RunCommandTool());
// 注册 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);
});
// ===== 安全模块 =====
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 =====
// 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);
// ===== 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());
// ===== 热重载 Adapter 回调(设置变更时触发)=====
const reloadAdapter = () => {
const newAdapter = createAdapter();
agentLoop.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 });
}
};
// ===== 窗口管理 =====
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; state?: 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',
};
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,
);
// 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' }; });
});