@isdk/tool-electron 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.cn.md ADDED
@@ -0,0 +1,588 @@
1
+ # @isdk/tool-electron
2
+
3
+ > ✨ **Electron 原生 IPC 传输层 —— 为 `ToolFunc` 框架打造**
4
+ > 通过 IPC 构建解耦、类型安全、实时的 Electron 应用,支持 RPC 工具调用和 Pub/Sub 事件总线。
5
+
6
+ [🌐 English](./README.md) | 🇨🇳 中文文档
7
+
8
+ [![npm version](https://img.shields.io/npm/v/@isdk/tool-electron.svg?style=flat-square)](https://www.npmjs.com/package/@isdk/tool-electron)
9
+ [![Vitest Tests](https://img.shields.io/badge/tests-vitest-green?style=flat-square)](https://vitest.dev/)
10
+ [![TypeScript](https://img.shields.io/badge/types-TypeScript-blue?style=flat-square)](https://www.typescriptlang.org/)
11
+ [![License: MIT](https://img.shields.io/badge/license-MIT-purple?style=flat-square)](LICENSE)
12
+
13
+ ```bash
14
+ npm install @isdk/tool-electron
15
+ ```
16
+
17
+ 基于 [`@isdk/tool-func`](https://github.com/isdk/ai-tools) —— 定义可复用的、自文档化的函数工具。
18
+
19
+ ---
20
+
21
+ ## 📖 目录
22
+
23
+ - [特点](#-特点)
24
+ - [架构](#-架构)
25
+ - [安全桥接模式](#-安全桥接模式)
26
+ - [快速开始](#-快速开始)
27
+ - [API 参考](#-api-参考)
28
+ - [EventServer/EventClient 集成](#-eventservereventclient-集成)
29
+ - [测试](#-测试)
30
+ - [CI 集成](#-ci-集成)
31
+ - [许可证](#-许可证)
32
+
33
+ ---
34
+
35
+ ## 🌟 特点
36
+
37
+ 与 `@isdk/tool-rpc` / `@isdk/tool-event` 配套使用。将业务逻辑定义一次为工具,然后在渲染进程中像调用本地方法一样调用它们——无需 HTTP。
38
+
39
+ * ✅ **零网络开销** — 使用 Electron 原生 IPC
40
+ * ✅ **RPC 工具调用** — 从渲染进程调用主进程定义的函数
41
+ * ✅ **实时事件总线** — 双向 Pub/Sub,带自动会话管理
42
+ * ✅ **统一的错误模型** — 通过 `@isdk/common-error`,服务端错误自动作为 `CommonError` 抛给客户端
43
+ * ✅ **AbortSignal 支持** — 客户端可取消等待中的 IPC 调用
44
+ * ✅ **安全桥接模式** — 所有传输类支持注入式桥接对象(`Bridge`、`ServerIpcMain`、`PubSubBridge`、`ServerPubSubIpcMain`),适配 `contextBridge`
45
+ * ✅ **动态命名空间** — 通过 `apiUrl` 参数运行多个隔离的工具/事件总线
46
+ * ✅ **URI Scheme 路由** — `apiUrl` 采用 URI 规范;`electron://` 路由到 IPC 传输层,`http://` 路由到 HTTP;`RpcTransportManager` 自动注册
47
+ * ✅ **IPC 通道命名规范** — 所有通道基于命名空间自动生成,避免冲突
48
+
49
+ ---
50
+
51
+ ## 🔄 架构
52
+
53
+ ```mermaid
54
+ graph LR
55
+ subgraph "主进程 (Main Process)"
56
+ A[IpcServerToolTransport] -->|ipcMain.handle| D[(IPC Channel)]
57
+ C[ElectronServerPubSubTransport] -->|ipcMain.on/send| D
58
+ end
59
+
60
+ subgraph "预加载脚本 (Preload — contextBridge)"
61
+ P["electronIpc.invoke/on/off/send"] -->|ipcRenderer| D
62
+ end
63
+
64
+ subgraph "渲染进程 (Renderer)"
65
+ F[IpcClientToolTransport] -->|bridge.invoke| P
66
+ G[ElectronClientPubSubTransport] -->|bridge.on/off/send| P
67
+ end
68
+ ```
69
+
70
+ 预加载脚本作为安全边界 —— 渲染进程永远不会直接 `import 'electron'`。
71
+
72
+ ### IPC 通道命名约定
73
+
74
+ 所有通道基于 `apiUrl` 命名空间自动生成:
75
+
76
+ | 通道 | 模式 | 用途 |
77
+ |------|------|------|
78
+ | RPC 发现 | `{namespace}:discover` | 客户端获取服务端工具列表 |
79
+ | RPC 调用 | `{namespace}:rpc` | 客户端调用服务端工具 |
80
+ | PubSub 下行 | `pubsub-downstream:{namespace}` | 服务端 → 客户端事件推送 |
81
+ | PubSub 上行 | `pubsub-upstream:{namespace}` | 客户端 → 服务端事件发送 |
82
+ | PubSub 连接 | `pubsub-connect:{namespace}` | 客户端发起 PubSub 连接 |
83
+ | PubSub 断开 | `pubsub-disconnect:{namespace}` | 客户端断开 PubSub 连接 |
84
+
85
+ 例如,`apiUrl = 'electron://my-app'` 会提取 `my-app` 作为命名空间,生成 `my-app:discover`、`my-app:rpc`、`pubsub-downstream:my-app` 等。
86
+
87
+ ---
88
+
89
+ ## 🔒 安全桥接模式
90
+
91
+ 当启用 `contextIsolation`(Electron 12 起默认开启)时,渲染进程无法直接访问 Node.js 或 Electron API。推荐的做法是:
92
+
93
+ 1. **预加载脚本** — 通过 `contextBridge` 暴露最小化的桥接对象
94
+ 2. **主进程** — 可选地注入自定义 `ipcMain` 对象用于测试
95
+ 3. **渲染进程** — 使用注入的桥接对象创建传输实例
96
+
97
+ ### 四种可注入桥接类型
98
+
99
+ | 类型 | 描述 | 方法 | 用于 |
100
+ |------|------|------|------|
101
+ | `Bridge` | 客户端 RPC 桥接 | `invoke(channel, ...args): Promise<any>` | `IpcClientToolTransport` |
102
+ | `ServerIpcMain` | 服务端 RPC 桥接 | `handle(channel, handler)`, `removeHandler(channel)` | `IpcServerToolTransport` |
103
+ | `PubSubBridge` | 客户端 Pub/Sub 桥接 | `on(channel, listener)`, `off(channel, listener)`, `send(channel, ...args)` | `ElectronClientPubSubTransport` |
104
+ | `ServerPubSubIpcMain` | 服务端 Pub/Sub 桥接 | `on(channel, listener)`, `removeAllListeners(channel?)` | `ElectronServerPubSubTransport` |
105
+
106
+ ### 预加载脚本
107
+
108
+ 预加载脚本是唯一能访问 `ipcRenderer` 的地方。只暴露所需的 API:
109
+
110
+ ```ts
111
+ // preload.ts
112
+ import { contextBridge, ipcRenderer } from 'electron';
113
+
114
+ const electronIpc = {
115
+ invoke: (ch: string, ...args: any[]) => ipcRenderer.invoke(ch, ...args),
116
+ on: (ch: string, listener: (event: any, ...args: any[]) => void) => {
117
+ ipcRenderer.on(ch, listener);
118
+ },
119
+ off: (ch: string, listener: (event: any, ...args: any[]) => void) => {
120
+ ipcRenderer.removeListener(ch, listener);
121
+ },
122
+ send: (ch: string, ...args: any[]) => ipcRenderer.send(ch, ...args),
123
+ };
124
+
125
+ contextBridge.exposeInMainWorld('electronIpc', electronIpc);
126
+ ```
127
+
128
+ ---
129
+
130
+ ## 🚀 快速开始
131
+
132
+ ### 1. 主进程 (Server)
133
+
134
+ ```ts
135
+ // main.ts
136
+ import { ServerTools } from '@isdk/tool-rpc';
137
+ import { EventServer } from '@isdk/tool-event';
138
+ import {
139
+ IpcServerToolTransport,
140
+ ElectronServerPubSubTransport,
141
+ } from '@isdk/tool-electron';
142
+
143
+ // 注册一个工具
144
+ ServerTools.register({
145
+ name: 'getUser',
146
+ func: async ({ id }: { id: string }) => ({ id, name: 'Alice' }),
147
+ });
148
+
149
+ // 挂载 RPC 传输层,命名空间 'my-app'
150
+ const server = new IpcServerToolTransport({ apiUrl: 'electron://my-app' });
151
+ server.addDiscoveryHandler('electron://my-app', () => ServerTools.toJSON());
152
+ server.addRpcHandler('electron://my-app');
153
+ await server.start();
154
+
155
+ // 设置事件总线
156
+ EventServer.setPubSubTransport(
157
+ new ElectronServerPubSubTransport('electron://my-app-events'),
158
+ );
159
+ EventServer.get()?.register();
160
+ ```
161
+
162
+ > **测试提示:** 可以传入 mock `ipcMain` 对象:
163
+ >
164
+ > ```ts
165
+ > const server = new IpcServerToolTransport({
166
+ > apiUrl: 'electron://test',
167
+ > ipcMain: { handle: () => {}, removeHandler: () => {} },
168
+ > });
169
+ > ```
170
+
171
+ ### 2. 预加载脚本
172
+
173
+ 见上方 [预加载脚本](#预加载脚本) 示例。暴露 `electronIpc` 桥接对象。
174
+
175
+ ### 3. 渲染进程 (Renderer)
176
+
177
+ ```ts
178
+ // renderer.ts
179
+ import {
180
+ IpcClientToolTransport,
181
+ type Bridge,
182
+ } from '@isdk/tool-electron';
183
+
184
+ const bridge: Bridge = (window as any).electronIpc;
185
+
186
+ // 创建传输层,注入桥接对象
187
+ const transport = new IpcClientToolTransport('electron://my-app', { bridge });
188
+
189
+ // 加载工具定义
190
+ await transport.loadApis();
191
+
192
+ // 调用远程工具
193
+ const result = await transport._fetch('getUser', { id: '42' });
194
+ console.log(result); // { id: '42', name: 'Alice' }
195
+ ```
196
+
197
+ ### 4. 完整示例(RPC + Pub/Sub)
198
+
199
+ **main.ts**
200
+
201
+ ```ts
202
+ import { ServerTools } from '@isdk/tool-rpc';
203
+ import {
204
+ IpcServerToolTransport,
205
+ ElectronServerPubSubTransport,
206
+ } from '@isdk/tool-electron';
207
+
208
+ ServerTools.register({
209
+ name: 'getUser',
210
+ func: async ({ id }: { id: string }) => ({ id, name: 'Alice' }),
211
+ });
212
+
213
+ const rpcServer = new IpcServerToolTransport({ apiUrl: 'electron://my-app' });
214
+ rpcServer.addDiscoveryHandler('electron://my-app', () => ServerTools.toJSON());
215
+ rpcServer.addRpcHandler('electron://my-app');
216
+ await rpcServer.start();
217
+
218
+ const pubSubServer = new ElectronServerPubSubTransport('electron://my-app-events');
219
+ pubSubServer.listen();
220
+ ```
221
+
222
+ **preload.ts** — 同上方的预加载脚本示例。
223
+
224
+ **renderer.ts**
225
+
226
+ ```ts
227
+ import {
228
+ IpcClientToolTransport,
229
+ ElectronClientPubSubTransport,
230
+ type Bridge,
231
+ type PubSubBridge,
232
+ } from '@isdk/tool-electron';
233
+
234
+ const bridge: Bridge = (window as any).electronIpc;
235
+ const pubsubBridge: PubSubBridge = (window as any).electronIpc;
236
+
237
+ const rpc = new IpcClientToolTransport('electron://my-app', { bridge });
238
+ await rpc.loadApis();
239
+
240
+ const pubsub = new ElectronClientPubSubTransport('electron://my-app-events', {
241
+ bridge: pubsubBridge,
242
+ });
243
+ const stream = pubsub.connect('electron://my-app-events');
244
+
245
+ stream.on('server-event', (data) => {
246
+ console.log('收到事件:', data);
247
+ });
248
+ ```
249
+
250
+ ---
251
+
252
+ ## 📦 API 参考
253
+
254
+ ### 类
255
+
256
+ #### `IpcServerToolTransport`
257
+
258
+ 服务端 RPC 传输层。在 Electron 主进程中使用。
259
+
260
+ ```ts
261
+ class IpcServerToolTransport extends ServerToolTransport {
262
+ constructor(options?: {
263
+ apiUrl?: string; // IPC 通道 URI,默认 'electron://ai-tool'
264
+ dispatcher?: RpcServerDispatcher; // v2.6 调度器
265
+ ipcMain?: ServerIpcMain; // 可注入的自定义 ipcMain
266
+ });
267
+
268
+ addDiscoveryHandler(apiPrefix: string, handler: () => any): void;
269
+ addRpcHandler(apiPrefix: string, options?: { registry?: any }): void;
270
+ start(): Promise<void>;
271
+ stop(force?: boolean): Promise<void>;
272
+ getRaw(): { ipcMain: ServerIpcMain; channels: Channels };
273
+ }
274
+ ```
275
+
276
+ **选项说明:**
277
+
278
+ - `apiUrl` — IPC 通道 URI(`electron://` 格式),例如 `'electron://my-app'` 会提取命名空间 `my-app`,生成 `my-app:discover` 和 `my-app:rpc` 两个 IPC 通道
279
+ - `ipcMain` — 可选的桥接对象,用于测试或自定义主进程 IPC 设置。不传时使用 `electron` 的 `ipcMain`
280
+ - `dispatcher` — V2.6 执行调度器,用于并发隔离(影子实例)和任务追踪
281
+
282
+ #### `IpcClientToolTransport`
283
+
284
+ 客户端 RPC 传输层。在 Electron 渲染进程中使用。
285
+
286
+ ```ts
287
+ class IpcClientToolTransport extends ClientToolTransport {
288
+ constructor(
289
+ apiUrl?: string, // IPC 通道 URI(electron:// 格式),默认 'electron://ai-tool'
290
+ options?: {
291
+ bridge?: Bridge; // 可注入的桥接对象(配合 contextBridge)
292
+ },
293
+ );
294
+
295
+ setApiUrl(apiUrl: string): void;
296
+ loadApis(): Promise<Funcs>;
297
+ _fetch(name: string, args?: any, act?: any, subName?: any, fetchOptions?: any): Promise<any>;
298
+ toObject(res: any): Promise<any>;
299
+ }
300
+ ```
301
+
302
+ **错误处理:**
303
+ `_fetch` 会在服务端返回错误时直接抛出 `CommonError`,无需手动调用 `toObject()`:
304
+
305
+ ```ts
306
+ import { CommonError } from '@isdk/common-error';
307
+
308
+ try {
309
+ const result = await transport._fetch('getUser', { id: 'invalid' });
310
+ } catch (err: any) {
311
+ if (err instanceof CommonError) {
312
+ console.error(`错误 [${err.code}]: ${err.message}`, err.data);
313
+ }
314
+ }
315
+ ```
316
+
317
+ #### `ElectronServerPubSubTransport`
318
+
319
+ 服务端 Pub/Sub 传输层。
320
+
321
+ ```ts
322
+ class ElectronServerPubSubTransport implements IPubSubServerTransport {
323
+ readonly name = 'electron';
324
+ readonly protocol = 'electron' as const;
325
+
326
+ constructor(
327
+ namespace: string, // 必须:命名空间(支持 electron:// 格式),用于生成 IPC 通道
328
+ options?: {
329
+ ipcMain?: ServerPubSubIpcMain; // 可注入的自定义 ipcMain
330
+ },
331
+ );
332
+
333
+ listen(): void; // 激活 IPC 监听器
334
+ connect(options?: { req: IpcMainEvent; res: WebContents; events?: string[] }): PubSubServerSession;
335
+ subscribe(session: PubSubServerSession, events: string[]): void;
336
+ unsubscribe(session: PubSubServerSession, events: string[]): void;
337
+ publish(event: string, data: any, target?: { clientId?: string | string[] }, ctx?: PubSubCtx): void;
338
+ cleanup(): void;
339
+ onConnection(cb: (s: PubSubServerSession) => void): void;
340
+ onDisconnect(cb: (s: PubSubServerSession) => void): void;
341
+ onMessage(cb: (session: PubSubServerSession, event: string, data: any, ctx?: PubSubCtx) => void): void;
342
+ }
343
+ ```
344
+
345
+ **注意:** `listen()` 必须在 `connect()` 被调用前调用一次。`cleanup()` 应在应用退出时调用。
346
+
347
+ #### `ElectronClientPubSubTransport`
348
+
349
+ 客户端 Pub/Sub 传输层。
350
+
351
+ ```ts
352
+ class ElectronClientPubSubTransport implements IPubSubClientTransport {
353
+ constructor(
354
+ apiRoot?: string, // 命名空间(支持 electron:// 格式)
355
+ options?: {
356
+ bridge?: PubSubBridge; // 可注入的桥接对象
357
+ },
358
+ );
359
+
360
+ setApiRoot(apiRoot: string): void;
361
+ connect(toolName: string, params?: any): PubSubClientStream;
362
+ disconnect(stream: PubSubClientStream): void;
363
+ cleanup(): Promise<void>;
364
+ }
365
+ ```
366
+
367
+ ### 类型
368
+
369
+ ```ts
370
+ // 客户端 RPC 桥接
371
+ type Bridge = {
372
+ invoke: (channel: string, ...args: any[]) => Promise<any>;
373
+ };
374
+
375
+ // 服务端 RPC 桥接
376
+ type ServerIpcMain = {
377
+ handle: (channel: string, handler: (event: any, ...args: any[]) => any) => void;
378
+ removeHandler: (channel: string) => void;
379
+ };
380
+
381
+ // 客户端 Pub/Sub 桥接
382
+ type PubSubBridge = {
383
+ on: (channel: string, listener: (event: any, ...args: any[]) => void) => void;
384
+ off: (channel: string, listener: (event: any, ...args: any[]) => void) => void;
385
+ send: (channel: string, ...args: any[]) => void;
386
+ };
387
+
388
+ // 服务端 Pub/Sub 桥接
389
+ type ServerPubSubIpcMain = {
390
+ on: (channel: string, listener: (event: any, ...args: any[]) => void) => void;
391
+ removeAllListeners: (channel?: string) => void;
392
+ };
393
+
394
+ // IPC 通道
395
+ type Channels = {
396
+ discover: string;
397
+ rpc: string;
398
+ };
399
+ ```
400
+
401
+ ### PubSub 流
402
+
403
+ `connect()` 返回的 `PubSubClientStream` 对象:
404
+
405
+ ```ts
406
+ interface PubSubClientStream {
407
+ clientId: string;
408
+ protocol: string;
409
+ readonly readyState: number; // 1 = OPEN, 2 = CLOSED
410
+
411
+ on(event: string, listener: (data: any, ctx?: PubSubCtx) => void): () => void;
412
+ off(event: string, listener: (data: any, ctx?: PubSubCtx) => void): void;
413
+ send(event: string, data: any, ctx?: PubSubCtx): void;
414
+ close(): void;
415
+ }
416
+ ```
417
+
418
+ ---
419
+
420
+ ## 🔄 EventServer/EventClient 集成
421
+
422
+ 本包可与 `@isdk/tool-event` 的 `EventServer` / `EventClient` 配合使用,在 Electron IPC 上实现完整的事件总线。
423
+
424
+ ### 测试 Fixture 中的配置
425
+
426
+ 实际测试使用了**两套独立的传输层**(均使用 `electron://` URI 格式):
427
+
428
+ | 流程 | 传输命名空间 | 触发方式 |
429
+ |------|-------------|----------|
430
+ | 基础 PubSub | `electron://integration-test` | `publishEvent` RPC 工具 → `pubSubServer.publish()` |
431
+ | EventClient | `electron://event-bus` | `eventPublish` RPC 工具 → `EventServer.publish()` |
432
+
433
+ ### 关键注意事项
434
+
435
+ 1. **`eventPublish` 必须调用 `EventServer.publish()`**,而不是 `pubSubServer.publish()`,否则 EventClient 收不到事件
436
+ 2. **`backendEventable` 会在回调中前置事件名参数**:`(eventName, data)` 而非 `(data)`
437
+ 3. **`forwardEvent` + `emit`** 经由上游 IPC 发送到服务端,服务端自动添加 `client:` 前缀
438
+ 4. **事件名称必须预先订阅**:服务端只在 `connect()` 时声明的 `events` 集合范围内过滤事件
439
+
440
+ ---
441
+
442
+ ## 🧪 测试
443
+
444
+ ### 单元测试(55 个)
445
+
446
+ 使用模拟的 Electron API,无需真实 Electron:
447
+
448
+ ```bash
449
+ pnpm test # 运行一次
450
+ pnpm run test:watch # 监听模式
451
+ ```
452
+
453
+ 测试文件位于 `test/` 目录:
454
+
455
+ - `test/namespaces.test.ts` — 命名空间规范化(含 electron:// URI 提取)
456
+ - `test/ipc-transport.test.ts` — RPC 传输(含 URI scheme 测试)
457
+ - `test/electron-pubsub.test.ts` — PubSub 传输
458
+ - `test/event-server-client.test.ts` — EventServer/EventClient
459
+
460
+ ### 集成测试(13 个)
461
+
462
+ 使用 Playwright + 真实 Electron:
463
+
464
+ ```bash
465
+ pnpm run test:integration
466
+ ```
467
+
468
+ 测试场景:
469
+
470
+ | # | 场景 | 说明 |
471
+ |---|------|------|
472
+ | 1 | 桥接初始化 | 验证 bridge ready |
473
+ | 2 | RPC 调用 | 简单的工具调用 |
474
+ | 3 | RPC 对象参数 | 返回结构化数据的工具 |
475
+ | 4 | RPC 错误 | 抛出 CommonError 的工具 |
476
+ | 5 | 未知工具 | 不存在的工具返回 404 |
477
+ | 6 | PubSub | 订阅 + 发布事件 |
478
+ | 7 | 多窗口 RPC | 两个独立窗口并发调用 |
479
+ | 8 | 多窗口 PubSub | 两个窗口顺序接收事件 |
480
+ | 9 | 窗口关闭/重连 | 新建窗口,关闭旧窗口,验证新窗口 |
481
+ | 10 | 并发 RPC | 15 个并行调用混合成功/错误/慢速 |
482
+ | 11 | EventServer 订阅 | EventClient 接收 EventServer.publish() |
483
+ | 12 | EventClient 转发 | forwardEvent + emit 到服务端 |
484
+ | 13 | EventClient 发布 | publish() RPC 到服务端 |
485
+
486
+ ### 运行全部测试
487
+
488
+ ```bash
489
+ pnpm run test:all # 构建 + 55 个单元测试 + 13 个集成测试(共 68 个)
490
+ ```
491
+
492
+ ### 集成测试共享工具
493
+
494
+ `test/integration/helpers.ts` 提供了以下共享函数:
495
+
496
+ | 函数 | 用途 |
497
+ |------|------|
498
+ | `launchElectronApp(mainPath)` | 启动 Electron(`--no-sandbox --disable-gpu`) |
499
+ | `captureConsole(page, logs)` | 捕获渲染进程日志 |
500
+ | `printConsoleLogs(logs)` | 在 afterAll 中打印日志 |
501
+ | `waitForBridge(page)` | 等待 `toolBridge.ready === true` |
502
+ | `callTool(page, name, params)` | 调用 RPC 工具 |
503
+ | `subscribeAndPublish(page, event, data)` | 原子化订阅 + 发布 |
504
+ | `subscribeAndWait(page, event)` | 仅订阅 |
505
+ | `createNewWindow(page, app)` | 通过 `createWindow` RPC 工具创建新窗口 |
506
+
507
+ ### 模板目录
508
+
509
+ `test/template/electron-integration/` 包含可重用的集成测试模板,供本仓库中其他包快速上手。适配指南:
510
+
511
+ ```bash
512
+ # 复制模板到你的 fixture 目录
513
+ mkdir -p test/fixtures/electron-app
514
+ cp test/template/electron-integration/*.cjs test/fixtures/electron-app/
515
+ cp test/template/electron-integration/*.html test/fixtures/electron-app/
516
+ cp test/template/electron-integration/electron.test.ts test/integration/
517
+ cp test/template/electron-integration/vite.integration.config.mjs .
518
+ ```
519
+
520
+ 模板中的 `<<< CHANGE:` 标记指明了需要修改的位置。
521
+
522
+ ### 经验教训
523
+
524
+ 1. **`sandbox: false` 是必须的** — 否则 preload 脚本没有 `__dirname`
525
+ 2. **不要添加 `window-all-closed` → `app.quit()`** — 测试需要关闭和重开窗口
526
+ 3. **先创建新窗口,再关闭旧窗口** — 不要对已关闭的页面调用 `callTool`
527
+ 4. **PubSub 广播竞态** — 多窗口测试务必顺序执行 `subscribeAndPublish`
528
+ 5. **服务端订阅过滤** — 事件必须在 `connect()` 时声明的集合内
529
+ 6. **使用 `.cjs` 扩展名** — 因为 monorepo 根目录有 `"type": "module"`
530
+ 7. **在无头 Linux 上必须使用 `xvfb-run`**
531
+ 8. **EventServer 和 EventClient 使用独立的传输命名空间**
532
+
533
+ ---
534
+
535
+ ## 🤖 CI 集成
536
+
537
+ `.github/workflows/ci.yml` 提供了 GitHub Actions CI 工作流:
538
+
539
+ - Node 20 和 22 矩阵
540
+ - `npx --yes playwright install-deps electron` 安装系统依赖
541
+ - 单元测试(无需 xvfb)
542
+ - 集成测试在 `xvfb-run --auto-servernum` 下运行
543
+ - ESLint 检查
544
+ - 超时限制 `timeout-minutes: 10`
545
+
546
+ ```yaml
547
+ # 简化的 CI 核心步骤
548
+ - uses: pnpm/action-setup@v4
549
+ - run: pnpm install --frozen-lockfile
550
+ - run: npx --yes playwright install-deps electron
551
+ - run: pnpm run build-fast
552
+ - run: pnpm test # 55 个单元测试
553
+ - run: xvfb-run --auto-servernum pnpm vitest run --config vite.integration.config.mjs # 13 个集成测试
554
+ ```
555
+
556
+ ---
557
+
558
+ ## 📚 相关文档
559
+
560
+ - [ToolFunc 核心](https://github.com/isdk/ai-tools/tree/main/packages/tool-func)
561
+ - [RPC 传输指南](https://github.com/isdk/ai-tools/tree/main/packages/tool-rpc)
562
+ - [事件系统指南](https://github.com/isdk/ai-tools/tree/main/packages/tool-event)
563
+ - [PubSub 开发者指南](./pubsub.md)
564
+ - [RPC 深度文档](./tool-rpc.md)
565
+
566
+ ---
567
+
568
+ ## 🤝 贡献
569
+
570
+ 欢迎贡献!
571
+
572
+ 1. Fork → `git clone`
573
+ 2. 创建分支 → `git checkout -b feat/your-feature`
574
+ 3. 提交 → `git commit -m 'feat: add XYZ'`
575
+ 4. 推送 → `git push origin feat/your-feature`
576
+ 5. 发起 PR 🎉
577
+
578
+ 请确保测试通过且类型检查无错误。
579
+
580
+ ---
581
+
582
+ ## 📜 许可证
583
+
584
+ MIT © [ISDK](https://github.com/isdk) — 参见 [LICENSE](LICENSE)
585
+
586
+ ---
587
+
588
+ > 💡 **小贴士**:使用 `EventServer.forward([...events])` 自动将全局事件中继给所有已连接的客户端!