Files
metona-ai-desktop/electron/ipc/handlers.ts
T
thzxx 97a3d3d53b fix: 全面修复审计问题并优化系统提示词
**崩溃/挂死修复 (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 (状态由右键菜单展示)
2026-06-30 23:17:38 +08:00

446 lines
14 KiB
TypeScript

/**
* IPC Handlers — 所有 IPC 通道处理逻辑
*
* 集成三层日志:
* - TOOL 层:AuditService 记录工具调用到 audit_logs 表
* - TRACE 层:SessionRecorder 写入 session_*.jsonl
* - AGENT 层:electron-log 记录状态转换
*
* @see docs/MetonaAI-Desktop 架构与交互设计.html — IPC 架构 + 日志分层
*/
import { ipcMain, BrowserWindow, shell, app, dialog } from 'electron';
import type { SessionService } from '../services/session.service';
import type { ConfigService } from '../services/config.service';
import type { WorkspaceService } from '../services/workspace.service';
import type { ContextBuilder } from '../harness/prompts/context-builder';
import type { AgentLoopEngine } from '../harness/agent-loop';
import type { ToolRegistry } from '../harness/tools/registry';
import type { AuditService } from '../services/audit.service';
import type { SessionRecorder } from '../services/session-recorder.service';
import type { MemoryManager } from '../harness/memory/manager';
import type { MCPManager } from '../services/mcp-manager.service';
import type { MetonaMessage, MetonaStreamEvent } from '../harness/types';
import { MetonaErrorCode, MetonaStreamEventType } from '../harness/types';
import type { MetonaError } from '../harness/types';
import log from 'electron-log';
/**
* 注册所有 IPC 处理器
*/
export function registerAllIPCHandlers(
mainWindow: BrowserWindow,
sessionService: SessionService,
configService: ConfigService,
workspaceService: WorkspaceService,
contextBuilder: ContextBuilder,
agentLoop: AgentLoopEngine,
_toolRegistry: ToolRegistry,
auditService: AuditService,
sessionRecorder: SessionRecorder,
memoryManager: MemoryManager,
mcpManager: MCPManager,
): void {
// ===== Agent 交互 =====
ipcMain.handle('agent:sendMessage', async (_event, userMessage: MetonaMessage, sessionId: string) => {
log.info('[AGENT] sendMessage:', sessionId, userMessage.content.slice(0, 80));
// TRACE 层:开始录制
sessionRecorder.startRecording(sessionId);
// TOOL 层:记录会话开始
auditService.logSessionStart(sessionId);
// 保存用户消息到数据库
sessionService.saveMessage({
sessionId,
role: 'user',
content: userMessage.content,
attachments: (userMessage as any).attachments,
});
// 加载历史消息
const historyRows = sessionService.getMessages(sessionId);
const history: MetonaMessage[] = historyRows
.filter((m) => m.role !== 'system')
.slice(0, -1)
.map((m) => ({
role: m.role as MetonaMessage['role'],
content: m.content,
reasoningContent: m.reasoningContent,
timestamp: m.timestamp,
}));
// 从工作空间文件构建 System Prompt
const workspaceFiles = workspaceService.getFiles();
const systemPrompt = contextBuilder.buildSystemPrompt(workspaceFiles);
// 监听 Agent Loop 事件
const onStreamEvent = (event: MetonaStreamEvent) => {
if (!mainWindow.isDestroyed()) {
mainWindow.webContents.send('agent:streamEvent', event);
}
};
const onStateChange = (data: { previous?: string; current?: string; sessionId?: string; iteration?: number; state?: string }) => {
// AGENT 层:记录状态转换
if (data.previous) log.info(`[AGENT] State: ${data.previous}${data.current}`);
// 转发到渲染进程(携带迭代号)
if (!mainWindow.isDestroyed()) {
mainWindow.webContents.send('agent:stateChange', data);
}
};
agentLoop.on('streamEvent', onStreamEvent);
agentLoop.on('stateChange', onStateChange);
try {
// TRACE 层:记录上下文构建
sessionRecorder.recordContextBuilt({
tokenCount: history.reduce((sum, m) => sum + Math.ceil(m.content.length / 2), 0),
usageRatio: 0,
});
// 启动 Agent Loop
const output = await agentLoop.runStream(userMessage, sessionId, history, systemPrompt);
// 保存 assistant 回复到数据库
sessionService.saveMessage({
sessionId,
role: 'assistant',
content: output.finalAnswer,
});
// 更新 Token 统计
if (output.totalTokenUsage.totalTokens > 0) {
sessionService.updateTokenUsage(sessionId, output.totalTokenUsage.totalTokens);
}
// 更新 MEMORY.md 时间戳
workspaceService.updateMemoryTimestamp();
// TOOL 层:记录会话结束
auditService.logSessionEnd({
sessionId,
totalIterations: output.iterations.length,
totalTokens: output.totalTokenUsage.totalTokens,
durationMs: output.durationMs,
terminationReason: output.terminationReason,
});
// TRACE 层:停止录制
sessionRecorder.stopRecording({
totalIterations: output.iterations.length,
totalTokens: output.totalTokenUsage.totalTokens,
durationMs: output.durationMs,
terminationReason: output.terminationReason,
});
log.info(`[AGENT] Completed: ${output.terminationReason}, ${output.iterations.length} iterations, ${output.durationMs}ms`);
return { success: true };
} catch (error) {
log.error('[AGENT] Error:', error);
// TOOL 层:记录错误
auditService.log({
sessionId,
eventType: 'error',
actor: 'agent',
target: 'agent_loop',
details: { error: (error as Error).message },
outcome: 'error',
});
// TRACE 层:停止录制
sessionRecorder.stopRecording({
totalIterations: 0,
totalTokens: 0,
durationMs: 0,
terminationReason: 'error',
});
// 发送错误事件到 UI
if (!mainWindow.isDestroyed()) {
const metonaError: MetonaError = {
code: MetonaErrorCode.UNKNOWN,
message: (error as Error).message,
retryable: false,
};
const errorEvent: MetonaStreamEvent = {
type: MetonaStreamEventType.ERROR,
requestId: '',
sessionId,
iteration: 0,
seq: 0,
timestamp: Date.now(),
error: metonaError,
};
mainWindow.webContents.send('agent:streamEvent', errorEvent);
}
return { success: false, error: (error as Error).message };
} finally {
agentLoop.off('streamEvent', onStreamEvent);
agentLoop.off('stateChange', onStateChange);
}
});
ipcMain.handle('agent:abortSession', async (_event, sessionId) => {
log.info('[AGENT] Abort:', sessionId);
agentLoop.abort();
// TOOL 层:记录中断
auditService.log({
sessionId,
eventType: 'session_end',
actor: 'user',
target: 'session',
details: { reason: 'user_abort' },
outcome: 'denied',
});
return { success: true };
});
// ===== 会话管理 =====
ipcMain.handle('sessions:list', async () => {
return sessionService.list();
});
ipcMain.handle('sessions:create', async (_event, title?: string) => {
return sessionService.create(title);
});
ipcMain.handle('sessions:rename', async (_event, sessionId, title) => {
return { success: sessionService.rename(sessionId, title) };
});
ipcMain.handle('sessions:delete', async (_event, sessionId) => {
return { success: sessionService.delete(sessionId) };
});
ipcMain.handle('sessions:getMessages', async (_event, sessionId) => {
return sessionService.getMessages(sessionId);
});
ipcMain.handle('sessions:pin', async (_event, sessionId, pinned) => {
return { success: sessionService.pin(sessionId, pinned) };
});
ipcMain.handle('sessions:archive', async (_event, sessionId, archived) => {
return { success: sessionService.archive(sessionId, archived) };
});
ipcMain.handle('sessions:deleteMessage', async (_event, messageId) => {
return { success: sessionService.deleteMessage(messageId) };
});
ipcMain.handle('sessions:clearMessages', async (_event, sessionId) => {
return { success: sessionService.clearMessages(sessionId) };
});
ipcMain.handle('sessions:saveTrace', async (_event, sessionId, data) => {
try {
sessionService.saveTraceData(sessionId, data);
return { success: true };
} catch (error) {
return { success: false, error: (error as Error).message };
}
});
ipcMain.handle('sessions:getTrace', async (_event, sessionId) => {
return sessionService.getTraceData(sessionId);
});
// ===== MCP 管理 =====
ipcMain.handle('mcp:listServers', async () => {
return mcpManager.getServerStates();
});
ipcMain.handle('mcp:addServer', async (_event, config: { name: string; transport: string; command?: string; args?: string[]; url?: string }) => {
try {
await mcpManager.addServer({
name: config.name,
transport: config.transport as 'stdio' | 'sse',
command: config.command,
args: config.args,
url: config.url,
enabled: true,
});
log.info(`MCP server added: ${config.name}`);
return { success: true };
} catch (error) {
return { success: false, error: (error instanceof Error ? error.message : String(error)) };
}
});
ipcMain.handle('mcp:removeServer', async (_event, name: string) => {
try {
await mcpManager.removeServer(name);
return { success: true };
} catch {
return { success: false };
}
});
ipcMain.handle('mcp:toggleServer', async (_event, name: string, enabled: boolean) => {
try {
await mcpManager.toggleServer(name, enabled);
return { success: true };
} catch {
return { success: false };
}
});
// ===== 记忆 =====
ipcMain.handle('db:searchMemories', async (_event, query, options) => {
return memoryManager.search(query, options);
});
// ===== 配置 =====
ipcMain.handle('config:get', async (_event, key) => {
return configService.get(key);
});
ipcMain.handle('config:set', async (_event, key, value) => {
configService.set(key, value);
// TOOL 层:记录配置变更
auditService.log({
sessionId: '',
eventType: 'config_change',
actor: 'user',
target: key,
details: { value },
outcome: 'success',
});
return { success: true };
});
// ===== 应用工具 =====
ipcMain.handle('app:getVersion', async () => {
return app.getVersion();
});
ipcMain.handle('app:getAppDataPath', async () => {
return app.getPath('userData');
});
ipcMain.handle('app:openExternal', async (_event, url) => {
await shell.openExternal(url);
});
ipcMain.handle('app:showItemInFolder', async (_event, path) => {
shell.showItemInFolder(path);
});
ipcMain.handle('app:selectFolder', async (_event, defaultPath?: string) => {
const result = await dialog.showOpenDialog(mainWindow, {
properties: ['openDirectory', 'createDirectory'],
defaultPath: defaultPath ?? app.getPath('home'),
title: '选择工作空间目录',
});
if (result.canceled || result.filePaths.length === 0) return { canceled: true, path: '' };
return { canceled: false, path: result.filePaths[0] };
});
// ===== 工具管理 =====
ipcMain.handle('tools:list', async () => {
return _toolRegistry.listTools().map((t) => ({
name: t.name,
description: t.description,
category: t.category,
riskLevel: t.riskLevel,
requiresPermission: t.requiresPermission,
}));
});
ipcMain.handle('tools:toggle', async (_event, toolName: string, enabled: boolean) => {
// 工具开关通过配置持久化
configService.set(`tools.${toolName}.enabled`, enabled);
log.info(`Tool ${toolName} ${enabled ? 'enabled' : 'disabled'}`);
return { success: true };
});
// ===== 数据管理 =====
ipcMain.handle('data:export', async (_event, sessionId?: string) => {
try {
const db = sessionService.getDB();
if (sessionId) {
// 导出单个会话
const messages = sessionService.getMessages(sessionId);
return { success: true, data: messages };
}
// 导出所有会话
const sessions = sessionService.list();
const allData: Record<string, unknown> = { sessions: [], config: configService.getAll() };
for (const session of sessions) {
(allData.sessions as Array<Record<string, unknown>>).push({
...session,
messages: sessionService.getMessages(session.id),
});
}
return { success: true, data: allData };
} catch (error) {
return { success: false, error: (error as Error).message };
}
});
ipcMain.handle('data:clearSessions', async () => {
try {
const db = sessionService.getDB();
db.exec('DELETE FROM messages');
db.exec('DELETE FROM sessions');
log.info('[DATA] All sessions cleared');
return { success: true };
} catch (error) {
return { success: false, error: (error as Error).message };
}
});
ipcMain.handle('data:clearMemories', async () => {
try {
const db = sessionService.getDB();
db.exec('DELETE FROM episodic_memories');
db.exec('DELETE FROM semantic_memories');
db.exec('DELETE FROM working_memories');
log.info('[DATA] All memories cleared');
return { success: true };
} catch (error) {
return { success: false, error: (error as Error).message };
}
});
ipcMain.handle('data:clearAuditLogs', async () => {
try {
// 审计日志是 INSERT-ONLY,需要先禁用触发器
const db = sessionService.getDB();
db.exec('DROP TRIGGER IF EXISTS audit_no_delete');
db.exec('DELETE FROM audit_logs');
db.exec(`
CREATE TRIGGER audit_no_delete BEFORE DELETE ON audit_logs
BEGIN
SELECT RAISE(ABORT, 'Audit logs are INSERT-ONLY. Deletion is not allowed.');
END
`);
log.info('[DATA] Audit logs cleared');
return { success: true };
} catch (error) {
return { success: false, error: (error as Error).message };
}
});
log.info('[SYS] All IPC handlers registered');
}