Files
metona-ai-desktop/electron/ipc/handlers.ts
T
thzxx 0363176215 feat: 工作空间切换校验 + 初始化检测 + 继承基础设置
新增功能:
- workspace:check IPC — 校验路径有效性 + 检测 4 个必需文件状态
- workspace:inheritFiles IPC — 从旧工作空间继承 SOUL/AGENTS/USERS 到新工作空间
- app:restart IPC — 渲染进程触发应用重启(app.relaunch + app.exit)

前端 WorkspaceSettings 改造:
- 选择文件夹后立即校验路径(是否存在、是否目录、4 个必需文件状态)
- 显示工作空间状态:
  · 新工作空间 → 提示将自动创建 4 个文件
  · 部分文件缺失 → 提示缺失文件将自动创建
  · 已有工作空间 → 提示将直接加载现有配置
- 继承选项:当切换到新工作空间时,可勾选继承 SOUL/AGENTS/USERS.md
  · MEMORY.md 不继承(记忆与工作空间项目上下文绑定)
  · 继承白名单:仅允许 SOUL.md、AGENTS.md、USERS.md(防路径遍历)
- 确认切换按钮 + 取消按钮
- 切换完成后弹出重启确认对话框(立即重启 / 稍后手动重启)

安全考虑:
- workspace:inheritFiles 使用文件名白名单,防止路径遍历攻击
- workspace:check 返回 resolvedPath,前端只展示不直接用于文件操作
- app:restart 延迟 200ms 让 IPC 响应先返回
2026-07-12 13:59:31 +08:00

