SDK · PC/Electron

Electron 适配层把真实 ImcoreClient、JWT / 登录身份、缓存、离线队列和媒体上传放在主进程。preload 只向 renderer 暴露受限 transport;renderer 仍使用同一套类型化 dmgrouproomcommunityairtc 与 operations API,但拿不到凭据和 Electron ipcRenderer

安装

npm install @imcore/sdk ws

适配层不依赖也不打包 Electron。main 与 renderer 是两个独立入口,确保 node:fs 不会进入 renderer 依赖图。

主进程

app.whenReady() 后创建客户端。token 刷新和登录身份必须留在这里,不要从 renderer IPC 接收。

import { app, BrowserWindow, ipcMain } from 'electron';
import { join } from 'node:path';
import WebSocket from 'ws';
import { ImcoreClient } from '@imcore/sdk';
import {
  createElectronFileStorage,
  registerElectronMainBridge,
} from '@imcore/sdk/electron/main';

await app.whenReady();

const win = new BrowserWindow({
  webPreferences: {
    preload: join(import.meta.dirname, 'preload.js'),
    contextIsolation: true,
    nodeIntegration: false,
    sandbox: true,
  },
});

const storage = createElectronFileStorage({
  filePath: join(app.getPath('userData'), 'imcore-state.json'),
});
const client = new ImcoreClient({
  url: 'wss://your-host/acc',
  webSocketImpl: WebSocket,
  auth: {
    getToken: () => readTokenFromKeychain(),
    getLogin: () => ({ userID: currentUserID(), userName: currentUserName() }),
  },
  storage,
});

const bridge = registerElectronMainBridge({
  client,
  ipcMain,
  webContents: () => win.webContents,
});

win.on('closed', () => bridge.dispose());

createElectronFileStorage() 会串行并原子替换写入。文件应放到 app.getPath('userData') 下;这里只存 SDK 缓存和离线队列,不存 JWT。凭据请放操作系统钥匙串,例如 Electron safeStorage 或项目已有的凭据服务。

registerElectronMainBridge() 默认只接受协议目录中已声明的命令,并只接受目标 webContents 发来的消息。renderer 信任级别较低时,可传 allowedCommands 进一步缩小范围。

Preload

打包下面的 preload 文件。它暴露的是窄 transport,不是 ipcRenderer

import { contextBridge, ipcRenderer } from 'electron';
import { exposeElectronRendererTransport } from '@imcore/sdk/electron/renderer';

exposeElectronRendererTransport(contextBridge, ipcRenderer);

Renderer

用 preload 暴露的 transport 创建类型化 facade:

import {
  createElectronRendererClient,
  type ElectronRendererTransport,
} from '@imcore/sdk/electron/renderer';

declare global {
  interface Window {
    imcoreTransport: ElectronRendererTransport;
  }
}

const im = createElectronRendererClient(window.imcoreTransport);

await im.connect();
await im.dm.send({ targetUserID: '1002', text: '来自 PC 客户端' });

const off = im.on('chat_message', (message) => renderMessage(message));
im.onStatus((status) => renderConnectionStatus(status));

// File/Blob 字节经 IPC 传递;HTTP 上传与 Authorization header 留在主进程。
await im.media.upload({ file, type: 'file', scene: 'dm', targetUserID: '1002' });

// renderer 销毁时:
off();
im.dispose();

错误会在 renderer 还原为 ElectronImcoreError,服务端错误和本地预检错误的数字 code 保持不变。Electron facade 的 send() 因序号需要跨 IPC 而返回 Promise;原有 typed feature 方法本身就是 Promise,调用形态不变。

多窗口

每个已登录窗口使用一套 bridge 和唯一 channelPrefix,并在 registerElectronMainBridge()exposeElectronRendererTransport() 两端保持一致。bridge 会拒绝其他 webContents ID 的调用;dispose() 会清理 IPC handler、事件订阅和连接。