feat: v0.4.1 质量加固版 — 工程化基线 + 安全加固 + 测试补齐 + 体验升级
工程化(从零到一): - 新增 Gitea Actions CI(debian-latest):类型检查 + Lint + 单元测试 + 产物编译验证 - 新增 husky + lint-staged 预提交钩子(lint-staged + typecheck 门禁) - 移除坏脚本 test:e2e(无 Playwright 配置必失败);prebuild 改用内置 fs.rmSync - 依赖清理:移除死依赖 sql.js(2MB)/@playwright/test,@types/shell-quote 移至 devDependencies 安全加固: - PolicyEngine 频率限制按会话隔离(多会话并发不再互抢配额) - ConfirmationHook 拒绝记忆加 10 分钟 TTL + 恢复询问入口(新增 2 个 IPC 通道) - Windows run_command 白名单工具(git/node/npm/npx/pnpm/yarn/tsc)改走 cmd.exe /c + 参数数组执行,收窄 shell 注入面 - web_search 四引擎 HTML 解析迁移 node-html-parser(结构化主层 + 正则降级) 缺陷修复(测试驱动发现): - mapError 大小写缺陷:网络错误码永远落入 UNKNOWN 无法触发重试 - 搜狗解析器自我过滤:相对链接补全后又被 sogou.com 过滤导致结果全丢 - 百度复合类名重复收录:class="result c-container" 被双重匹配 测试补齐(113 → 194 用例): - 新增 5 个测试文件:sse-stream / base-adapter / confirmation-hook / ipc-agent 编排链路 / web-search 解析器 - 覆盖 sendMessage 全分支、SSE 流解析、错误映射、确认钩子竞态/超时/批量审批 体验升级: - OutputValidator 验证结果可见化(VALIDATION 流事件 → 聊天流提示卡) - SettingsModal 巨型组件拆分(1503 行 → 10 个文件,可独立维护) - MessageList 接入 react-virtuoso 真虚拟滚动(千条消息恒定开销) - MCP 新增 streamable HTTP 传输支持(SDK 内置传输 + DB 迁移 6 + UI 双模式)
This commit is contained in:
+65
-20
@@ -15,7 +15,8 @@ const metonaAPI = {
|
||||
// ===== Agent 交互 =====
|
||||
agent: {
|
||||
/** 发送用户消息 */
|
||||
sendMessage: (message: unknown, sessionId: string) => ipcRenderer.invoke('agent:sendMessage', message, sessionId),
|
||||
sendMessage: (message: unknown, sessionId: string) =>
|
||||
ipcRenderer.invoke('agent:sendMessage', message, sessionId),
|
||||
/** 监听流式事件 */
|
||||
onStreamEvent: (callback: (event: unknown) => void) => {
|
||||
const listener = (_event: Electron.IpcRendererEvent, data: unknown) => callback(data);
|
||||
@@ -42,17 +43,21 @@ const metonaAPI = {
|
||||
sessions: {
|
||||
list: () => ipcRenderer.invoke('sessions:list'),
|
||||
create: (title?: string) => ipcRenderer.invoke('sessions:create', title),
|
||||
rename: (sessionId: string, title: string) => ipcRenderer.invoke('sessions:rename', sessionId, title),
|
||||
rename: (sessionId: string, title: string) =>
|
||||
ipcRenderer.invoke('sessions:rename', sessionId, title),
|
||||
delete: (sessionId: string) => ipcRenderer.invoke('sessions:delete', sessionId),
|
||||
getMessages: (sessionId: string) => ipcRenderer.invoke('sessions:getMessages', sessionId),
|
||||
pin: (sessionId: string, pinned: boolean) => ipcRenderer.invoke('sessions:pin', sessionId, pinned),
|
||||
archive: (sessionId: string, archived: boolean) => ipcRenderer.invoke('sessions:archive', sessionId, archived),
|
||||
pin: (sessionId: string, pinned: boolean) =>
|
||||
ipcRenderer.invoke('sessions:pin', sessionId, pinned),
|
||||
archive: (sessionId: string, archived: boolean) =>
|
||||
ipcRenderer.invoke('sessions:archive', sessionId, archived),
|
||||
deleteMessage: (messageId: string) => ipcRenderer.invoke('sessions:deleteMessage', messageId),
|
||||
clearMessages: (sessionId: string) => ipcRenderer.invoke('sessions:clearMessages', sessionId),
|
||||
/** P2-11: 截断消息(编辑重发/重新生成——删除锚点消息之后的所有消息) */
|
||||
truncateAfter: (sessionId: string, messageId: string, inclusive?: boolean) =>
|
||||
ipcRenderer.invoke('sessions:truncateAfter', sessionId, messageId, inclusive),
|
||||
saveTrace: (sessionId: string, data: unknown) => ipcRenderer.invoke('sessions:saveTrace', sessionId, data),
|
||||
saveTrace: (sessionId: string, data: unknown) =>
|
||||
ipcRenderer.invoke('sessions:saveTrace', sessionId, data),
|
||||
getTrace: (sessionId: string) => ipcRenderer.invoke('sessions:getTrace', sessionId),
|
||||
},
|
||||
|
||||
@@ -61,12 +66,14 @@ const metonaAPI = {
|
||||
listServers: () => ipcRenderer.invoke('mcp:listServers'),
|
||||
addServer: (config: unknown) => ipcRenderer.invoke('mcp:addServer', config),
|
||||
removeServer: (name: string) => ipcRenderer.invoke('mcp:removeServer', name),
|
||||
toggleServer: (name: string, enabled: boolean) => ipcRenderer.invoke('mcp:toggleServer', name, enabled),
|
||||
toggleServer: (name: string, enabled: boolean) =>
|
||||
ipcRenderer.invoke('mcp:toggleServer', name, enabled),
|
||||
},
|
||||
|
||||
// ===== 记忆系统 =====
|
||||
memory: {
|
||||
search: (query: string, options?: unknown) => ipcRenderer.invoke('db:searchMemories', query, options),
|
||||
search: (query: string, options?: unknown) =>
|
||||
ipcRenderer.invoke('db:searchMemories', query, options),
|
||||
listAll: (options?: { type?: string; limit?: number }) =>
|
||||
ipcRenderer.invoke('memory:listAll', options),
|
||||
delete: (type: string, id: string) => ipcRenderer.invoke('memory:delete', type, id),
|
||||
@@ -75,10 +82,24 @@ const metonaAPI = {
|
||||
// ===== v0.2.0: 任务管理 =====
|
||||
tasks: {
|
||||
list: (sessionId?: string) => ipcRenderer.invoke('tasks:list', sessionId),
|
||||
create: (data: { sessionId: string; title: string; description?: string; priority?: string; parentId?: string }) =>
|
||||
ipcRenderer.invoke('tasks:create', data),
|
||||
update: (id: string, updates: { title?: string; description?: string; status?: string; priority?: string; assignedTo?: string }, sessionId: string) =>
|
||||
ipcRenderer.invoke('tasks:update', id, updates, sessionId),
|
||||
create: (data: {
|
||||
sessionId: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
priority?: string;
|
||||
parentId?: string;
|
||||
}) => ipcRenderer.invoke('tasks:create', data),
|
||||
update: (
|
||||
id: string,
|
||||
updates: {
|
||||
title?: string;
|
||||
description?: string;
|
||||
status?: string;
|
||||
priority?: string;
|
||||
assignedTo?: string;
|
||||
},
|
||||
sessionId: string,
|
||||
) => ipcRenderer.invoke('tasks:update', id, updates, sessionId),
|
||||
delete: (id: string, sessionId: string) => ipcRenderer.invoke('tasks:delete', id, sessionId),
|
||||
// P2(v0.3.13): 订阅任务变更事件(Agent 通过 task_manager 写入后触发)
|
||||
onTaskChanged: (callback: (sessionId: string) => void) => {
|
||||
@@ -102,11 +123,19 @@ const metonaAPI = {
|
||||
ipcRenderer.on('tool:confirmationRequest', listener);
|
||||
return () => ipcRenderer.removeListener('tool:confirmationRequest', listener);
|
||||
},
|
||||
sendConfirmationResponse: (response: { toolCallId: string; approved: boolean; remember: boolean; autoExecute?: boolean }) =>
|
||||
ipcRenderer.send('tool:confirmationResponse', response),
|
||||
sendConfirmationResponse: (response: {
|
||||
toolCallId: string;
|
||||
approved: boolean;
|
||||
remember: boolean;
|
||||
autoExecute?: boolean;
|
||||
}) => ipcRenderer.send('tool:confirmationResponse', response),
|
||||
// v0.3.2: 批量确认响应(并行工具一次性审批)
|
||||
sendConfirmationResponseBatch: (response: { toolCallIds: string[]; approved: boolean; remember: boolean; autoExecute?: boolean }) =>
|
||||
ipcRenderer.send('tool:confirmationResponseBatch', response),
|
||||
sendConfirmationResponseBatch: (response: {
|
||||
toolCallIds: string[];
|
||||
approved: boolean;
|
||||
remember: boolean;
|
||||
autoExecute?: boolean;
|
||||
}) => ipcRenderer.send('tool:confirmationResponseBatch', response),
|
||||
// v0.3.2: 拉取当前所有 pending 确认(前端弹框打开时调用,防止 state 覆盖丢失)
|
||||
getPendingConfirmations: () =>
|
||||
ipcRenderer.invoke('tool:getPendingConfirmations') as Promise<{
|
||||
@@ -120,6 +149,17 @@ const metonaAPI = {
|
||||
expiresAt?: number;
|
||||
}>;
|
||||
}>,
|
||||
// v0.4.1: 会话内拒绝记忆管理(拒绝记忆 10 分钟 TTL,支持手动恢复询问)
|
||||
getRememberedDenials: () =>
|
||||
ipcRenderer.invoke('tool:getRememberedDenials') as Promise<{
|
||||
success: boolean;
|
||||
data: Array<{ toolName: string; expiresInSeconds: number }>;
|
||||
}>,
|
||||
resetRememberedDenial: (toolName: string) =>
|
||||
ipcRenderer.invoke('tool:resetRememberedDenial', toolName) as Promise<{
|
||||
success: boolean;
|
||||
error?: string;
|
||||
}>,
|
||||
// v0.2.0: 持久化自动执行设置
|
||||
setAutoExecute: (toolName: string, enabled: boolean) =>
|
||||
ipcRenderer.invoke('tool:setAutoExecute', toolName, enabled),
|
||||
@@ -169,15 +209,18 @@ const metonaAPI = {
|
||||
// ===== 工具管理 =====
|
||||
tools: {
|
||||
list: () => ipcRenderer.invoke('tools:list'),
|
||||
toggle: (toolName: string, enabled: boolean) => ipcRenderer.invoke('tools:toggle', toolName, enabled),
|
||||
toggle: (toolName: string, enabled: boolean) =>
|
||||
ipcRenderer.invoke('tools:toggle', toolName, enabled),
|
||||
/** v0.3.18 修复: 监听工具就绪事件(MCP 初始化完成后触发) */
|
||||
onReady: (callback: (data: { toolCount: number }) => void) => {
|
||||
const listener = (_event: Electron.IpcRendererEvent, data: { toolCount: number }) => callback(data);
|
||||
const listener = (_event: Electron.IpcRendererEvent, data: { toolCount: number }) =>
|
||||
callback(data);
|
||||
ipcRenderer.on('tools:ready', listener);
|
||||
return () => ipcRenderer.removeListener('tools:ready', listener);
|
||||
},
|
||||
/** v0.3.18 修复: 查询工具当前是否已就绪(解决事件竞态,前端注册监听器后立即查询一次) */
|
||||
isReady: () => ipcRenderer.invoke('tools:isReady') as Promise<{ ready: boolean; toolCount: number }>,
|
||||
isReady: () =>
|
||||
ipcRenderer.invoke('tools:isReady') as Promise<{ ready: boolean; toolCount: number }>,
|
||||
},
|
||||
|
||||
// ===== 数据管理 =====
|
||||
@@ -197,8 +240,10 @@ const metonaAPI = {
|
||||
// ===== Toast 通知桥接 =====
|
||||
toast: {
|
||||
onShow: (callback: (data: { type: string; message: string; options?: unknown }) => void) => {
|
||||
const listener = (_event: Electron.IpcRendererEvent, data: { type: string; message: string; options?: unknown }) =>
|
||||
callback(data);
|
||||
const listener = (
|
||||
_event: Electron.IpcRendererEvent,
|
||||
data: { type: string; message: string; options?: unknown },
|
||||
) => callback(data);
|
||||
ipcRenderer.on('toast:show', listener);
|
||||
return () => ipcRenderer.removeListener('toast:show', listener);
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user