Files
metona-ai-desktop/electron/preload.ts
T
thzxx 3e5ddea72d feat: v0.3.17 修复多项问题(sandbox/全局配置层/继承选项/配置刷新/附件提示/content_filter 错误处理)
- fix: sandbox: true → false(ESM preload 不兼容 sandbox 导致 window.metona 不可用,选择文件夹按钮无反应)
- feat: 全局配置层(GlobalConfigService)— LLM/Agent 等配置跨工作空间共享,切换工作空间后不再丢失
  - 读取时工作空间 DB 空值或默认值回退到全局 JSON
  - 写入时双写(DB + 全局 JSON)
  - 首次启动迁移旧配置到全局层
- fix: 工作空间继承选项显示条件放宽 + push 前校验避免覆盖目标已有文件(SOUL.md/agent.db)
- fix: config:changed IPC 广播 + 前端监听,修改 Agent 配置后详情栏 maxIterations 实时刷新
- feat: 上传图片/文件时在 System Prompt 注入附件提示,避免 AI 在工作空间查找用户上传的文件
- feat: content_filter 错误识别(ContentFilterError)+ 友好提示,替代原始 JSON 错误体透传
- fix: JSDoc 注释中 llm.* / agent.* / onboarding.completed 中的 */ 被解析为注释结束符
2026-07-22 13:12:15 +08:00

