CI / verify (push) Canceled after 0s
安全修复: - 开启 webSecurity(CORS 改为 webRequest 允许清单精确放行 Ollama 地址) - 新增 net-guard SSRF 防护:web_fetch/download_file/browser_open 拦截环回/内网/链路本地地址(DNS 解析后校验) - browser_open 协议白名单(仅 http/https,阻止 file:// 绕过路径安全层) - git 参数注入防护(用户可控参数禁止 - 开头;git add 强制 -- 分隔) - 身份文件保护:SOUL.md/AGENT.md/USER.md 工具只读(防提示注入持久化劫持) - 系统目录硬红线 + 工作空间/白名单不可豁免系统目录 - spawn_task 权限只降不升(封顶于用户设置 subAgentMaxPermission) - 子代理写类工具接入主 Agent 确认管线 + 完整路径沙箱 - toast 改 textContent、HTML 导出 escapeHtml(XSS 修复) - Agent 浏览器改用 memory: 内存分区(退出清空 cookie/storage) 数据层重构: - sql.js 写入改防抖批量落盘(300ms 合并快照 + temp 原子替换 + 退出刷盘) - Schema 迁移改 PRAGMA user_version 顺序迁移数组 - 消息/设置/轨迹批量写(单事务);SearXNG 配置 13 次写合并为 1 次 - 会话摘要查询(getSessionSummaries/searchSessions 单条 SQL)消除 N+1 - 导出改 getAllSessionsData 一次 IPC 取回全部行 Bug 修复: - edit_file 替换符污染($&/$1 被特殊解释导致文件写坏) - truncateToolResult 暴力截断拼接非法 JSON 必然崩溃 - diff 算法 100MB dp 数组 → 前缀/后缀裁剪 + LCS 限额 + 回退 - move_file 跨盘 rename 失败回退 copy+delete - Ctrl+K 快捷键冲突(双注册);全局错误处理器双注册 - ffmpeg stderr 无限累积 + 帧进度 O(n²) 正则 - 搜索可达性预检只取响应头(Range: bytes=0-0) - 备份导出逐字节 base64 拼接(O(n²))改 FileReader - MCP clientInfo 版本硬编码 5.0.0 改真实版本;tools/list 支持 nextCursor 分页 - 看门狗默认值统一为 30 分钟;download_file 超时跟随用户配置 架构改进: - 主进程工具分发注册表 tool-dispatch.ts(消除 switch 硬编码) - agent-engine 拆分 result-formatter.ts / tool-parsing.ts(纯函数) - 文本兜底解析白名单改从注册表派生(补齐 browser_*/diff/spawn_task/mcp_*) - diff 工具默认启用;MODE_TOOLS 单一事实来源(tools-modal 复用) - 记忆系统:条目缓存 + 访问统计(hits/last)持久化 + removeById 按 ID 删除 - 度量历史启动恢复 + Metrics 仪表盘接入 JSON/Prometheus 导出 - 子代理模型下拉框打开设置时刷新(此前从未填充) 死代码清理(约 1400 行): - 删除 context-indexer 整模块、agent-safety 震荡检测/性能报告/依赖图/记忆调优/归档取回 - 删除 context-manager 水印/跳过压缩/自适应窗口/趋势分析/预算分配等未接线函数 - 删除 sanitizeToolArgs(污染 write_file 内容,防注入职责移交主进程安全层) - infra-service 裁剪为全局错误处理器唯一定义 文档对齐: - 新增内置 AGENT.md(工作空间同名文件可覆盖) - README/帮助面板/DEVELOPMENT 移除失实描述(WAL/内部URL拦截/5层防御/并行白名单/Hook 数量) - 工具数量口径统一 33;安全机制表新增 SSRF/身份保护/子代理权限等 9 项 工程化: - Vitest + 34 个单元测试(myers-diff/calculator/net-guard/MEMORY.md 格式) - Gitea Actions CI(typecheck + test + build) - package.json 新增 typecheck/test 脚本
147 lines
8.1 KiB
TypeScript
147 lines
8.1 KiB
TypeScript
/**
|
|
* Metona Ollama Desktop - Preload 脚本
|
|
*/
|
|
|
|
import { contextBridge, ipcRenderer } from 'electron';
|
|
import * as os from 'os';
|
|
|
|
contextBridge.exposeInMainWorld('metonaDesktop', {
|
|
isDesktop: true,
|
|
info: () => ipcRenderer.invoke('app:info'),
|
|
/** 读取应用内置资源(SOUL.md / AGENT.md),basename 防路径穿越 */
|
|
readAppResource: (name: string) => ipcRenderer.invoke('app:readResource', name),
|
|
/** 更新 Ollama 服务地址的 CORS 允许清单(设置面板保存地址后调用) */
|
|
setOllamaOrigin: (url: string) => ipcRenderer.invoke('app:setOllamaOrigin', url),
|
|
sys: {
|
|
homeDir: os.homedir(),
|
|
tmpDir: os.tmpdir(),
|
|
shell: process.env.SHELL || process.env.ComSpec || (process.platform === 'win32' ? 'cmd.exe' : 'bash'),
|
|
arch: os.arch(),
|
|
platform: os.platform(),
|
|
hostname: os.hostname(),
|
|
username: os.userInfo().username,
|
|
},
|
|
dialog: {
|
|
openFile: (options?: unknown) => ipcRenderer.invoke('dialog:openFile', options),
|
|
saveFile: (options?: unknown) => ipcRenderer.invoke('dialog:saveFile', options),
|
|
openDirectory: () => ipcRenderer.invoke('dialog:openDirectory')
|
|
},
|
|
fs: {
|
|
readFile: (filePath: string) => ipcRenderer.invoke('fs:readFile', filePath),
|
|
readFileBase64: (filePath: string) => ipcRenderer.invoke('fs:readFileBase64', filePath),
|
|
writeFile: (filePath: string, content: string, encoding?: string) => ipcRenderer.invoke('fs:writeFile', filePath, content, encoding)
|
|
},
|
|
tool: {
|
|
execute: (toolName: string, args: Record<string, unknown>) => ipcRenderer.invoke('tool:execute', toolName, args),
|
|
getConfig: () => ipcRenderer.invoke('tool:getConfig'),
|
|
setAllowedDirs: (dirs: string[]) => ipcRenderer.invoke('tool:setAllowedDirs', dirs),
|
|
setTimeouts: (timeouts: { http?: number; mcp?: number }) => ipcRenderer.invoke('tool:setTimeouts', timeouts)
|
|
},
|
|
notify: (title: string, body: string) => ipcRenderer.invoke('notify', title, body),
|
|
window: {
|
|
minimize: () => ipcRenderer.invoke('window:minimize'),
|
|
maximize: () => ipcRenderer.invoke('window:maximize'),
|
|
close: () => ipcRenderer.invoke('window:close')
|
|
},
|
|
openExternal: (url: string) => ipcRenderer.invoke('shell:openExternal', url),
|
|
onMenuAction: (callback: (action: string) => void) => {
|
|
ipcRenderer.on('menu-action', (_: unknown, action: string) => callback(action));
|
|
},
|
|
onTrayAction: (callback: (action: string) => void) => {
|
|
ipcRenderer.on('tray-action', (_: unknown, action: string) => callback(action));
|
|
},
|
|
onAppQuit: (callback: () => void) => {
|
|
ipcRenderer.on('app-quit', () => callback());
|
|
},
|
|
removeAllListeners: (channel: string) => {
|
|
ipcRenderer.removeAllListeners(channel);
|
|
},
|
|
onMainLog: (callback: (data: { level: string; message: string; detail?: string }) => void) => {
|
|
ipcRenderer.on('main:log', (_: unknown, data: { level: string; message: string; detail?: string }) => callback(data));
|
|
},
|
|
db: {
|
|
saveSession: (session: unknown) => ipcRenderer.invoke('db:saveSession', session),
|
|
getSession: (id: string) => ipcRenderer.invoke('db:getSession', id),
|
|
getAllSessions: () => ipcRenderer.invoke('db:getAllSessions'),
|
|
/** 会话摘要列表(单条 SQL,历史列表/搜索不再全量加载消息) */
|
|
getSessionSummaries: () => ipcRenderer.invoke('db:getSessionSummaries'),
|
|
/** 按标题或消息内容搜索会话 */
|
|
searchSessions: (query: string) => ipcRenderer.invoke('db:searchSessions', query),
|
|
/** 全量会话+消息行(导出用,一次 IPC 取代 N+1 往返) */
|
|
getAllSessionsData: () => ipcRenderer.invoke('db:getAllSessionsData'),
|
|
deleteSession: (id: string) => ipcRenderer.invoke('db:deleteSession', id),
|
|
clearAllSessions: () => ipcRenderer.invoke('db:clearAllSessions'),
|
|
saveMessage: (msg: unknown) => ipcRenderer.invoke('db:saveMessage', msg),
|
|
saveMessagesBatch: (msgs: unknown[]) => ipcRenderer.invoke('db:saveMessagesBatch', msgs),
|
|
getMessages: (sessionId: string) => ipcRenderer.invoke('db:getMessages', sessionId),
|
|
saveSetting: (key: string, value: unknown) => ipcRenderer.invoke('db:saveSetting', key, value),
|
|
/** 批量保存设置(单事务 + 单次刷盘) */
|
|
saveSettingsBatch: (entries: Array<{ key: string; value: unknown }>) => ipcRenderer.invoke('db:saveSettingsBatch', entries),
|
|
getSetting: (key: string, defaultValue?: unknown) => ipcRenderer.invoke('db:getSetting', key, defaultValue),
|
|
saveTrace: (trace: unknown) => ipcRenderer.invoke('db:saveTrace', trace),
|
|
saveTracesBatch: (traces: unknown[]) => ipcRenderer.invoke('db:saveTracesBatch', traces),
|
|
getTraces: (sessionId: string) => ipcRenderer.invoke('db:getTraces', sessionId),
|
|
exportSessions: () => ipcRenderer.invoke('db:exportSessions'),
|
|
importSessions: (data: unknown) => ipcRenderer.invoke('db:importSessions', data),
|
|
|
|
getAllTokenStats: () => ipcRenderer.invoke('db:getAllTokenStats'),
|
|
saveToolAudit: (audit: unknown) => ipcRenderer.invoke('db:saveToolAudit', audit),
|
|
getToolAudits: (sessionId: string) => ipcRenderer.invoke('db:getToolAudits', sessionId),
|
|
getAllToolAudits: (limit?: number) => ipcRenderer.invoke('db:getAllToolAudits', limit),
|
|
},
|
|
workspace: {
|
|
getDir: () => ipcRenderer.invoke('workspace:getDir'),
|
|
setDir: (dir: string) => ipcRenderer.invoke('workspace:setDir', dir),
|
|
listDir: (dirPath?: string) => ipcRenderer.invoke('workspace:listDir', dirPath),
|
|
readFile: (filePath: string) => ipcRenderer.invoke('workspace:readFile', filePath),
|
|
exec: (params: { id: string; command: string; cwd?: string }) => {
|
|
ipcRenderer.send('workspace:exec', params);
|
|
},
|
|
kill: (id: string) => {
|
|
ipcRenderer.send('workspace:kill', id);
|
|
},
|
|
onOutput: (callback: (data: { id: string; type: 'stdout' | 'stderr'; data: string }) => void) => {
|
|
ipcRenderer.on('workspace:output', (_: unknown, data: { id: string; type: 'stdout' | 'stderr'; data: string }) => callback(data));
|
|
},
|
|
onExit: (callback: (data: { id: string; code: number | null }) => void) => {
|
|
ipcRenderer.on('workspace:exit', (_: unknown, data: { id: string; code: number | null }) => callback(data));
|
|
},
|
|
/** 无超时工具命令执行,用于 AI Tool Calling */
|
|
execTool: (command: string, cwd?: string) => ipcRenderer.invoke('tool:execute', 'run_command', { command, cwd }),
|
|
/** AI 命令实时输出推送到工作空间终端 */
|
|
onCmdOutput: (callback: (data: { command: string; type: 'stdout' | 'stderr'; data: string }) => void) => {
|
|
ipcRenderer.on('cmd:output', (_: unknown, data: { command: string; type: 'stdout' | 'stderr'; data: string }) => callback(data));
|
|
},
|
|
/** AI 命令执行完毕 */
|
|
onCmdDone: (callback: (data: { command: string; exitCode: number | null }) => void) => {
|
|
ipcRenderer.on('cmd:done', (_: unknown, data: { command: string; exitCode: number | null }) => callback(data));
|
|
},
|
|
/** 终止当前 AI 命令 */
|
|
cmdKill: () => ipcRenderer.invoke('cmd:kill')
|
|
},
|
|
mcp: {
|
|
startServer: (config: { name: string; command: string; args: string[]; enabled: boolean; description?: string }) => ipcRenderer.invoke('mcp:startServer', config),
|
|
stopServer: (name: string) => ipcRenderer.invoke('mcp:stopServer', name),
|
|
stopAll: () => ipcRenderer.invoke('mcp:stopAll'),
|
|
callTool: (serverName: string, toolName: string, args: Record<string, unknown>) => ipcRenderer.invoke('mcp:callTool', serverName, toolName, args),
|
|
getTools: () => ipcRenderer.invoke('mcp:getTools'),
|
|
getStatuses: () => ipcRenderer.invoke('mcp:getStatuses'),
|
|
refreshTools: (name: string) => ipcRenderer.invoke('mcp:refreshTools', name)
|
|
},
|
|
video: {
|
|
extractFrames: (filePath: string, options?: { maxFrames?: number; maxWidth?: number; maxSize?: number }) =>
|
|
ipcRenderer.invoke('video:extractFrames', filePath, options),
|
|
onProgress: (callback: (data: { current: number }) => void) => {
|
|
ipcRenderer.on('video:progress', (_: unknown, data: { current: number }) => callback(data));
|
|
},
|
|
removeProgressListener: () => {
|
|
ipcRenderer.removeAllListeners('video:progress');
|
|
}
|
|
},
|
|
memoryAccess: {
|
|
read: () => ipcRenderer.invoke('memory:read'),
|
|
write: (content: string) => ipcRenderer.invoke('memory:write', content),
|
|
init: () => ipcRenderer.invoke('memory:init'),
|
|
}
|
|
});
|