sessix-server 0.1.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.
Files changed (61) hide show
  1. package/dist/approval/ApprovalProxy.d.ts +86 -0
  2. package/dist/approval/ApprovalProxy.d.ts.map +1 -0
  3. package/dist/approval/ApprovalProxy.js +363 -0
  4. package/dist/approval/ApprovalProxy.js.map +1 -0
  5. package/dist/hooks/HookInstaller.d.ts +55 -0
  6. package/dist/hooks/HookInstaller.d.ts.map +1 -0
  7. package/dist/hooks/HookInstaller.js +215 -0
  8. package/dist/hooks/HookInstaller.js.map +1 -0
  9. package/dist/index.d.ts +2 -0
  10. package/dist/index.d.ts.map +1 -0
  11. package/dist/index.js +3115 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/mdns/MdnsService.d.ts +36 -0
  14. package/dist/mdns/MdnsService.d.ts.map +1 -0
  15. package/dist/mdns/MdnsService.js +66 -0
  16. package/dist/mdns/MdnsService.js.map +1 -0
  17. package/dist/notification/ActivityPushChannel.d.ts +54 -0
  18. package/dist/notification/ActivityPushChannel.d.ts.map +1 -0
  19. package/dist/notification/ActivityPushChannel.js +235 -0
  20. package/dist/notification/ActivityPushChannel.js.map +1 -0
  21. package/dist/notification/ExpoNotificationChannel.d.ts +17 -0
  22. package/dist/notification/ExpoNotificationChannel.d.ts.map +1 -0
  23. package/dist/notification/ExpoNotificationChannel.js +57 -0
  24. package/dist/notification/ExpoNotificationChannel.js.map +1 -0
  25. package/dist/notification/MacNotificationChannel.d.ts +22 -0
  26. package/dist/notification/MacNotificationChannel.d.ts.map +1 -0
  27. package/dist/notification/MacNotificationChannel.js +33 -0
  28. package/dist/notification/MacNotificationChannel.js.map +1 -0
  29. package/dist/notification/NotificationService.d.ts +50 -0
  30. package/dist/notification/NotificationService.d.ts.map +1 -0
  31. package/dist/notification/NotificationService.js +177 -0
  32. package/dist/notification/NotificationService.js.map +1 -0
  33. package/dist/providers/ExecutionProvider.d.ts +60 -0
  34. package/dist/providers/ExecutionProvider.d.ts.map +1 -0
  35. package/dist/providers/ExecutionProvider.js +3 -0
  36. package/dist/providers/ExecutionProvider.js.map +1 -0
  37. package/dist/providers/ProcessProvider.d.ts +117 -0
  38. package/dist/providers/ProcessProvider.d.ts.map +1 -0
  39. package/dist/providers/ProcessProvider.js +507 -0
  40. package/dist/providers/ProcessProvider.js.map +1 -0
  41. package/dist/server.d.ts +32 -0
  42. package/dist/server.d.ts.map +1 -0
  43. package/dist/server.js +3054 -0
  44. package/dist/server.js.map +1 -0
  45. package/dist/session/ProjectReader.d.ts +44 -0
  46. package/dist/session/ProjectReader.d.ts.map +1 -0
  47. package/dist/session/ProjectReader.js +471 -0
  48. package/dist/session/ProjectReader.js.map +1 -0
  49. package/dist/session/SessionFileWatcher.d.ts +35 -0
  50. package/dist/session/SessionFileWatcher.d.ts.map +1 -0
  51. package/dist/session/SessionFileWatcher.js +207 -0
  52. package/dist/session/SessionFileWatcher.js.map +1 -0
  53. package/dist/session/SessionManager.d.ts +114 -0
  54. package/dist/session/SessionManager.d.ts.map +1 -0
  55. package/dist/session/SessionManager.js +356 -0
  56. package/dist/session/SessionManager.js.map +1 -0
  57. package/dist/ws/WsBridge.d.ts +55 -0
  58. package/dist/ws/WsBridge.d.ts.map +1 -0
  59. package/dist/ws/WsBridge.js +220 -0
  60. package/dist/ws/WsBridge.js.map +1 -0
  61. package/package.json +38 -0