938 lines
35 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.
/**
* 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 { join } from 'path';
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 { PromptInjectionDefender } from '../harness/security/prompt-injection-defense';
import type { OutputValidator } from '../harness/verification/output-validator';
import type { ConfirmationHook } from '../harness/hooks/confirmation-hook';
import type { MemoryConsolidator } from '../harness/memory/consolidator';
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,
reloadAdapter: () => boolean,
promptInjectionDefender: PromptInjectionDefender,
outputValidator: OutputValidator,
confirmationHook: ConfirmationHook,
memoryConsolidator: MemoryConsolidator,
): void {
// ===== Agent 交互 =====
ipcMain.handle('agent:sendMessage', async (_event, userMessage: MetonaMessage, sessionId: string) => {
log.info('[AGENT] sendMessage:', sessionId, userMessage.content.slice(0, 80));
// 发送消息前确保 Adapter 使用最新配置(失败则中止,防止用旧 Provider 的 adapter 发送)
if (!reloadAdapter()) {
const errorMsg = 'Adapter 加载失败,请检查 LLM 配置(Provider、API Key、Base URL、Model 是否完整)';
log.error('[AGENT]', errorMsg);
if (!mainWindow.isDestroyed()) {
const errorEvent: MetonaStreamEvent = {
type: MetonaStreamEventType.ERROR,
requestId: '',
sessionId,
iteration: 0,
seq: 0,
timestamp: Date.now(),
error: { code: MetonaErrorCode.UNKNOWN, message: errorMsg, retryable: false },
};
mainWindow.webContents.send('agent:streamEvent', errorEvent);
mainWindow.webContents.send('agent:streamEvent', {
...errorEvent,
type: MetonaStreamEventType.DONE,
});
}
sessionRecorder.stopRecording({ totalIterations: 0, totalTokens: 0, durationMs: 0, terminationReason: 'error' });
return { success: false, error: errorMsg };
}
// TRACE 层:开始录制
sessionRecorder.startRecording(sessionId);
// TOOL 层:记录会话开始
auditService.logSessionStart(sessionId);
// 保存用户消息到数据库
// IPC boundary: frontend sends attachments not defined in the MetonaMessage type
sessionService.saveMessage({
sessionId,
role: 'user',
content: userMessage.content,
attachments: (userMessage as MetonaMessage & { attachments?: unknown[] }).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,
toolCalls: m.toolCalls as MetonaMessage['toolCalls'],
toolResult: m.toolResult as MetonaMessage['toolResult'],
timestamp: m.timestamp,
}));
// 从工作空间文件构建 System Prompt(注入当前工作空间路径,便于 LLM 在需要绝对路径时使用)
const workspaceFiles = workspaceService.getFiles();
const systemPrompt = contextBuilder.buildSystemPrompt(workspaceFiles, workspaceService.getPath());
// 检索与用户消息相关的记忆,注入到 System Prompt 动态区
try {
const memories = memoryManager.search(userMessage.content, { topK: 5, minImportance: 0.3 });
if (memories.length > 0) {
const memorySection = memories.map((m, i) =>
`[${i + 1}] (${m.type}, 重要度: ${m.importance.toFixed(1)}) ${m.content.slice(0, 200)}`,
).join('\n');
const memoryBlock = `## Relevant Memories (Retrieved)\n${memorySection}`;
systemPrompt.dynamicReminders = systemPrompt.dynamicReminders
? `${systemPrompt.dynamicReminders}\n\n---\n\n${memoryBlock}`
: memoryBlock;
log.debug(`[AGENT] Injected ${memories.length} memories into system prompt`);
}
} catch (err) {
log.warn('[AGENT] Memory retrieval failed, proceeding without memories:', err);
}
// 监听 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; runId?: string }) => {
// AGENT 层:记录状态转换
if (data.previous) log.info(`[AGENT] State: ${data.previous}${data.current}`);
// 转发到渲染进程(携带迭代号)
if (!mainWindow.isDestroyed()) {
mainWindow.webContents.send('agent:stateChange', data);
}
};
// M-6: 转发上下文压缩事件为 toast 通知
const onCompressed = (data: { iteration?: number; originalTokens?: number; compressedTokens?: number }) => {
if (!mainWindow.isDestroyed()) {
mainWindow.webContents.send('toast:show', {
type: 'info',
message: `上下文压缩: ${data.originalTokens ?? '?'}${data.compressedTokens ?? '?'} tokens`,
});
}
};
agentLoop.on('streamEvent', onStreamEvent);
agentLoop.on('stateChange', onStateChange);
agentLoop.on('compressed', onCompressed);
try {
// 提示注入检测(安全模块)
const injectionResult = promptInjectionDefender.detect(userMessage.content);
if (injectionResult.riskScore >= 7) {
log.warn('[PromptInjectionDefender] Blocked message:', injectionResult.findings);
if (!mainWindow.isDestroyed()) {
const metonaError: MetonaError = {
code: MetonaErrorCode.UNKNOWN,
message: `Message blocked by prompt injection defense: ${injectionResult.recommendation}`,
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);
}
// TRACE 层:停止录制
sessionRecorder.stopRecording({
totalIterations: 0,
totalTokens: 0,
durationMs: 0,
terminationReason: 'error',
});
return { success: false, error: 'Message blocked by prompt injection defense' };
}
if (injectionResult.riskScore >= 4) {
log.warn('[PromptInjectionDefender] Suspicious patterns detected:', injectionResult.findings);
}
// 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);
// 输出验证(不阻塞响应,仅记录警告)
try {
const validation = await outputValidator.validate(output.finalAnswer);
if (!validation.valid || validation.issues.length > 0) {
log.warn('[OutputValidator] Validation issues:', validation.issues);
}
log.debug(`[OutputValidator] Score: ${validation.score}, Valid: ${validation.valid}`);
} catch (err) {
log.error('[OutputValidator] Validation failed:', err);
}
// 保存每轮迭代的 assistant 消息到数据库(含思考内容和工具调用)
for (const step of output.iterations) {
if (!step.thought) continue;
// 将 MetonaToolCall + MetonaToolResult 转换为前端 ToolCallInfo 格式
const toolCallsWithResults = step.toolCalls?.map((tc) => {
const result = step.toolResults?.find((r) => r.toolCallId === tc.id);
return {
id: tc.id,
name: tc.name,
args: tc.args,
status: result?.success ? 'success' as const : 'error' as const,
result: result?.result,
durationMs: result?.durationMs,
error: result?.error,
};
});
// 只有当有内容、思考内容或工具调用时才保存
if (step.thought.content || step.thought.reasoningContent || toolCallsWithResults?.length) {
sessionService.saveMessage({
sessionId,
role: 'assistant',
content: step.thought.content,
reasoningContent: step.thought.reasoningContent || undefined,
toolCalls: toolCallsWithResults,
iteration: step.iteration,
});
}
}
// 更新 Token 统计
if (output.totalTokenUsage.totalTokens > 0) {
sessionService.updateTokenUsage(sessionId, output.totalTokenUsage.totalTokens);
}
// 更新 MEMORY.md 时间戳
workspaceService.updateMemoryTimestamp();
// 会话结束:AI 判断本次对话有哪些重要内容需要持久化到 MEMORY.md
// 异步执行,不阻塞主流程返回;失败仅记录日志
memoryConsolidator
.consolidate(userMessage.content, output.finalAnswer, output.iterations)
.then((result) => {
if (result.appended > 0) {
log.info(`[AGENT] Memory consolidated: ${result.appended} entries appended to MEMORY.md`);
// 通知渲染进程记忆已更新
if (!mainWindow.isDestroyed()) {
mainWindow.webContents.send('toast:show', {
type: 'info',
message: `AI 已将 ${result.appended} 条重要记忆写入 MEMORY.md`,
});
}
}
})
.catch((err) => {
log.warn('[AGENT] Memory consolidation failed:', err);
});
// 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);
agentLoop.off('compressed', onCompressed);
}
});
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 (error) {
return { success: false, error: (error instanceof Error ? error.message : String(error)) };
}
});
ipcMain.handle('mcp:toggleServer', async (_event, name: string, enabled: boolean) => {
try {
await mcpManager.toggleServer(name, enabled);
return { success: true };
} catch (error) {
return { success: false, error: (error instanceof Error ? error.message : String(error)) };
}
});
// ===== 记忆 =====
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',
});
// LLM 相关配置变更时热重载 Adapter
if (['llm.provider', 'llm.model', 'llm.apiKey', 'llm.baseURL', 'ollama.numCtx'].includes(key)) {
reloadAdapter();
log.info(`[CONFIG] LLM config changed (${key}), adapter reloaded`);
}
// Agent 配置变更时热更新 Engine
if (key === 'agent.maxIterations') {
agentLoop.updateConfig({ maxIterations: value as number });
log.info(`[CONFIG] Agent maxIterations updated to ${value}`);
} else if (key === 'agent.totalTimeoutMs') {
agentLoop.updateConfig({ totalTimeoutMs: value as number });
log.info(`[CONFIG] Agent totalTimeoutMs updated to ${value}`);
} else if (key === 'agent.enableThinking') {
agentLoop.updateConfig({ thinkingEnabled: value as boolean });
log.info(`[CONFIG] Agent thinkingEnabled updated to ${value}`);
} else if (key === 'agent.thinkingEffort') {
agentLoop.updateConfig({ thinkingEffort: value as 'low' | 'medium' | 'high' | 'max' });
log.info(`[CONFIG] Agent thinkingEffort updated to ${value}`);
} else if (key === 'ollama.numCtx') {
// numCtx 同时更新 Engine 的 contextLength
agentLoop.updateConfig({ contextLength: (value as number) || undefined });
log.info(`[CONFIG] Agent contextLength updated to ${value}`);
}
// 日志级别变更时即时应用
if (key === 'logging.level') {
const level = value as 'error' | 'warn' | 'info' | 'debug' | 'verbose' | 'silly';
log.transports.file.level = level;
log.transports.console.level = level;
log.info(`[CONFIG] Log level updated to ${level}`);
}
// 工作空间路径变更时写入独立文件(下次启动生效)
if (key === 'workspace.path') {
try {
const { writeWorkspacePathToFile } = await import('../main');
if (value) writeWorkspacePathToFile(value as string);
log.info(`[CONFIG] Workspace path saved (restart required): ${value}`);
} catch (err) {
log.error('[CONFIG] Failed to save workspace path:', err);
}
}
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('app:restart', async () => {
log.info('[APP] Restart requested');
// 延迟 200ms 让 IPC 响应先返回
setTimeout(() => {
app.relaunch();
app.exit(0);
}, 200);
return { success: true };
});
// ===== 工作空间校验与继承 =====
// 校验工作空间路径:检测路径有效性 + 4 个必需文件状态
ipcMain.handle('workspace:check', async (_event, targetPath: string) => {
if (!targetPath || typeof targetPath !== 'string') {
return { valid: false, reason: '路径不能为空' };
}
const { existsSync, statSync } = await import('fs');
const { resolve } = await import('path');
const resolvedPath = resolve(targetPath);
// 校验 1: 路径是否存在
if (!existsSync(resolvedPath)) {
return {
valid: true,
path: resolvedPath,
exists: false,
missingFiles: ['SOUL.md', 'AGENTS.md', 'MEMORY.md', 'USERS.md'],
isNewWorkspace: true,
reason: '目录不存在,将在切换后自动创建',
};
}
// 校验 2: 是否为目录
try {
const stat = statSync(resolvedPath);
if (!stat.isDirectory()) {
return { valid: false, reason: '路径不是目录' };
}
} catch {
return { valid: false, reason: '无法访问路径' };
}
// 校验 3: 检测 4 个必需文件状态
const requiredFiles = ['SOUL.md', 'AGENTS.md', 'MEMORY.md', 'USERS.md'];
const missingFiles: string[] = [];
for (const f of requiredFiles) {
if (!existsSync(join(resolvedPath, f))) missingFiles.push(f);
}
return {
valid: true,
path: resolvedPath,
exists: true,
missingFiles,
isNewWorkspace: missingFiles.length === requiredFiles.length,
};
});
// 从旧工作空间继承文件到新工作空间
ipcMain.handle('workspace:inheritFiles', async (_event, params: {
targetPath: string;
sourcePath: string;
files: string[];
}) => {
const { targetPath, sourcePath, files } = params;
if (!targetPath || !sourcePath || !Array.isArray(files)) {
return { success: false, error: '参数无效' };
}
const { existsSync, mkdirSync, copyFileSync } = await import('fs');
const { resolve } = await import('path');
const resolvedTarget = resolve(targetPath);
const resolvedSource = resolve(sourcePath);
// 确保目标目录存在
if (!existsSync(resolvedTarget)) {
mkdirSync(resolvedTarget, { recursive: true });
}
const inherited: string[] = [];
const failed: Array<{ file: string; error: string }> = [];
for (const fileName of files) {
// 仅允许继承白名单文件(防止路径遍历)
if (!['SOUL.md', 'AGENTS.md', 'USERS.md'].includes(fileName)) {
failed.push({ file: fileName, error: '不在继承白名单中' });
continue;
}
const srcFile = join(resolvedSource, fileName);
const dstFile = join(resolvedTarget, fileName);
try {
if (existsSync(srcFile)) {
copyFileSync(srcFile, dstFile);
inherited.push(fileName);
log.info(`[WORKSPACE] Inherited ${fileName}: ${resolvedSource}${resolvedTarget}`);
} else {
failed.push({ file: fileName, error: '源文件不存在' });
}
} catch (err) {
failed.push({ file: fileName, error: (err as Error).message });
}
}
return { success: true, inherited, failed };
});
// ===== 工具管理 =====
ipcMain.handle('tools:list', async () => {
return toolRegistry.listAllTools().map((t) => ({
name: t.name,
description: t.description,
category: t.category,
riskLevel: t.riskLevel,
requiresPermission: t.requiresPermission,
enabled: t.enabled,
}));
});
ipcMain.handle('tools:toggle', async (_event, toolName: string, enabled: boolean) => {
// 工具开关通过配置持久化
configService.set(`tools.${toolName}.enabled`, enabled);
// 同步到 ToolRegistry(立即生效)
toolRegistry.setToolEnabled(toolName, enabled);
// 同步到 AgentLoop 的工具列表
agentLoop.setTools(toolRegistry.listTools());
log.info(`Tool ${toolName} ${enabled ? 'enabled' : 'disabled'}`);
return { success: true };
});
// ===== 数据管理 =====
ipcMain.handle('data:export', async (_event, sessionId?: string) => {
try {
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 () => {
const db = sessionService.getDB();
try {
db.exec('BEGIN');
db.exec('DELETE FROM messages');
db.exec('DELETE FROM sessions');
db.exec('COMMIT');
log.info('[DATA] All sessions cleared');
return { success: true };
} catch (error) {
try { db.exec('ROLLBACK'); } catch { /* 忽略回滚错误 */ }
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 () => {
// 审计日志是 INSERT-ONLY,需要先禁用触发器
const db = sessionService.getDB();
try {
db.exec('BEGIN');
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
`);
db.exec('COMMIT');
log.info('[DATA] Audit logs cleared');
return { success: true };
} catch (error) {
try { db.exec('ROLLBACK'); } catch { /* 忽略回滚错误 */ }
return { success: false, error: (error as Error).message };
}
});
// ===== SearXNG =====
ipcMain.handle('searxng:testConnection', async (_event, url: string, authKey: string, authType: string) => {
try {
if (!url || !/^https?:\/\//.test(url)) {
return { success: false, error: 'URL 需以 http:// 或 https:// 开头' };
}
const startTime = Date.now();
const headers: Record<string, string> = {};
if (authKey) {
if (authType === 'bearer') {
headers['Authorization'] = `Bearer ${authKey}`;
} else if (authType === 'basic') {
headers['Authorization'] = `Basic ${Buffer.from(authKey).toString('base64')}`;
}
}
const testUrl = `${url.replace(/\/$/, '')}/search?q=test&format=json&pageno=1`;
const response = await fetch(testUrl, {
headers,
signal: AbortSignal.timeout(10_000),
});
const latencyMs = Date.now() - startTime;
if (response.ok) {
return { success: true, statusCode: response.status, latencyMs };
}
return { success: false, statusCode: response.status, latencyMs, error: `HTTP ${response.status} ${response.statusText}` };
} catch (error) {
return { success: false, error: (error as Error).message };
}
});
// ===== v0.2.0: 工具确认响应(ConfirmationDialog → 主进程)=====
// 使用 ipcMain.on(渲染进程通过 ipcRenderer.send 发送)
ipcMain.on('tool:confirmationResponse', (_event, data: { toolCallId: string; approved: boolean; remember: boolean; autoExecute?: boolean }) => {
confirmationHook.resolveConfirmation(data.toolCallId, data.approved, data.remember, data.autoExecute ?? false);
log.info(`[CONFIRM] Tool ${data.toolCallId} ${data.approved ? 'approved' : 'denied'}${data.remember ? ' (remembered)' : ''}${data.autoExecute ? ' (autoExecute)' : ''}`);
});
// ===== v0.2.0: 持久化自动执行设置 =====
ipcMain.handle('tool:setAutoExecute', async (_event, toolName: string, enabled: boolean) => {
try {
confirmationHook.setAutoExecute(toolName, enabled);
log.info(`[CONFIRM] Tool ${toolName} autoExecute set to ${enabled}`);
return { success: true };
} catch (error) {
return { success: false, error: (error as Error).message };
}
});
ipcMain.handle('tool:getAutoExecuteList', async () => {
return { success: true, data: confirmationHook.getAutoExecuteList() };
});
// ===== v0.2.0: 审计日志链式哈希验证 =====
ipcMain.handle('audit:verifyChain', async () => {
try {
const result = auditService.verifyChain();
log.info(`[AUDIT] Chain verification: ${result.valid ? 'valid' : 'TAMPERED'} (${result.verifiedRecords}/${result.totalRecords})`);
return { success: true, ...result };
} catch (error) {
log.error('[AUDIT] Chain verification failed:', error);
return { success: false, error: (error as Error).message };
}
});
ipcMain.handle('audit:query', async (_event, filters?: { sessionId?: string; eventType?: string; limit?: number }) => {
try {
return { success: true, data: auditService.query(filters as Parameters<typeof auditService.query>[0]) };
} catch (error) {
return { success: false, error: (error as Error).message };
}
});
// ===== v0.2.0: 任务管理 IPCTaskList UI=====
ipcMain.handle('tasks:list', async (_event, sessionId?: string) => {
const db = sessionService.getDB();
let sql = 'SELECT * FROM tasks';
const params: unknown[] = [];
if (sessionId) {
sql += ' WHERE session_id = ?';
params.push(sessionId);
}
sql += ' ORDER BY order_idx ASC, created_at ASC';
return { success: true, data: db.prepare(sql).all(...params) };
});
ipcMain.handle('tasks:create', async (_event, data: { sessionId: string; title: string; description?: string; priority?: string; parentId?: string }) => {
const db = sessionService.getDB();
const id = `task_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
try {
db.prepare(`
INSERT INTO tasks (id, session_id, title, description, status, priority, parent_id, created_at, updated_at)
VALUES (?, ?, ?, ?, 'pending', ?, ?, ?, ?)
`).run(id, data.sessionId, data.title, data.description ?? '', data.priority ?? 'medium', data.parentId ?? null, Date.now(), Date.now());
return { success: true, id };
} catch (error) {
return { success: false, error: (error as Error).message };
}
});
ipcMain.handle('tasks:update', async (_event, id: string, updates: { title?: string; description?: string; status?: string; priority?: string; assignedTo?: string }) => {
const db = sessionService.getDB();
try {
const fields: string[] = [];
const values: unknown[] = [];
if (updates.title !== undefined) { fields.push('title = ?'); values.push(updates.title); }
if (updates.description !== undefined) { fields.push('description = ?'); values.push(updates.description); }
if (updates.status !== undefined) { fields.push('status = ?'); values.push(updates.status); }
if (updates.priority !== undefined) { fields.push('priority = ?'); values.push(updates.priority); }
if (updates.assignedTo !== undefined) { fields.push('assigned_to = ?'); values.push(updates.assignedTo); }
if (fields.length === 0) return { success: true };
fields.push('updated_at = ?'); values.push(Date.now());
if (updates.status === 'completed') { fields.push('completed_at = ?'); values.push(Date.now()); }
values.push(id);
db.prepare(`UPDATE tasks SET ${fields.join(', ')} WHERE id = ?`).run(...values);
return { success: true };
} catch (error) {
return { success: false, error: (error as Error).message };
}
});
ipcMain.handle('tasks:delete', async (_event, id: string) => {
const db = sessionService.getDB();
try {
db.prepare('DELETE FROM tasks WHERE id = ?').run(id);
return { success: true };
} catch (error) {
return { success: false, error: (error as Error).message };
}
});
// ===== v0.2.0: 记忆系统增强查询(Memory Viewer UI=====
ipcMain.handle('memory:listAll', async (_event, options?: { type?: string; limit?: number }) => {
const db = sessionService.getDB();
const limit = options?.limit ?? 100;
const type = options?.type;
const results: Record<string, unknown[]> = {};
try {
if (!type || type === 'episodic') {
const rows = db.prepare('SELECT * FROM episodic_memories ORDER BY created_at DESC LIMIT ?').all(limit) as Array<Record<string, unknown>>;
results.episodic = rows.map((r) => ({ ...r, type: 'episodic', content: r.content ?? '' }));
}
if (!type || type === 'semantic') {
const rows = db.prepare('SELECT * FROM semantic_memories ORDER BY updated_at DESC LIMIT ?').all(limit) as Array<Record<string, unknown>>;
results.semantic = rows.map((r) => ({ ...r, type: 'semantic', content: r.value ?? r.key ?? '', importance: r.confidence ?? 0, created_at: r.created_at ?? r.updated_at }));
}
if (!type || type === 'working') {
const rows = db.prepare('SELECT * FROM working_memories ORDER BY updated_at DESC LIMIT ?').all(limit) as Array<Record<string, unknown>>;
results.working = rows.map((r) => ({ ...r, type: 'working', content: r.value ?? r.key ?? '', importance: 0.5, created_at: r.updated_at ?? Date.now() }));
}
return { success: true, data: results };
} catch (error) {
return { success: false, error: (error as Error).message };
}
});
ipcMain.handle('memory:delete', async (_event, type: string, id: string) => {
const db = sessionService.getDB();
try {
const table = type === 'episodic' ? 'episodic_memories' : type === 'semantic' ? 'semantic_memories' : 'working_memories';
db.prepare(`DELETE FROM ${table} WHERE id = ?`).run(id);
return { success: true };
} catch (error) {
return { success: false, error: (error as Error).message };
}
});
log.info('[SYS] All IPC handlers registered');
}