【上下文压缩机制修复】 - engine.ts 新增 lastRealInputTokens 记录 LLM 返回的真实输入 token,压缩判断取 max(估算值, 真实值),避免估算偏低导致不压缩但 API 413 - 修复 effectiveContextWindow 缺少默认值导致 compressionThreshold 变 NaN、压缩永不触发的 bug(添加 ?? 128_000 兜底) - compressMessages 保留区从固定 10 条改为按 token 预算动态截断(50% 上下文窗口) - 二次截断 charsPerToken 从 2 调整为 1.0,与 CJK_TOKEN_RATIO 一致 - 压缩后重置 lastRealInputTokens,避免跨迭代污染 - 每个 run 开始时重置 lastRealInputTokens 【前端 Token 显示修复】 - 区分"累计消耗"和"上下文占用"语义——之前 totalTokens(累计) / contextWindow(单次窗口) 得出无意义百分比 - TokenUsage.tsx 上下文占用改用 lastInputTokens,新增压缩节省行(绿色,仅当 > 0 时显示) - agent-store.ts TokenUsage 接口新增 lastInputTokens 和 lastCompressedSaved 字段,4 处初始值统一更新 - useAgentStream.ts usage 事件 lastInputTokens 替换不累加,compressed 事件通过 streamEvent 接收 savedTokens - handlers.ts onCompressed 同时发 toast + streamEvent,解决"压缩触发但前端 token 显示不降"缺陷 - 旧数据兼容使用 ?? 0,保证历史会话加载不崩溃 【MCP 工具就绪竞态修复】 - 修复输入框永久显示"工具加载中"的竞态条件:MCP initialize 几乎立即 resolve(connectServer 不 await),tools:ready 事件在前端监听器注册前已发出 - main.ts 维护 toolsReady 标志 + 注册 tools:isReady IPC handler 查询当前状态 - preload.ts 暴露 tools.isReady() 方法 - App.tsx 注册 onReady 监听器后立即查询 isReady(),无论事件是否错过都能恢复正确状态 【记忆系统加固】 - consolidator.ts 新增 runningPromise + waitForCompletion(35s),before-quit 等待固化完成,防止退出时异步 consolidate 数据丢失 - 固化到 MEMORY.md 的同时写入 semantic_memories 表,解决双轨存储无交叉验证问题 - manager.ts tokenize 按中英文标点切分子句后再做 bigram,优化中文分词 - 清理正则冗余括号 【SOUL.md 降级处理】 - context-builder.ts SOUL.md 为空或不存在时降级到默认身份,向前端发 toast 提示用户 - 新增 fallbackRoleNotified 去重标志,仅首次降级通知,避免每次发消息都弹 toast - SOUL.md 恢复内容时重置标志 【Token 估算调整】 - token-estimator.ts CJK_TOKEN_RATIO 从 1.5 调整为 1.0 【MCP 异步初始化】 - main.ts MCP 完成后广播 tools:ready 事件,前端 UI 据以控制输入框可用性 - preload.ts + global.d.ts 暴露 tools.onReady() 监听器 - App.tsx + ChatInput.tsx toolsReady 状态控制输入框 【版本号】 - package.json + package-lock.json 从 0.3.16 升级到 0.3.18
233 lines
12 KiB
TypeScript
233 lines
12 KiB
TypeScript
/**
|
||
* 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),
|
||
// P2(v0.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),
|
||
/** v0.3.18 修复: 监听工具就绪事件(MCP 初始化完成后触发) */
|
||
onReady: (callback: (data: { toolCount: number }) => void) => {
|
||
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 }>,
|
||
},
|
||
|
||
// ===== 数据管理 =====
|
||
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;
|
||
}
|