@@ -0,0 +1,356 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SessionManager = void 0;
4
+ const uuid_1 = require("uuid");
5
+ const BUFFER_MAX = 5000;
6
+ /**
7
+ * 会话管理器 — 后端核心协调模块
8
+ *
9
+ * 职责:
10
+ * - 创建 / 终止 / 发送消息到 Claude 会话
11
+ * - 监听 ExecutionProvider 的事件流,将 ClaudeStreamEvent 包装成 ServerEvent 转发
12
+ * - 检测 status 变化(system init → running,result → completed/error)
13
+ * - 维护 AskUserQuestion 问题映射,连接 ExecutionProvider 和 WsBridge
14
+ */
15
+ class SessionManager {
16
+ provider;
17
+ /** 事件回调列表(事件会被转发到 WsBridge) */
18
+ eventCallbacks = [];
19
+ /** 每个会话的事件流取消订阅函数 */
20
+ unsubscribeMap = new Map();
21
+ /** 每个会话的事件缓冲区(用于新订阅者重放)*/
22
+ sessionEventBuffers = new Map();
23
+ /** AskUserQuestion 问题映射:requestId → resolve 回调 */
24
+ pendingQuestions = new Map();
25
+ /**
26
+ * 会话状态缓存(用于追踪 status 变化,检测 oldStatus !== newStatus 时广播)
27
+ *
28
+ * 这是 status 变化的唯一检测源。ProcessProvider 的 session.status 是实际值,
29
+ * 这里只缓存上次广播的值,用于去重。
30
+ */
31
+ lastBroadcastStatus = new Map();
32
+ /** 每个会话的服务器端累计统计 */
33
+ sessionStats = new Map();
34
+ /** assistant 事件合并缓冲区(30ms 窗口内的 assistant 事件合并为一次发送) */
35
+ pendingAssistantEvents = new Map();
36
+ constructor(provider) {
37
+ this.provider = provider;
38
+ }
39
+ // ============================================
40
+ // 公开 API
41
+ // ============================================
42
+ /**
43
+ * 创建新会话
44
+ *
45
+ * 调用 provider.startSession(),订阅事件流,
46
+ * 将 ClaudeStreamEvent 包装为 ServerEvent 转发。
47
+ */
48
+ async createSession(projectPath, message, resumeSessionId, newSessionId, model) {
49
+ const session = await this.provider.startSession({
50
+ projectPath,
51
+ message,
52
+ sessionId: resumeSessionId ?? newSessionId,
53
+ resume: !!resumeSessionId,
54
+ model,
55
+ });
56
+ // 记录初始状态
57
+ this.lastBroadcastStatus.set(session.id, session.status);
58
+ // 订阅该会话的事件流
59
+ this.subscribeToSession(session.id);
60
+ console.log(`[SessionManager] 会话已创建: ${session.id} (项目: ${projectPath})`);
61
+ return session;
62
+ }
63
+ /**
64
+ * 发送消息到已有会话
65
+ */
66
+ async sendMessage(sessionId, message) {
67
+ await this.provider.sendMessage(sessionId, message);
68
+ this.updateSessionStatus(sessionId, 'running');
69
+ console.log(`[SessionManager] 消息已发送到会话: ${sessionId}`);
70
+ }
71
+ /**
72
+ * 终止会话
73
+ */
74
+ async killSession(sessionId) {
75
+ // 取消事件订阅
76
+ this.unsubscribeSession(sessionId);
77
+ // 清除该会话的待回答问题
78
+ this.clearPendingQuestions(sessionId);
79
+ // 清除状态缓存、事件缓冲区、统计和 assistant 合并缓冲
80
+ this.lastBroadcastStatus.delete(sessionId);
81
+ this.sessionEventBuffers.delete(sessionId);
82
+ this.sessionStats.delete(sessionId);
83
+ const pending = this.pendingAssistantEvents.get(sessionId);
84
+ if (pending) {
85
+ clearTimeout(pending.timer);
86
+ this.pendingAssistantEvents.delete(sessionId);
87
+ }
88
+ await this.provider.killSession(sessionId);
89
+ console.log(`[SessionManager] 会话已终止: ${sessionId}`);
90
+ }
91
+ /**
92
+ * 获取会话的缓冲事件(用于新订阅者重放)
93
+ */
94
+ getSessionEvents(sessionId) {
95
+ return this.sessionEventBuffers.get(sessionId) ?? [];
96
+ }
97
+ /**
98
+ * 处理 AskUserQuestion 回答(从手机端传来)
99
+ */
100
+ handleQuestionResponse(requestId, answer) {
101
+ const pending = this.pendingQuestions.get(requestId);
102
+ if (!pending) {
103
+ console.warn(`[SessionManager] 未找到问题请求: ${requestId}`);
104
+ return;
105
+ }
106
+ this.pendingQuestions.delete(requestId);
107
+ // 回答完成后,会话状态回到 running
108
+ this.updateSessionStatus(pending.sessionId, 'running');
109
+ pending.resolve(answer);
110
+ console.log(`[SessionManager] 问题已回答: ${requestId}`);
111
+ }
112
+ /**
113
+ * 获取所有活跃会话(含服务器端统计)
114
+ */
115
+ getActiveSessions() {
116
+ return this.provider.getActiveSessions().map((session) => {
117
+ const stats = this.sessionStats.get(session.id);
118
+ return stats ? { ...session, stats } : session;
119
+ });
120
+ }
121
+ /**
122
+ * 注册事件回调(事件会被转发到 WsBridge)
123
+ *
124
+ * @returns 取消注册的函数
125
+ */
126
+ onEvent(callback) {
127
+ this.eventCallbacks.push(callback);
128
+ return () => {
129
+ const index = this.eventCallbacks.indexOf(callback);
130
+ if (index !== -1) {
131
+ this.eventCallbacks.splice(index, 1);
132
+ }
133
+ };
134
+ }
135
+ /**
136
+ * 清理所有资源
137
+ */
138
+ destroy() {
139
+ // 取消所有事件订阅
140
+ for (const [, unsub] of this.unsubscribeMap) {
141
+ unsub();
142
+ }
143
+ this.unsubscribeMap.clear();
144
+ this.sessionEventBuffers.clear();
145
+ this.sessionStats.clear();
146
+ // 清理 assistant 事件合并定时器
147
+ for (const [, pending] of this.pendingAssistantEvents) {
148
+ clearTimeout(pending.timer);
149
+ }
150
+ this.pendingAssistantEvents.clear();
151
+ // 清除所有待回答问题
152
+ this.pendingQuestions.clear();
153
+ this.lastBroadcastStatus.clear();
154
+ this.eventCallbacks.length = 0;
155
+ console.log('[SessionManager] 已销毁');
156
+ }
157
+ // ============================================
158
+ // 内部方法
159
+ // ============================================
160
+ /**
161
+ * 订阅指定会话的事件流(包括 AskUserQuestion 问题事件)
162
+ */
163
+ subscribeToSession(sessionId) {
164
+ const unsubscribeEvent = this.provider.onEvent(sessionId, (event) => {
165
+ this.handleClaudeEvent(sessionId, event);
166
+ });
167
+ const unsubscribeQuestion = this.provider.onQuestion(sessionId, ({ toolUseId, question, options }) => {
168
+ this.handleAskUserQuestion(sessionId, toolUseId, question, options);
169
+ });
170
+ this.unsubscribeMap.set(sessionId, () => {
171
+ unsubscribeEvent();
172
+ unsubscribeQuestion();
173
+ });
174
+ }
175
+ /**
176
+ * 取消指定会话的事件订阅
177
+ */
178
+ unsubscribeSession(sessionId) {
179
+ const unsub = this.unsubscribeMap.get(sessionId);
180
+ if (unsub) {
181
+ unsub();
182
+ this.unsubscribeMap.delete(sessionId);
183
+ }
184
+ }
185
+ /**
186
+ * 处理来自 provider 的 Claude 事件
187
+ *
188
+ * - 包装为 ServerEvent 转发
189
+ * - assistant 事件在 30ms 窗口内合并后批量发送(减少 WebSocket 帧数)
190
+ * - 检测 status 变化
191
+ */
192
+ handleClaudeEvent(sessionId, event) {
193
+ // 1. 缓冲事件(供新订阅者重放)
194
+ const buffer = this.sessionEventBuffers.get(sessionId) ?? [];
195
+ buffer.push(event);
196
+ // 限制缓冲区大小(最多保留 500 条最新事件)
197
+ if (buffer.length > BUFFER_MAX) {
198
+ buffer.splice(0, buffer.length - BUFFER_MAX);
199
+ }
200
+ this.sessionEventBuffers.set(sessionId, buffer);
201
+ // 2. 根据事件类型处理
202
+ switch (event.type) {
203
+ case 'assistant':
204
+ // 合并 assistant 事件:30ms 窗口内累积后一次性发送
205
+ this.bufferAssistantEvent(sessionId, event);
206
+ break;
207
+ case 'system':
208
+ // 非 assistant 事件:先 flush 已缓冲的 assistant 事件(保证顺序),再立即转发
209
+ this.flushPendingAssistant(sessionId);
210
+ this.emit({ type: 'claude_event', sessionId, event });
211
+ if (event.subtype === 'init') {
212
+ this.updateSessionStatus(sessionId, 'running');
213
+ }
214
+ break;
215
+ case 'user':
216
+ this.flushPendingAssistant(sessionId);
217
+ this.emit({ type: 'claude_event', sessionId, event });
218
+ break;
219
+ case 'result': {
220
+ this.flushPendingAssistant(sessionId);
221
+ this.emit({ type: 'claude_event', sessionId, event });
222
+ // 累计会话统计(来自 result 事件的权威数据)
223
+ const stats = this.sessionStats.get(sessionId) ?? {
224
+ totalInputTokens: 0,
225
+ totalOutputTokens: 0,
226
+ totalDurationMs: 0,
227
+ };
228
+ stats.totalDurationMs += event.duration_ms ?? 0;
229
+ if (event.usage) {
230
+ stats.totalInputTokens += event.usage.input_tokens ?? 0;
231
+ stats.totalOutputTokens += event.usage.output_tokens ?? 0;
232
+ }
233
+ if (event.total_cost_usd != null) {
234
+ stats.totalCostUsd = (stats.totalCostUsd ?? 0) + event.total_cost_usd;
235
+ }
236
+ this.sessionStats.set(sessionId, stats);
237
+ if (event.is_error) {
238
+ this.updateSessionStatus(sessionId, 'error');
239
+ }
240
+ else {
241
+ this.updateSessionStatus(sessionId, 'idle');
242
+ }
243
+ break;
244
+ }
245
+ }
246
+ }
247
+ /**
248
+ * 缓冲 assistant 事件到 30ms 窗口
249
+ */
250
+ bufferAssistantEvent(sessionId, event) {
251
+ let pending = this.pendingAssistantEvents.get(sessionId);
252
+ if (!pending) {
253
+ pending = {
254
+ events: [],
255
+ timer: setTimeout(() => this.flushPendingAssistant(sessionId), 30),
256
+ };
257
+ this.pendingAssistantEvents.set(sessionId, pending);
258
+ }
259
+ pending.events.push(event);
260
+ }
261
+ /**
262
+ * 刷新缓冲的 assistant 事件,批量发送
263
+ */
264
+ flushPendingAssistant(sessionId) {
265
+ const pending = this.pendingAssistantEvents.get(sessionId);
266
+ if (!pending)
267
+ return;
268
+ clearTimeout(pending.timer);
269
+ this.pendingAssistantEvents.delete(sessionId);
270
+ if (pending.events.length === 1) {
271
+ // 只有一条事件,不用批量包装
272
+ this.emit({ type: 'claude_event', sessionId, event: pending.events[0] });
273
+ }
274
+ else if (pending.events.length > 1) {
275
+ // 多条事件,用 claude_events(复数)批量发送
276
+ this.emit({ type: 'claude_events', sessionId, events: pending.events });
277
+ }
278
+ }
279
+ /**
280
+ * 更新会话状态,如果状态发生变化则广播通知
281
+ *
282
+ * 使用 lastBroadcastStatus 去重,只在状态实际变化时广播。
283
+ */
284
+ updateSessionStatus(sessionId, newStatus) {
285
+ const lastStatus = this.lastBroadcastStatus.get(sessionId);
286
+ if (lastStatus !== newStatus) {
287
+ this.lastBroadcastStatus.set(sessionId, newStatus);
288
+ this.emit({
289
+ type: 'status_change',
290
+ sessionId,
291
+ status: newStatus,
292
+ });
293
+ console.log(`[SessionManager] 会话 ${sessionId} 状态变化: ${lastStatus ?? '(无)'} → ${newStatus}`);
294
+ }
295
+ }
296
+ /**
297
+ * 处理 AskUserQuestion 事件:广播问题请求到手机,等待用户回答
298
+ */
299
+ handleAskUserQuestion(sessionId, toolUseId, question, options) {
300
+ const requestId = (0, uuid_1.v4)();
301
+ const request = {
302
+ id: requestId,
303
+ sessionId,
304
+ toolUseId,
305
+ question,
306
+ options,
307
+ createdAt: Date.now(),
308
+ };
309
+ // 更新会话状态为等待回答
310
+ this.updateSessionStatus(sessionId, 'waiting_question');
311
+ // 广播问题请求到手机端
312
+ this.emit({ type: 'question_request', request });
313
+ // 等待用户回答,然后通过 provider.answerQuestion 写入 stdin
314
+ const answerPromise = new Promise((resolve) => {
315
+ this.pendingQuestions.set(requestId, { sessionId, toolUseId, resolve });
316
+ });
317
+ answerPromise.then(async (answer) => {
318
+ try {
319
+ await this.provider.answerQuestion(sessionId, toolUseId, answer);
320
+ }
321
+ catch (err) {
322
+ console.error(`[SessionManager] answerQuestion 失败 (${sessionId}):`, err);
323
+ }
324
+ });
325
+ console.log(`[SessionManager] 会话 ${sessionId}: AskUserQuestion 已推送 (requestId=${requestId})`);
326
+ }
327
+ /**
328
+ * 清除指定会话的所有待回答问题
329
+ */
330
+ clearPendingQuestions(sessionId) {
331
+ const toRemove = [];
332
+ for (const [requestId, pending] of this.pendingQuestions) {
333
+ if (pending.sessionId === sessionId) {
334
+ toRemove.push(requestId);
335
+ }
336
+ }
337
+ for (const requestId of toRemove) {
338
+ this.pendingQuestions.delete(requestId);
339
+ }
340
+ }
341
+ /**
342
+ * 发出 ServerEvent 到所有已注册的回调
343
+ */
344
+ emit(event) {
345
+ for (const callback of this.eventCallbacks) {
346
+ try {
347
+ callback(event);
348
+ }
349
+ catch (err) {
350
+ console.error('[SessionManager] 事件回调执行异常:', err);
351
+ }
352
+ }
353
+ }
354
+ }
355
+ exports.SessionManager = SessionManager;
356
+ //# sourceMappingURL=SessionManager.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SessionManager.js","sourceRoot":"","sources":["../../src/session/SessionManager.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,55 @@
1
+ import { WebSocket } from 'ws';
2
+ import type { ServerEvent, ClientEvent } from '@sessix/shared';
3
+ /** WsBridge 配置 */
4
+ interface WsBridgeOptions {
5
+ port: number;
6
+ token: string;
7
+ }
8
+ /**
9
+ * WebSocket 桥接服务
10
+ *
11
+ * 负责与手机端建立 WebSocket 连接,完成事件的收发。
12
+ * - 连接鉴权(URL 参数 token)
13
+ * - 心跳保活 & 死连接检测
14
+ * - 事件广播 / 单播
15
+ */
16
+ export declare class WsBridge {
17
+ private wss;
18
+ private token;
19
+ private heartbeatTimer;
20
+ private clientEventCallbacks;
21
+ private connectionCallbacks;
22
+ private disconnectCallbacks;
23
+ /** 每个连接的最后一次 pong 时间 */
24
+ private lastPongMap;
25
+ constructor(options: WsBridgeOptions);
26
+ /**
27
+ * 异步工厂方法:等待端口监听成功后 resolve,端口占用等错误时 reject。
28
+ * 使用此方法代替 new WsBridge(),确保 EADDRINUSE 等错误能被调用方的 try-catch 捕获。
29
+ */
30
+ static create(options: WsBridgeOptions): Promise<WsBridge>;
31
+ /** 注册客户端事件回调 */
32
+ onClientEvent(callback: (event: ClientEvent, ws: WebSocket) => void): void;
33
+ /** 注册新连接回调(用于自动推送初始数据) */
34
+ onConnection(callback: (ws: WebSocket) => void): void;
35
+ /** 注册断开连接回调(任意客户端断开时触发,可通过 getConnectionCount() 判断是否全部断开) */
36
+ onDisconnect(callback: () => void): void;
37
+ /** 广播事件到所有已连接的客户端 */
38
+ broadcast(event: ServerEvent): void;
39
+ /** 发送事件到指定客户端 */
40
+ send(ws: WebSocket, event: ServerEvent): void;
41
+ /** 获取当前活跃连接数 */
42
+ getConnectionCount(): number;
43
+ /** 优雅关闭 WebSocket 服务 */
44
+ close(): Promise<void>;
45
+ /** 验证连接 token(token 为空字符串时跳过验证) */
46
+ private verifyToken;
47
+ /** 处理新的 WebSocket 连接 */
48
+ private handleConnection;
49
+ /** 分发客户端事件到所有注册的回调 */
50
+ private dispatchClientEvent;
51
+ /** 启动心跳机制 */
52
+ private startHeartbeat;
53
+ }
54
+ export {};
55
+ //# sourceMappingURL=WsBridge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"WsBridge.d.ts","sourceRoot":"","sources":["../../src/ws/WsBridge.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmB,SAAS,EAAE,MAAM,IAAI,CAAA;AAE/C,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAE9D,kBAAkB;AAClB,UAAU,eAAe;IACvB,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;CACd;AAED;;;;;;;GAOG;AACH,qBAAa,QAAQ;IACnB,OAAO,CAAC,GAAG,CAAiB;IAC5B,OAAO,CAAC,KAAK,CAAQ;IACrB,OAAO,CAAC,cAAc,CAA8B;IACpD,OAAO,CAAC,oBAAoB,CAAyD;IACrF,OAAO,CAAC,mBAAmB,CAAqC;IAChE,OAAO,CAAC,mBAAmB,CAAwB;IAEnD,wBAAwB;IACxB,OAAO,CAAC,WAAW,CAA+B;gBAEtC,OAAO,EAAE,eAAe;IAuBpC;;;OAGG;WACU,MAAM,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,QAAQ,CAAC;IAgBhE,gBAAgB;IAChB,aAAa,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,WAAW,EAAE,EAAE,EAAE,SAAS,KAAK,IAAI,GAAG,IAAI;IAI1E,0BAA0B;IAC1B,YAAY,CAAC,QAAQ,EAAE,CAAC,EAAE,EAAE,SAAS,KAAK,IAAI,GAAG,IAAI;IAIrD,6DAA6D;IAC7D,YAAY,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,IAAI;IAIxC,qBAAqB;IACrB,SAAS,CAAC,KAAK,EAAE,WAAW,GAAG,IAAI;IASnC,iBAAiB;IACjB,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE,WAAW,GAAG,IAAI;IAM7C,gBAAgB;IAChB,kBAAkB,IAAI,MAAM;IAI5B,wBAAwB;IACxB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IA4BtB,mCAAmC;IACnC,OAAO,CAAC,WAAW;IAYnB,wBAAwB;IACxB,OAAO,CAAC,gBAAgB;IAqDxB,sBAAsB;IACtB,OAAO,CAAC,mBAAmB;IAU3B,aAAa;IACb,OAAO,CAAC,cAAc;CA0BvB"}
@@ -0,0 +1,220 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WsBridge = void 0;
4
+ const ws_1 = require("ws");
5
+ /**
6
+ * WebSocket 桥接服务
7
+ *
8
+ * 负责与手机端建立 WebSocket 连接,完成事件的收发。
9
+ * - 连接鉴权(URL 参数 token)
10
+ * - 心跳保活 & 死连接检测
11
+ * - 事件广播 / 单播
12
+ */
13
+ class WsBridge {
14
+ wss;
15
+ token;
16
+ heartbeatTimer = null;
17
+ clientEventCallbacks = [];
18
+ connectionCallbacks = [];
19
+ disconnectCallbacks = [];
20
+ /** 每个连接的最后一次 pong 时间 */
21
+ lastPongMap = new Map();
22
+ constructor(options) {
23
+ this.token = options.token;
24
+ this.wss = new ws_1.WebSocketServer({
25
+ port: options.port,
26
+ verifyClient: (info, callback) => {
27
+ const authorized = this.verifyToken(info.req);
28
+ if (!authorized) {
29
+ callback(false, 401, 'Unauthorized');
30
+ }
31
+ else {
32
+ callback(true);
33
+ }
34
+ },
35
+ });
36
+ this.wss.on('connection', (ws) => this.handleConnection(ws));
37
+ // 启动心跳定时器
38
+ this.startHeartbeat();
39
+ console.log(`[WsBridge] WebSocket 服务已启动,端口 ${options.port}`);
40
+ }
41
+ /**
42
+ * 异步工厂方法:等待端口监听成功后 resolve,端口占用等错误时 reject。
43
+ * 使用此方法代替 new WsBridge(),确保 EADDRINUSE 等错误能被调用方的 try-catch 捕获。
44
+ */
45
+ static async create(options) {
46
+ return new Promise((resolve, reject) => {
47
+ const bridge = new WsBridge(options);
48
+ bridge.wss.once('listening', () => {
49
+ // 启动成功后注册持久错误处理器,防止运行时错误变成 Uncaught Exception
50
+ bridge.wss.on('error', (err) => console.error('[WsBridge] 服务运行错误:', err));
51
+ resolve(bridge);
52
+ });
53
+ bridge.wss.once('error', reject);
54
+ });
55
+ }
56
+ // ============================================
57
+ // 公开 API
58
+ // ============================================
59
+ /** 注册客户端事件回调 */
60
+ onClientEvent(callback) {
61
+ this.clientEventCallbacks.push(callback);
62
+ }
63
+ /** 注册新连接回调(用于自动推送初始数据) */
64
+ onConnection(callback) {
65
+ this.connectionCallbacks.push(callback);
66
+ }
67
+ /** 注册断开连接回调(任意客户端断开时触发,可通过 getConnectionCount() 判断是否全部断开) */
68
+ onDisconnect(callback) {
69
+ this.disconnectCallbacks.push(callback);
70
+ }
71
+ /** 广播事件到所有已连接的客户端 */
72
+ broadcast(event) {
73
+ const data = JSON.stringify(event);
74
+ for (const ws of this.wss.clients) {
75
+ if (ws.readyState === ws_1.WebSocket.OPEN) {
76
+ ws.send(data);
77
+ }
78
+ }
79
+ }
80
+ /** 发送事件到指定客户端 */
81
+ send(ws, event) {
82
+ if (ws.readyState === ws_1.WebSocket.OPEN) {
83
+ ws.send(JSON.stringify(event));
84
+ }
85
+ }
86
+ /** 获取当前活跃连接数 */
87
+ getConnectionCount() {
88
+ return this.wss.clients.size;
89
+ }
90
+ /** 优雅关闭 WebSocket 服务 */
91
+ close() {
92
+ return new Promise((resolve, reject) => {
93
+ // 停止心跳
94
+ if (this.heartbeatTimer) {
95
+ clearInterval(this.heartbeatTimer);
96
+ this.heartbeatTimer = null;
97
+ }
98
+ // 关闭所有连接
99
+ for (const ws of this.wss.clients) {
100
+ ws.terminate();
101
+ }
102
+ this.wss.close((err) => {
103
+ if (err) {
104
+ reject(err);
105
+ }
106
+ else {
107
+ console.log('[WsBridge] WebSocket 服务已关闭');
108
+ resolve();
109
+ }
110
+ });
111
+ });
112
+ }
113
+ // ============================================
114
+ // 内部方法
115
+ // ============================================
116
+ /** 验证连接 token(token 为空字符串时跳过验证) */
117
+ verifyToken(req) {
118
+ // 空 token = 开发模式,跳过验证
119
+ if (this.token === '')
120
+ return true;
121
+ try {
122
+ const url = new URL(req.url ?? '', `http://${req.headers.host}`);
123
+ const clientToken = url.searchParams.get('token');
124
+ return clientToken === this.token;
125
+ }
126
+ catch {
127
+ return false;
128
+ }
129
+ }
130
+ /** 处理新的 WebSocket 连接 */
131
+ handleConnection(ws) {
132
+ // 记录初始 pong 时间(连接建立即视为活跃)
133
+ this.lastPongMap.set(ws, Date.now());
134
+ console.log(`[WsBridge] 新客户端连接,当前连接数: ${this.getConnectionCount()}`);
135
+ // 触发连接回调(自动推送初始数据)
136
+ for (const callback of this.connectionCallbacks) {
137
+ try {
138
+ callback(ws);
139
+ }
140
+ catch (err) {
141
+ console.error('[WsBridge] 连接回调执行异常:', err);
142
+ }
143
+ }
144
+ // 监听 pong 回应
145
+ ws.on('pong', () => {
146
+ this.lastPongMap.set(ws, Date.now());
147
+ });
148
+ // 监听消息
149
+ ws.on('message', (raw) => {
150
+ try {
151
+ const event = JSON.parse(raw.toString());
152
+ this.dispatchClientEvent(event, ws);
153
+ }
154
+ catch (err) {
155
+ console.error('[WsBridge] 消息解析失败:', err);
156
+ this.send(ws, {
157
+ type: 'error',
158
+ message: '消息格式无效',
159
+ code: 'INVALID_MESSAGE',
160
+ });
161
+ }
162
+ });
163
+ // 监听关闭
164
+ // 注意:close 事件时 ws 可能尚未从 wss.clients 移除,延迟到下一 tick 获取准确的连接数
165
+ ws.on('close', () => {
166
+ this.lastPongMap.delete(ws);
167
+ setTimeout(() => {
168
+ console.log(`[WsBridge] 客户端断开,当前连接数: ${this.getConnectionCount()}`);
169
+ for (const cb of this.disconnectCallbacks) {
170
+ try {
171
+ cb();
172
+ }
173
+ catch (err) {
174
+ console.error('[WsBridge] 断开回调执行异常:', err);
175
+ }
176
+ }
177
+ }, 0);
178
+ });
179
+ // 监听错误
180
+ ws.on('error', (err) => {
181
+ console.error('[WsBridge] 连接错误:', err.message);
182
+ });
183
+ }
184
+ /** 分发客户端事件到所有注册的回调 */
185
+ dispatchClientEvent(event, ws) {
186
+ for (const callback of this.clientEventCallbacks) {
187
+ try {
188
+ callback(event, ws);
189
+ }
190
+ catch (err) {
191
+ console.error('[WsBridge] 事件回调执行异常:', err);
192
+ }
193
+ }
194
+ }
195
+ /** 启动心跳机制 */
196
+ startHeartbeat() {
197
+ // 每 30 秒发送一次心跳
198
+ this.heartbeatTimer = setInterval(() => {
199
+ const now = Date.now();
200
+ // 广播心跳事件给所有客户端
201
+ this.broadcast({ type: 'heartbeat', timestamp: now });
202
+ // 检查每个连接是否存活
203
+ for (const ws of this.wss.clients) {
204
+ const lastPong = this.lastPongMap.get(ws) ?? 0;
205
+ // 超过 60 秒未收到 pong,视为死连接
206
+ if (now - lastPong > 60_000) {
207
+ console.log('[WsBridge] 检测到死连接,主动断开');
208
+ ws.terminate();
209
+ continue;
210
+ }
211
+ // 发送 ping 帧
212
+ if (ws.readyState === ws_1.WebSocket.OPEN) {
213
+ ws.ping();
214
+ }
215
+ }
216
+ }, 30_000);
217
+ }
218
+ }
219
+ exports.WsBridge = WsBridge;
220
+ //# sourceMappingURL=WsBridge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"WsBridge.js","sourceRoot":"","sources":["../../src/ws/WsBridge.ts"],"names":[],"mappings":";;;AAAA,2BAA+C;AAU/C;;;;;;;GAOG;AACH,MAAa,QAAQ;IACX,GAAG,CAAiB;IACpB,KAAK,CAAQ;IACb,cAAc,GAA0B,IAAI,CAAA;IAC5C,oBAAoB,GAAuD,EAAE,CAAA;IAC7E,mBAAmB,GAAmC,EAAE,CAAA;IACxD,mBAAmB,GAAsB,EAAE,CAAA;IAEnD,wBAAwB;IAChB,WAAW,GAAG,IAAI,GAAG,EAAqB,CAAA;IAElD,YAAY,OAAwB;QAClC,IAAI,CAAC,KAAK,GAAG,OAAO,CAAC,KAAK,CAAA;QAE1B,IAAI,CAAC,GAAG,GAAG,IAAI,oBAAe,CAAC;YAC7B,IAAI,EAAE,OAAO,CAAC,IAAI;YAClB,YAAY,EAAE,CAAC,IAAI,EAAE,QAAQ,EAAE,EAAE;gBAC/B,MAAM,UAAU,GAAG,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;gBAC7C,IAAI,CAAC,UAAU,EAAE,CAAC;oBAChB,QAAQ,CAAC,KAAK,EAAE,GAAG,EAAE,cAAc,CAAC,CAAA;gBACtC,CAAC;qBAAM,CAAC;oBACN,QAAQ,CAAC,IAAI,CAAC,CAAA;gBAChB,CAAC;YACH,CAAC;SACF,CAAC,CAAA;QAEF,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,YAAY,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,IAAI,CAAC,gBAAgB,CAAC,EAAE,CAAC,CAAC,CAAA;QAE5D,UAAU;QACV,IAAI,CAAC,cAAc,EAAE,CAAA;QAErB,OAAO,CAAC,GAAG,CAAC,iCAAiC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAA;IAC9D,CAAC;IAED;;;OAGG;IACH,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,OAAwB;QAC1C,OAAO,IAAI,OAAO,CAAW,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YAC/C,MAAM,MAAM,GAAG,IAAI,QAAQ,CAAC,OAAO,CAAC,CAAA;YACpC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,WAAW,EAAE,GAAG,EAAE;gBAChC,8CAA8C;gBAC9C,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,oBAAoB,EAAE,GAAG,CAAC,CAAC,CAAA;gBACzE,OAAO,CAAC,MAAM,CAAC,CAAA;YACjB,CAAC,CAAC,CAAA;YACF,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,CAAA;QAClC,CAAC,CAAC,CAAA;IACJ,CAAC;IAED,+CAA+C;IAC/C,SAAS;IACT,+CAA+C;IAE/C,gBAAgB;IAChB,aAAa,CAAC,QAAqD;QACjE,IAAI,CAAC,oBAAoB,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;IAC1C,CAAC;IAED,0BAA0B;IAC1B,YAAY,CAAC,QAAiC;QAC5C,IAAI,CAAC,mBAAmB,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;IACzC,CAAC;IAED,6DAA6D;IAC7D,YAAY,CAAC,QAAoB;QAC/B,IAAI,CAAC,mBAAmB,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;IACzC,CAAC;IAED,qBAAqB;IACrB,SAAS,CAAC,KAAkB;QAC1B,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAA;QAClC,KAAK,MAAM,EAAE,IAAI,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC;YAClC,IAAI,EAAE,CAAC,UAAU,KAAK,cAAS,CAAC,IAAI,EAAE,CAAC;gBACrC,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;YACf,CAAC;QACH,CAAC;IACH,CAAC;IAED,iBAAiB;IACjB,IAAI,CAAC,EAAa,EAAE,KAAkB;QACpC,IAAI,EAAE,CAAC,UAAU,KAAK,cAAS,CAAC,IAAI,EAAE,CAAC;YACrC,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAA;QAChC,CAAC;IACH,CAAC;IAED,gBAAgB;IAChB,kBAAkB;QAChB,OAAO,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,CAAA;IAC9B,CAAC;IAED,wBAAwB;IACxB,KAAK;QACH,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YACrC,OAAO;YACP,IAAI,IAAI,CAAC,cAAc,EAAE,CAAC;gBACxB,aAAa,CAAC,IAAI,CAAC,cAAc,CAAC,CAAA;gBAClC,IAAI,CAAC,cAAc,GAAG,IAAI,CAAA;YAC5B,CAAC;YAED,SAAS;YACT,KAAK,MAAM,EAAE,IAAI,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC;gBAClC,EAAE,CAAC,SAAS,EAAE,CAAA;YAChB,CAAC;YAED,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;gBACrB,IAAI,GAAG,EAAE,CAAC;oBACR,MAAM,CAAC,GAAG,CAAC,CAAA;gBACb,CAAC;qBAAM,CAAC;oBACN,OAAO,CAAC,GAAG,CAAC,4BAA4B,CAAC,CAAA;oBACzC,OAAO,EAAE,CAAA;gBACX,CAAC;YACH,CAAC,CAAC,CAAA;QACJ,CAAC,CAAC,CAAA;IACJ,CAAC;IAED,+CAA+C;IAC/C,OAAO;IACP,+CAA+C;IAE/C,mCAAmC;IAC3B,WAAW,CAAC,GAAoB;QACtC,sBAAsB;QACtB,IAAI,IAAI,CAAC,KAAK,KAAK,EAAE;YAAE,OAAO,IAAI,CAAA;QAClC,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,GAAG,IAAI,EAAE,EAAE,UAAU,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAA;YAChE,MAAM,WAAW,GAAG,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;YACjD,OAAO,WAAW,KAAK,IAAI,CAAC,KAAK,CAAA;QACnC,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,KAAK,CAAA;QACd,CAAC;IACH,CAAC;IAED,wBAAwB;IAChB,gBAAgB,CAAC,EAAa;QACpC,0BAA0B;QAC1B,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,EAAE,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC,CAAA;QAEpC,OAAO,CAAC,GAAG,CAAC,4BAA4B,IAAI,CAAC,kBAAkB,EAAE,EAAE,CAAC,CAAA;QAEpE,mBAAmB;QACnB,KAAK,MAAM,QAAQ,IAAI,IAAI,CAAC,mBAAmB,EAAE,CAAC;YAChD,IAAI,CAAC;gBACH,QAAQ,CAAC,EAAE,CAAC,CAAA;YACd,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,OAAO,CAAC,KAAK,CAAC,sBAAsB,EAAE,GAAG,CAAC,CAAA;YAC5C,CAAC;QACH,CAAC;QAED,aAAa;QACb,EAAE,CAAC,EAAE,CAAC,MAAM,EAAE,GAAG,EAAE;YACjB,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,EAAE,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC,CAAA;QACtC,CAAC,CAAC,CAAA;QAEF,OAAO;QACP,EAAE,CAAC,EAAE,CAAC,SAAS,EAAE,CAAC,GAAG,EAAE,EAAE;YACvB,IAAI,CAAC;gBACH,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAgB,CAAA;gBACvD,IAAI,CAAC,mBAAmB,CAAC,KAAK,EAAE,EAAE,CAAC,CAAA;YACrC,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,OAAO,CAAC,KAAK,CAAC,oBAAoB,EAAE,GAAG,CAAC,CAAA;gBACxC,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE;oBACZ,IAAI,EAAE,OAAO;oBACb,OAAO,EAAE,QAAQ;oBACjB,IAAI,EAAE,iBAAiB;iBACxB,CAAC,CAAA;YACJ,CAAC;QACH,CAAC,CAAC,CAAA;QAEF,OAAO;QACP,2DAA2D;QAC3D,EAAE,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE;YAClB,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,EAAE,CAAC,CAAA;YAC3B,UAAU,CAAC,GAAG,EAAE;gBACd,OAAO,CAAC,GAAG,CAAC,2BAA2B,IAAI,CAAC,kBAAkB,EAAE,EAAE,CAAC,CAAA;gBACnE,KAAK,MAAM,EAAE,IAAI,IAAI,CAAC,mBAAmB,EAAE,CAAC;oBAC1C,IAAI,CAAC;wBAAC,EAAE,EAAE,CAAA;oBAAC,CAAC;oBAAC,OAAO,GAAG,EAAE,CAAC;wBAAC,OAAO,CAAC,KAAK,CAAC,sBAAsB,EAAE,GAAG,CAAC,CAAA;oBAAC,CAAC;gBACzE,CAAC;YACH,CAAC,EAAE,CAAC,CAAC,CAAA;QACP,CAAC,CAAC,CAAA;QAEF,OAAO;QACP,EAAE,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE;YACrB,OAAO,CAAC,KAAK,CAAC,kBAAkB,EAAE,GAAG,CAAC,OAAO,CAAC,CAAA;QAChD,CAAC,CAAC,CAAA;IACJ,CAAC;IAED,sBAAsB;IACd,mBAAmB,CAAC,KAAkB,EAAE,EAAa;QAC3D,KAAK,MAAM,QAAQ,IAAI,IAAI,CAAC,oBAAoB,EAAE,CAAC;YACjD,IAAI,CAAC;gBACH,QAAQ,CAAC,KAAK,EAAE,EAAE,CAAC,CAAA;YACrB,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,OAAO,CAAC,KAAK,CAAC,sBAAsB,EAAE,GAAG,CAAC,CAAA;YAC5C,CAAC;QACH,CAAC;IACH,CAAC;IAED,aAAa;IACL,cAAc;QACpB,eAAe;QACf,IAAI,CAAC,cAAc,GAAG,WAAW,CAAC,GAAG,EAAE;YACrC,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAA;YAEtB,eAAe;YACf,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC,CAAA;YAErD,aAAa;YACb,KAAK,MAAM,EAAE,IAAI,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC;gBAClC,MAAM,QAAQ,GAAG,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,CAAA;gBAE9C,wBAAwB;gBACxB,IAAI,GAAG,GAAG,QAAQ,GAAG,MAAM,EAAE,CAAC;oBAC5B,OAAO,CAAC,GAAG,CAAC,wBAAwB,CAAC,CAAA;oBACrC,EAAE,CAAC,SAAS,EAAE,CAAA;oBACd,SAAQ;gBACV,CAAC;gBAED,YAAY;gBACZ,IAAI,EAAE,CAAC,UAAU,KAAK,cAAS,CAAC,IAAI,EAAE,CAAC;oBACrC,EAAE,CAAC,IAAI,EAAE,CAAA;gBACX,CAAC;YACH,CAAC;QACH,CAAC,EAAE,MAAM,CAAC,CAAA;IACZ,CAAC;CACF;AAjOD,4BAiOC"}