// preload: 把主进程能力通过 contextBridge 暴露给渲染进程 import { contextBridge, ipcRenderer, webUtils } from 'electron' contextBridge.exposeInMainWorld('api', { platform: process.platform, versions: process.versions, // self / 设备 self: () => ipcRenderer.invoke('self:info'), listDevices: () => ipcRenderer.invoke('device:list'), // 启动时拉一次主进程当前状态 (WS 通道等) — 补上订阅前丢失的事件 appState: () => ipcRenderer.invoke('app:state'), triggerScan: () => ipcRenderer.invoke('device:triggerScan'), deleteDevice: (deviceId: string) => ipcRenderer.invoke('device:delete', deviceId), // 消息 listMessages: (peerId: string) => ipcRenderer.invoke('message:list', peerId), sendText: (toDeviceId: string, content: string, opts?: { replyTo?: any }) => ipcRenderer.invoke('message:sendText', { toDeviceId, content, replyTo: opts?.replyTo }), sendFile: (toDeviceId: string, localPath: string, opts?: { name?: string; mime?: string; asImage?: boolean; withProgress?: boolean; replyTo?: any }) => ipcRenderer.invoke('message:sendFile', { toDeviceId, localPath, ...(opts || {}) }), sendBuffer: (toDeviceId: string, dataBase64: string, name: string, mime: string, opts?: { replyTo?: any }) => ipcRenderer.invoke('message:sendBuffer', { toDeviceId, dataBase64, name, mime, replyTo: opts?.replyTo }), recall: (messageId: string) => ipcRenderer.invoke('message:recall', messageId), deleteMessage: (messageId: string, opts?: { deleteFile?: boolean }) => ipcRenderer.invoke('message:delete', messageId, opts), markRead: (peerId: string, upToMessageId: string) => ipcRenderer.invoke('message:markRead', { peerId, upToMessageId }), retry: (messageId: string) => ipcRenderer.invoke('message:retry', messageId), typing: (toDeviceId: string) => ipcRenderer.invoke('message:typing', toDeviceId), // 未读 unreadList: () => ipcRenderer.invoke('unread:list'), clearUnread: (deviceId: string) => ipcRenderer.invoke('unread:clear', deviceId), // 设置 getSettings: () => ipcRenderer.invoke('settings:get'), setSettings: (patch: any) => ipcRenderer.invoke('settings:set', patch), chooseDownloadDir: () => ipcRenderer.invoke('settings:chooseDownloadDir'), // 文件 openInFolder: (p: string) => ipcRenderer.invoke('file:openInFolder', p), open: (p: string) => ipcRenderer.invoke('file:open', p), readBase64: (p: string) => ipcRenderer.invoke('file:readBase64', p), pickFile: (opts?: { image?: boolean }) => ipcRenderer.invoke('file:pick', opts), // 图片: 缩略图 / 复制到剪贴板 / 看原图 imageThumb: (args: { path?: string; base64?: string; mime?: string; dataUrl?: string }) => ipcRenderer.invoke('image:thumb', args), imageCopy: (args: { path?: string; base64?: string; mime?: string; dataUrl?: string }) => ipcRenderer.invoke('image:copy', args), imageDataUrl: (path: string) => ipcRenderer.invoke('image:dataUrl', { path }), // 拖拽文件 -> 真实磁盘路径 (Electron 32+ 必须用 webUtils.getPathForFile) getPathForFile: (file: File): string => { try { return webUtils.getPathForFile(file) || '' } catch { return '' } }, // 应用 hide: () => ipcRenderer.invoke('app:hide'), appVersion: () => ipcRenderer.invoke('app:version'), // 开机自启 getAutoStart: () => ipcRenderer.invoke('autostart:get'), setAutoStart: (enabled: boolean) => ipcRenderer.invoke('autostart:set', enabled), // 网络接口 (只读) listInterfaces: () => ipcRenderer.invoke('network:listInterfaces'), // 事件订阅: 主进程 webContents.send('xxx', payload) 会在此处派发 on(channel: string, cb: (payload: any) => void) { const listener = (_e: any, payload: any) => cb(payload) ipcRenderer.on(channel, listener) return () => ipcRenderer.removeListener(channel, listener) }, // 离线消息: 启动后让主进程把 pending 重新尝试一遍 flushPending: () => ipcRenderer.invoke('message:flushPending'), // 远程: 终端 terminalOpen: (peerId: string, opts?: { rows?: number; cols?: number; readOnly?: boolean }) => ipcRenderer.invoke('terminal:open', { peerId, ...(opts || {}) }), terminalInput: (sessionId: string, dataBase64: string) => ipcRenderer.invoke('terminal:input', { sessionId, dataBase64 }), terminalResize: (sessionId: string, rows: number, cols: number) => ipcRenderer.invoke('terminal:resize', { sessionId, rows, cols }), terminalClose: (sessionId: string, reason?: string) => ipcRenderer.invoke('terminal:close', { sessionId, reason }), terminalListSessions: () => ipcRenderer.invoke('terminal:listSessions'), // 远程: 端口转发 forwardOpen: (peerId: string, args: { direction?: 'self-out' | 'self-in'; listenPort: number; targetHost: string; targetPort: number; ttlSec?: number }) => ipcRenderer.invoke('forward:open', { peerId, ...args }), forwardClose: (sessionId: string) => ipcRenderer.invoke('forward:close', { sessionId }), forwardListSessions: () => ipcRenderer.invoke('forward:listSessions'), // 远程: 串口 (映射 + 串口助手) // 串口: 列表 / 映射 / 助手 serialList: (peerId: string) => ipcRenderer.invoke('serial:list', { peerId }), serialListLocal: () => ipcRenderer.invoke('serial:listLocal'), serialAttach: (peerId: string, busId: string, opts: { direction?: 'self-out' | 'self-in'; config: any }) => ipcRenderer.invoke('serial:attach', { peerId, busId, ...(opts || {}) }), serialSend: (sessionId: string, dataBase64: string) => ipcRenderer.invoke('serial:send', { sessionId, dataBase64 }), serialDetach: (sessionId: string) => ipcRenderer.invoke('serial:detach', { sessionId }), serialListSessions: () => ipcRenderer.invoke('serial:listSessions'), serialDiagnose: () => ipcRenderer.invoke('serial:diagnose'), serialFixPerms: () => ipcRenderer.invoke('serial:fixPerms'), serialEnvInfo: () => ipcRenderer.invoke('serial:envInfo'), serialSaveLog: (content: string, suggestedName?: string) => ipcRenderer.invoke('serial:saveLog', { content, suggestedName }), // 网盘: 浏览对方共享目录 + 选文件下载 driveList: (peerId: string, path?: string) => ipcRenderer.invoke('drive:list', { peerId, path: path || '' }), driveRoot: (peerId: string) => ipcRenderer.invoke('drive:root', { peerId }), driveDownload: (peerId: string, paths: string[]) => ipcRenderer.invoke('drive:download', { peerId, paths }), driveOpenFolder: () => ipcRenderer.invoke('drive:openFolder'), drivePickDir: () => ipcRenderer.invoke('drive:pickDir'), // 文件夹同步: 本地目录 <-> 对方网盘目录 syncPickLocalDir: () => ipcRenderer.invoke('sync:pickLocalDir'), syncScan: (args: { peerId: string; localDir?: string; remotePath?: string; relationId?: string }) => ipcRenderer.invoke('sync:scan', args), syncApply: (args: { peerId: string; localDir?: string; remotePath?: string; relationId?: string; items: { relPath: string; action: 'upload' | 'download' | 'delete-local' | 'delete-remote' | 'skip' }[] }) => ipcRenderer.invoke('sync:apply', args), // 关系 (同步盘): 建立/授权/吊销 —— 对方那边直接弹窗, 不用邀请码 relationList: () => ipcRenderer.invoke('relation:list'), relationRequest: (args: { peerId: string; syncLocalDir: string; syncRemotePath?: string; grants: string[] }) => ipcRenderer.invoke('relation:request', args), relationSetGrants: (args: { relationId: string; grants: string[] }) => ipcRenderer.invoke('relation:setGrants', args), relationSetSync: (args: { relationId: string; syncLocalDir?: string; syncRemotePath?: string; syncEnabled?: boolean }) => ipcRenderer.invoke('relation:setSync', args), relationRevoke: (relationId: string) => ipcRenderer.invoke('relation:revoke', { relationId }), relationDelete: (relationId: string) => ipcRenderer.invoke('relation:delete', { relationId }), // 授权 / 审计 remoteApprovalList: () => ipcRenderer.invoke('remote:approval:list'), remoteApprovalReply: (requestId: string, ok: boolean, remember?: boolean, extra?: { grants?: string[]; syncLocalDir?: string; syncRemotePath?: string }) => ipcRenderer.invoke('remote:approval:reply', { requestId, ok, remember, ...(extra || {}) }), auditList: (args?: { limit?: number; since?: number; sessionId?: string }) => ipcRenderer.invoke('audit:list', args || {}), auditPrune: (olderThanMs?: number) => ipcRenderer.invoke('audit:prune', olderThanMs), })