225 lines
11 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.
/**
* MetonaAI Desktop — Preload 脚本
*
* 通过 contextBridge 安全地将 IPC 通道暴露给渲染进程。
* 渲染进程无 Node.js 访问权限,只能通过此桥接层通信。
*/
import { contextBridge, ipcRenderer } from 'electron';
import { electronAPI } from '@electron-toolkit/preload';
/**
* Metona API — 暴露给渲染进程的安全接口
*/
const metonaAPI = {
// ===== Agent 交互 =====
agent: {
/** 发送用户消息 */
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);
ipcRenderer.on('agent:streamEvent', listener);
return () => ipcRenderer.removeListener('agent:streamEvent', listener);
},
/** 监听状态变化 */
onStateChange: (callback: (state: unknown) => void) => {
const listener = (_event: Electron.IpcRendererEvent, data: unknown) => callback(data);
ipcRenderer.on('agent:stateChange', listener);
return () => ipcRenderer.removeListener('agent:stateChange', listener);
},
/** 中断会话 */
abortSession: (sessionId: string) => ipcRenderer.invoke('agent:abortSession', sessionId),
/** 监听 Provider 切换通知 */
onProviderSwitched: (callback: (data: unknown) => void) => {
const listener = (_event: Electron.IpcRendererEvent, data: unknown) => callback(data);
ipcRenderer.on('agent:providerSwitched', listener);
return () => ipcRenderer.removeListener('agent:providerSwitched', listener);
},
},
// ===== 会话管理 =====
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),
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),
deleteMessage: (messageId: string) => ipcRenderer.invoke('sessions:deleteMessage', messageId),
clearMessages: (sessionId: string) => ipcRenderer.invoke('sessions:clearMessages', sessionId),
saveTrace: (sessionId: string, data: unknown) => ipcRenderer.invoke('sessions:saveTrace', sessionId, data),
getTrace: (sessionId: string) => ipcRenderer.invoke('sessions:getTrace', sessionId),
},
// ===== MCP 管理 =====
mcp: {
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),
},
// ===== 记忆系统 =====
memory: {
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),
},
// ===== 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),
delete: (id: string, sessionId: string) => ipcRenderer.invoke('tasks:delete', id, sessionId),
// P2v0.3.13: 订阅任务变更事件(Agent 通过 task_manager 写入后触发)
onTaskChanged: (callback: (sessionId: string) => void) => {
const listener = (_event: unknown, sessionId: string): void => callback(sessionId);
ipcRenderer.on('task:changed', listener);
return () => ipcRenderer.removeListener('task:changed', listener);
},
},
// ===== v0.2.0: 审计日志 =====
audit: {
verifyChain: () => ipcRenderer.invoke('audit:verifyChain'),
query: (filters?: { sessionId?: string; eventType?: string; limit?: number }) =>
ipcRenderer.invoke('audit:query', filters),
},
// ===== v0.2.0: 工具确认请求 =====
tool: {
onConfirmationRequest: (callback: (request: unknown) => void) => {
const listener = (_event: Electron.IpcRendererEvent, data: unknown) => callback(data);
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),
// v0.3.2: 批量确认响应(并行工具一次性审批)
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<{
success: boolean;
data: Array<{
toolCallId: string;
toolName: string;
args: Record<string, unknown>;
riskLevel: string;
reason: string;
expiresAt?: number;
}>;
}>,
// v0.2.0: 持久化自动执行设置
setAutoExecute: (toolName: string, enabled: boolean) =>
ipcRenderer.invoke('tool:setAutoExecute', toolName, enabled),
getAutoExecuteList: () => ipcRenderer.invoke('tool:getAutoExecuteList'),
},
// ===== 配置 =====
config: {
get: (key: string) => ipcRenderer.invoke('config:get', key),
set: (key: string, value: unknown) => ipcRenderer.invoke('config:set', key, value),
// v0.3.9: 批量保存配置,避免串行保存中间态触发 reloadAdapter 失败
setBatch: (entries: Array<{ key: string; value: unknown }>) =>
ipcRenderer.invoke('config:setBatch', entries),
// v0.3.17: 监听配置变更广播(后端 config:set/setBatch 后触发,用于前端 store 实时更新)
onChanged: (callback: (data: { key: string; value: unknown }) => void) => {
const listener = (_event: Electron.IpcRendererEvent, data: { key: string; value: unknown }) =>
callback(data);
ipcRenderer.on('config:changed', listener);
return () => ipcRenderer.removeListener('config:changed', listener);
},
},
// ===== 应用工具 =====
app: {
getVersion: () => ipcRenderer.invoke('app:getVersion'),
getAppDataPath: () => ipcRenderer.invoke('app:getAppDataPath'),
openExternal: (url: string) => ipcRenderer.invoke('app:openExternal', url),
showItemInFolder: (path: string) => ipcRenderer.invoke('app:showItemInFolder', path),
selectFolder: (defaultPath?: string) => ipcRenderer.invoke('app:selectFolder', defaultPath),
restart: () => ipcRenderer.invoke('app:restart'),
// #49 修复: 前端错误上报通道(主进程可注册 'error:report' handler 记录到 electron-log/审计日志)
// 使用 send(单向)而非 invoke,即使主进程未注册 handler 也不会 reject
reportError: (payload: unknown) => ipcRenderer.send('error:report', payload),
},
// ===== 工作空间管理 =====
workspace: {
check: (targetPath: string) => ipcRenderer.invoke('workspace:check', targetPath),
inheritFiles: (params: { targetPath: string; sourcePath: string; files: string[] }) =>
ipcRenderer.invoke('workspace:inheritFiles', params),
getInfo: () => ipcRenderer.invoke('workspace:getInfo'),
// #51 修复: 校验源数据库完整性(PRAGMA integrity_check),用于 inheritDatabase 前置校验
checkDatabaseIntegrity: (sourcePath: string) =>
ipcRenderer.invoke('workspace:checkDatabaseIntegrity', sourcePath),
},
// ===== 工具管理 =====
tools: {
list: () => ipcRenderer.invoke('tools:list'),
toggle: (toolName: string, enabled: boolean) => ipcRenderer.invoke('tools:toggle', toolName, enabled),
},
// ===== 数据管理 =====
data: {
exportData: (sessionId?: string) => ipcRenderer.invoke('data:export', sessionId),
clearSessions: () => ipcRenderer.invoke('data:clearSessions'),
clearMemories: () => ipcRenderer.invoke('data:clearMemories'),
clearAuditLogs: () => ipcRenderer.invoke('data:clearAuditLogs'),
},
// ===== SearXNG =====
searxng: {
testConnection: (url: string, authKey: string, authType: string) =>
ipcRenderer.invoke('searxng:testConnection', url, authKey, authType),
},
// ===== 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);
ipcRenderer.on('toast:show', listener);
return () => ipcRenderer.removeListener('toast:show', listener);
},
},
};
// ===== 安全暴露到渲染进程 =====
if (process.contextIsolated) {
try {
contextBridge.exposeInMainWorld('electron', electronAPI);
contextBridge.exposeInMainWorld('metona', metonaAPI);
} catch (error) {
console.error('Failed to expose APIs via contextBridge:', error);
// #7 修复: 生产环境强制失败,防止以不安全状态启动(contextBridge 失败意味着
// context isolation 边界已破坏,继续运行可能导致渲染进程直接访问 Node API)
// 开发环境降级到 globalThis 以便调试
// 审查修复: process.defaultApp 在 sandbox: true 下可能为 undefined
// 导致开发环境也无法降级。补充 process.env.NODE_ENV 判断(sandbox 下 process.env 仍可用)
if (process.env.NODE_ENV === 'development' || process.defaultApp) {
console.warn('[DEV] Falling back to globalThis assignment — context isolation is weakened');
(globalThis as unknown as Record<string, unknown>).electron = electronAPI;
(globalThis as unknown as Record<string, unknown>).metona = metonaAPI;
} else {
// 审查修复 M19: 在 throw 前记录详细错误,便于在 DevTools 中排查白屏问题。
// 生产环境 contextBridge 失败时用户只看到白屏,console.error 至少能在 DevTools 中留下线索。
console.error('[FATAL] contextBridge failed in production:', error);
throw error;
}
}
} else {
// context isolation 未启用(不推荐,仅开发环境)
(globalThis as unknown as Record<string, unknown>).electron = electronAPI;
(globalThis as unknown as Record<string, unknown>).metona = metonaAPI;
}