feat: 升级至 v0.2.1 — 流式渲染修复、安全增强、工具自动执行
流式渲染修复: - runId 机制防止 abort 后旧流事件污染新 run - run lock 防止并发 run 污染引擎状态 - abort race 提前退出工具执行等待 - TERMINATED 状态通过 stateChange 发射 - tool_call_delta 流式参数拼接 + pending 占位替换 - 首轮卡片创建路径统一,traceStep 按 ID 精确匹配 - compressed 事件转发为 toast 通知 安全增强: - ConfirmationHook 支持持久化自动执行(跨会话) - 设置面板新增自动执行工具管理 UI - SandboxManager 双重安全校验 fail-closed - 审计日志链式哈希防篡改 - PromptInjectionDefender 中文注入标记清理 - scanCode 28 模式 + base64/$() 检测 - validatePath realpathSync 防符号链接逃逸 - code-search 使用 execFile 防命令注入 新增工具: - file_editor、code_search、task_manager、diff_viewer 其他: - Agent Loop 加 PARSING/REFLECTING 状态 + 指数退避重试 - MemoryManager TF-IDF 语义检索 - run_command Windows 中文编码修复(chcp 65001) - 版本号 0.2.0 → 0.2.1
This commit is contained in:
+173
-1
@@ -22,6 +22,8 @@ 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';
|
||||
@@ -45,6 +47,8 @@ export function registerAllIPCHandlers(
|
||||
reloadAdapter: () => void,
|
||||
promptInjectionDefender: PromptInjectionDefender,
|
||||
outputValidator: OutputValidator,
|
||||
confirmationHook: ConfirmationHook,
|
||||
memoryConsolidator: MemoryConsolidator,
|
||||
): void {
|
||||
|
||||
// ===== Agent 交互 =====
|
||||
@@ -112,7 +116,7 @@ export function registerAllIPCHandlers(
|
||||
}
|
||||
};
|
||||
|
||||
const onStateChange = (data: { previous?: string; current?: string; sessionId?: string; iteration?: number; state?: string }) => {
|
||||
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}`);
|
||||
// 转发到渲染进程(携带迭代号)
|
||||
@@ -121,8 +125,19 @@ export function registerAllIPCHandlers(
|
||||
}
|
||||
};
|
||||
|
||||
// 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 {
|
||||
// 提示注入检测(安全模块)
|
||||
@@ -218,6 +233,26 @@ export function registerAllIPCHandlers(
|
||||
// 更新 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,
|
||||
@@ -282,6 +317,7 @@ export function registerAllIPCHandlers(
|
||||
} finally {
|
||||
agentLoop.off('streamEvent', onStreamEvent);
|
||||
agentLoop.off('stateChange', onStateChange);
|
||||
agentLoop.off('compressed', onCompressed);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -628,5 +664,141 @@ export function registerAllIPCHandlers(
|
||||
}
|
||||
});
|
||||
|
||||
// ===== 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: 任务管理 IPC(TaskList 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');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user