Ctrl K
ComunidadGuías…

desktop/electron/preload.cjs

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
const { contextBridge, ipcRenderer } = require('electron')

function onIpc(channel, callback) {
  const handler = (_event, data) => callback(data)
  ipcRenderer.on(channel, handler)
  return () => ipcRenderer.removeListener(channel, handler)
}

contextBridge.exposeInMainWorld('electronAPI', {
  // Auth token persistence (Electron userData/auth.json, all load protocols)
  getAuthTokens: () => ipcRenderer.invoke('auth-get-tokens'),
  setAuthTokens: (tokens) => ipcRenderer.invoke('auth-set-tokens', tokens),
  clearAuthTokens: () => ipcRenderer.invoke('auth-clear-tokens'),

  // Multi-server baseUrl configuration
  serverConfig: {
    get: () => ipcRenderer.invoke('server-config-get'),
    set: (payload) => ipcRenderer.invoke('server-config-set', payload),
    probe: (serverBaseUrl, options) =>
      ipcRenderer.invoke('server-config-probe', {
        serverBaseUrl,
        allowHttp: options?.allowHttp === true,
      }),
    openPicker: () => ipcRenderer.invoke('server-config-open-picker'),
    closePicker: () => ipcRenderer.invoke('server-picker-close'),
    removeHistory: (serverBaseUrl) =>
      ipcRenderer.invoke('server-config-remove-history', { serverBaseUrl }),
  },

  // Original APIs
  getAppVersion: () => ipcRenderer.invoke('get-app-version'),
  getPlatform: () => ipcRenderer.invoke('get-platform'),
  getEnvironmentInfo: (workspace) => ipcRenderer.invoke('get-environment-info', { workspace }),
  gitStatus: (workspace, repoPath) => ipcRenderer.invoke('git-status', { workspace, repoPath }),
  refreshGitStatus: (workspace, repoPath) => ipcRenderer.invoke('git-status-refresh', { workspace, repoPath }),
  gitFileDiff: (workspace, repoPath, filePath) => ipcRenderer.invoke('git-file-diff', { workspace, repoPath, path: filePath }),
  gitRepos: (workspace) => ipcRenderer.invoke('git-repos', { workspace }),
  gitCommitInput: (workspace, repoPath) => ipcRenderer.invoke('git-commit-input', { workspace, repoPath }),
  gitCommit: (workspace, repoPath, message) => ipcRenderer.invoke('git-commit', { workspace, repoPath, message }),
  gitPull: (workspace, repoPath) => ipcRenderer.invoke('git-pull', { workspace, repoPath }),
  gitPush: (workspace, repoPath) => ipcRenderer.invoke('git-push', { workspace, repoPath }),
  gitLog: (workspace, repoPath, options) => ipcRenderer.invoke('git-log', { workspace, repoPath, options }),
  gitCommitFiles: (workspace, repoPath, hash) => ipcRenderer.invoke('git-commit-files', { workspace, repoPath, hash }),
  gitCommitFileDiff: (workspace, repoPath, hash, path, oldPath) => ipcRenderer.invoke('git-commit-file-diff', { workspace, repoPath, hash, path, oldPath }),
  gitBranchCompare: (workspace, repoPath, base, target) => ipcRenderer.invoke('git-branch-compare', { workspace, repoPath, base, target }),
  gitBranchCompareFileDiff: (workspace, repoPath, base, target, path, oldPath) => ipcRenderer.invoke('git-branch-compare-file-diff', { workspace, repoPath, base, target, path, oldPath }),
  gitBranches: (workspace, repoPath) => ipcRenderer.invoke('git-branches', { workspace, repoPath }),
  gitStashes: (workspace, repoPath) => ipcRenderer.invoke('git-stashes', { workspace, repoPath }),
  gitRemotes: (workspace, repoPath) => ipcRenderer.invoke('git-remotes', { workspace, repoPath }),
  gitFetch: (workspace, repoPath) => ipcRenderer.invoke('git-fetch', { workspace, repoPath }),
  gitSwitchBranch: (workspace, repoPath, branch, create) => ipcRenderer.invoke('git-switch-branch', { workspace, repoPath, branch, create }),
  gitStashOp: (workspace, repoPath, action, ref) => ipcRenderer.invoke('git-stash-op', { workspace, repoPath, action, ref }),
  checkForUpdate: () => ipcRenderer.invoke('check-for-update'),
  installUpdate: () => ipcRenderer.invoke('install-update'),
  onUpdateChecking: (callback) => onIpc('update-checking', callback),
  onDownloadProgress: (callback) => onIpc('download-progress', callback),
  onUpdateAvailable: (callback) => onIpc('update-available', callback),
  onUpdateNotAvailable: (callback) => onIpc('update-not-available', callback),
  onUpdateDownloaded: (callback) => onIpc('update-downloaded', callback),
  onUpdateError: (callback) => onIpc('update-error', callback),
  minimizeWindow: () => ipcRenderer.invoke('window-minimize'),
  maximizeWindow: () => ipcRenderer.invoke('window-maximize'),
  closeWindow: () => ipcRenderer.invoke('window-close'),
  openExternal: (url) => ipcRenderer.invoke('open-external', url),
  openFeishuAuthWindow: (authUrl, callbackPath) => ipcRenderer.invoke('open-feishu-auth-window', { authUrl, callbackPath }),
  selectDirectory: () => ipcRenderer.invoke('select-directory'),
  openFolder: (folderPath) => ipcRenderer.invoke('open-folder', folderPath),
  openTerminal: (folderPath) => ipcRenderer.invoke('open-terminal', folderPath),
  showItemInFolder: (fullPath) => ipcRenderer.invoke('show-item-in-folder', fullPath),

  // Tool execution via Streaming WS (called by renderer when server sends tool_execute)
  // approvalHint(V135)为第 8 参:服务端生成的「总是允许」模式提示,透传给审批卡片;旧调用缺省 null
  toolExecute: (toolName, args, requestId, workspace, sessionId, needApproval, dangerReason, approvalHint) =>
    ipcRenderer.invoke('tool-execute', { toolName, args, requestId, workspace, sessionId, needApproval, dangerReason, approvalHint: approvalHint ?? null }),

  // Local tool execution - direct IPC (for renderer-initiated calls)
  localReadFile: (args) => ipcRenderer.invoke('local-read-file', args),
  localReadBytes: (args) => ipcRenderer.invoke('local-read-bytes', args),
  localWriteFile: (args) => ipcRenderer.invoke('local-write-file', args),
  localEditFile: (args) => ipcRenderer.invoke('local-edit-file', args),

  // Workspace file listing for file references
  listWorkspaceFiles: (workspace, filter, limit) =>
    ipcRenderer.invoke('list-workspace-files', { workspace, filter, limit }),

  // Directory listing for file browser (non-recursive, single level)
  listDirectory: (dirPath, workspace) =>
    ipcRenderer.invoke('list-directory', { dirPath, workspace }),

  // Tool approval — renderer UI, main process waits for response
  onToolApprovalRequest: (callback) => {
    ipcRenderer.on('tool-approval-request', (event, data) => callback(data))
  },
  removeToolApprovalRequestListener: () => {
    ipcRenderer.removeAllListeners('tool-approval-request')
  },
  onToolApprovalDismiss: (callback) => {
    ipcRenderer.on('tool-approval-dismiss', (event, data) => callback(data))
  },
  removeToolApprovalDismissListener: () => {
    ipcRenderer.removeAllListeners('tool-approval-dismiss')
  },
  // 多端审批(技术方案 2026-11-06):本地卡点击 / 远端回声 / 清理都走这一个解锁入口。
  // alwaysAllow 仅在需要时透传,主进程不消费(规则由服务端按 hint 生成)。
  respondToolApprovalRemote: (requestId, approved, alwaysAllow) =>
    ipcRenderer.invoke('tool-approval-remote-response', { requestId, approved, alwaysAllow: alwaysAllow === true }),

  // Skill sync — renderer triggers, main process downloads & extracts zip
  skillSync: (sessionId, syncUrl, token, workspace, apiBase) =>
    ipcRenderer.invoke('skill-sync', { sessionId, syncUrl, token, workspace, apiBase }),
  onSkillSyncComplete: (callback) => {
    ipcRenderer.on('skill-sync-complete', (event, data) => callback(data))
  },
  removeSkillSyncCompleteListener: () => {
    ipcRenderer.removeAllListeners('skill-sync-complete')
  },

  // MCP — renderer triggers, main process connects & lists tools
  mcpSync: (sessionId, servers) =>
    ipcRenderer.invoke('mcp-sync', { sessionId, servers }),
  mcpClose: (sessionId) =>
    ipcRenderer.invoke('mcp-close', { sessionId }),

  // Local skills in ~/.agents/skills
  listLocalSkills: () => ipcRenderer.invoke('list-local-skills'),
  getLocalSkillDetail: (folderName) => ipcRenderer.invoke('get-local-skill-detail', { folderName }),
  readLocalSkillFiles: (folderName) => ipcRenderer.invoke('read-local-skill-files', { folderName }),

  // Read AGENTS.md from workspace root (for LOCAL mode system prompt injection)
  readAgentsMd: (workspace) => ipcRenderer.invoke('read-agents-md', { workspace }),

  // Terminal (node-pty) — embedded terminal panel
  terminal: {
    create: (options) => ipcRenderer.invoke('terminal:create', options),
    write: (id, data) => ipcRenderer.send('terminal:data', { id, data }),
    resize: (id, cols, rows) => ipcRenderer.send('terminal:resize', { id, cols, rows }),
    kill: (id) => ipcRenderer.invoke('terminal:kill', { id }),
    list: () => ipcRenderer.invoke('terminal:list'),
    onData: (callback) => {
      const handler = (_event, payload) => callback(payload)
      ipcRenderer.on('terminal:data', handler)
      return () => ipcRenderer.removeListener('terminal:data', handler)
    },
    onExit: (callback) => {
      const handler = (_event, payload) => callback(payload)
      ipcRenderer.on('terminal:exit', handler)
      return () => ipcRenderer.removeListener('terminal:exit', handler)
    },
  },

  // LOCAL attachments (P1) — copy a machine-local file into ~/.mao/attachments/<host>/<sid>/
  localCopyAttachment: (payload) => ipcRenderer.invoke('local-copy-attachment', payload),
  // LOCAL text dump — Agent 的 dump_user_messages 经服务端委托走 tool-execute;本桥为备用入口。
  localWriteTextFile: (payload) => ipcRenderer.invoke('local-write-text-file', payload),
  // Electron 32.0 removes File.path; webUtils.getPathForFile (same version) replaces it.
  // Implemented in the preload (a File object cannot cross structured-clone IPC); 28-31 keep
  // using file.path and never call this. Renderer compat: file.path ?? electronAPI.getPathForFile(file).
  getPathForFile: (file) => {
    try {
      const { webUtils } = require('electron')
      if (webUtils && typeof webUtils.getPathForFile === 'function') {
        return webUtils.getPathForFile(file) || ''
      }
    } catch {
      // Electron < 32: webUtils missing — caller falls back to File.path
    }
    return ''
  },

  // Desktop shell (P2/P3) — main-process notification / badge / prefs / shortcut / tray
  desktopNotify: (payload) => ipcRenderer.invoke('desktop-notify', payload),
  onDesktopNotificationClick: (callback) => onIpc('desktop-notification-click', callback),
  // 桥接就绪后主动拉取主进程积存的点击深链(点击时监听未注册会丢);收到后 ack 清除
  consumePendingDesktopNotificationClick: () => ipcRenderer.invoke('desktop-notification-click-consume'),
  ackDesktopNotificationClick: () => ipcRenderer.invoke('desktop-notification-click-ack'),
  onDesktopNotifyResult: (callback) => onIpc('desktop-notify-result', callback),
  desktopBadge: (count) => ipcRenderer.invoke('desktop-badge', { count }),
  desktopPrefsGet: () => ipcRenderer.invoke('desktop-prefs-get'),
  desktopPrefsSet: (patch) => ipcRenderer.invoke('desktop-prefs-set', patch),
  getDeviceInfo: () => ipcRenderer.invoke('device-info-get'),
  desktopShortcutSet: (accel) => ipcRenderer.invoke('desktop-shortcut-set', accel),
  onDesktopShortcutFailed: (callback) => onIpc('desktop-shortcut-failed', callback),
  onTrayNewTask: (callback) => onIpc('tray-new-task', callback),
  onTrayFirstCloseHint: (callback) => onIpc('tray-first-close-hint', callback),
})