opencode-oncall 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 (105) hide show
  1. package/LICENSE +151 -0
  2. package/README.md +50 -0
  3. package/dist/common-settings-actions.d.ts +15 -0
  4. package/dist/common-settings-actions.js +48 -0
  5. package/dist/common-settings-store.d.ts +1 -0
  6. package/dist/common-settings-store.js +1 -0
  7. package/dist/index.d.ts +1 -0
  8. package/dist/index.js +1 -0
  9. package/dist/plugin-hooks.d.ts +51 -0
  10. package/dist/plugin-hooks.js +288 -0
  11. package/dist/plugin.d.ts +10 -0
  12. package/dist/plugin.js +115 -0
  13. package/dist/settings-store.d.ts +50 -0
  14. package/dist/settings-store.js +214 -0
  15. package/dist/store-paths.d.ts +16 -0
  16. package/dist/store-paths.js +61 -0
  17. package/dist/ui/wechat-menu.d.ts +26 -0
  18. package/dist/ui/wechat-menu.js +90 -0
  19. package/dist/wechat/bind-flow.d.ts +29 -0
  20. package/dist/wechat/bind-flow.js +207 -0
  21. package/dist/wechat/bridge.d.ts +136 -0
  22. package/dist/wechat/bridge.js +1059 -0
  23. package/dist/wechat/broker-client.d.ts +23 -0
  24. package/dist/wechat/broker-client.js +274 -0
  25. package/dist/wechat/broker-endpoint.d.ts +21 -0
  26. package/dist/wechat/broker-endpoint.js +78 -0
  27. package/dist/wechat/broker-entry.d.ts +123 -0
  28. package/dist/wechat/broker-entry.js +1321 -0
  29. package/dist/wechat/broker-launcher.d.ts +37 -0
  30. package/dist/wechat/broker-launcher.js +418 -0
  31. package/dist/wechat/broker-mutation-queue.d.ts +93 -0
  32. package/dist/wechat/broker-mutation-queue.js +126 -0
  33. package/dist/wechat/broker-server.d.ts +86 -0
  34. package/dist/wechat/broker-server.js +1340 -0
  35. package/dist/wechat/broker-state-store.d.ts +335 -0
  36. package/dist/wechat/broker-state-store.js +1964 -0
  37. package/dist/wechat/command-parser.d.ts +18 -0
  38. package/dist/wechat/command-parser.js +58 -0
  39. package/dist/wechat/compat/jiti-loader.d.ts +27 -0
  40. package/dist/wechat/compat/jiti-loader.js +118 -0
  41. package/dist/wechat/compat/openclaw-account-helpers.d.ts +29 -0
  42. package/dist/wechat/compat/openclaw-account-helpers.js +60 -0
  43. package/dist/wechat/compat/openclaw-bind-helpers.d.ts +29 -0
  44. package/dist/wechat/compat/openclaw-bind-helpers.js +169 -0
  45. package/dist/wechat/compat/openclaw-guided-smoke.d.ts +180 -0
  46. package/dist/wechat/compat/openclaw-guided-smoke.js +1134 -0
  47. package/dist/wechat/compat/openclaw-public-entry.d.ts +33 -0
  48. package/dist/wechat/compat/openclaw-public-entry.js +62 -0
  49. package/dist/wechat/compat/openclaw-public-helpers.d.ts +70 -0
  50. package/dist/wechat/compat/openclaw-public-helpers.js +68 -0
  51. package/dist/wechat/compat/openclaw-qr-gateway.d.ts +15 -0
  52. package/dist/wechat/compat/openclaw-qr-gateway.js +39 -0
  53. package/dist/wechat/compat/openclaw-smoke.d.ts +48 -0
  54. package/dist/wechat/compat/openclaw-smoke.js +100 -0
  55. package/dist/wechat/compat/openclaw-sync-buf.d.ts +24 -0
  56. package/dist/wechat/compat/openclaw-sync-buf.js +80 -0
  57. package/dist/wechat/compat/openclaw-updates-send.d.ts +47 -0
  58. package/dist/wechat/compat/openclaw-updates-send.js +38 -0
  59. package/dist/wechat/compat/qrcode-terminal-loader.d.ts +12 -0
  60. package/dist/wechat/compat/qrcode-terminal-loader.js +16 -0
  61. package/dist/wechat/compat/slash-guard.d.ts +11 -0
  62. package/dist/wechat/compat/slash-guard.js +24 -0
  63. package/dist/wechat/dead-letter-store.d.ts +48 -0
  64. package/dist/wechat/dead-letter-store.js +224 -0
  65. package/dist/wechat/debug-bundle-collector.d.ts +49 -0
  66. package/dist/wechat/debug-bundle-collector.js +580 -0
  67. package/dist/wechat/debug-bundle-flow.d.ts +37 -0
  68. package/dist/wechat/debug-bundle-flow.js +180 -0
  69. package/dist/wechat/debug-bundle-redaction.d.ts +14 -0
  70. package/dist/wechat/debug-bundle-redaction.js +339 -0
  71. package/dist/wechat/handle.d.ts +10 -0
  72. package/dist/wechat/handle.js +57 -0
  73. package/dist/wechat/ipc-auth.d.ts +6 -0
  74. package/dist/wechat/ipc-auth.js +39 -0
  75. package/dist/wechat/latest-account-state-store.d.ts +8 -0
  76. package/dist/wechat/latest-account-state-store.js +38 -0
  77. package/dist/wechat/notification-dispatcher.d.ts +34 -0
  78. package/dist/wechat/notification-dispatcher.js +266 -0
  79. package/dist/wechat/notification-format.d.ts +15 -0
  80. package/dist/wechat/notification-format.js +196 -0
  81. package/dist/wechat/notification-store.d.ts +72 -0
  82. package/dist/wechat/notification-store.js +807 -0
  83. package/dist/wechat/notification-types.d.ts +37 -0
  84. package/dist/wechat/notification-types.js +1 -0
  85. package/dist/wechat/openclaw-account-adapter.d.ts +30 -0
  86. package/dist/wechat/openclaw-account-adapter.js +60 -0
  87. package/dist/wechat/operator-store.d.ts +9 -0
  88. package/dist/wechat/operator-store.js +69 -0
  89. package/dist/wechat/protocol.d.ts +150 -0
  90. package/dist/wechat/protocol.js +197 -0
  91. package/dist/wechat/question-interaction.d.ts +24 -0
  92. package/dist/wechat/question-interaction.js +180 -0
  93. package/dist/wechat/request-store.d.ts +108 -0
  94. package/dist/wechat/request-store.js +669 -0
  95. package/dist/wechat/session-digest.d.ts +50 -0
  96. package/dist/wechat/session-digest.js +167 -0
  97. package/dist/wechat/state-paths.d.ts +26 -0
  98. package/dist/wechat/state-paths.js +92 -0
  99. package/dist/wechat/status-format.d.ts +26 -0
  100. package/dist/wechat/status-format.js +616 -0
  101. package/dist/wechat/token-store.d.ts +20 -0
  102. package/dist/wechat/token-store.js +193 -0
  103. package/dist/wechat/wechat-status-runtime.d.ts +89 -0
  104. package/dist/wechat/wechat-status-runtime.js +518 -0
  105. package/package.json +74 -0
@@ -0,0 +1,1340 @@
1
+ import { appendFile, chmod, mkdir, rm, stat } from "node:fs/promises";
2
+ import net from "node:net";
3
+ import path from "node:path";
4
+ import { createBrokerSocket, isTcpBrokerEndpoint, listenOnBrokerEndpoint, } from "./broker-endpoint.js";
5
+ import { createBrokerMutationQueue } from "./broker-mutation-queue.js";
6
+ import { applyBridgeEvent as applyBrokerStateEvent, cleanupBrokerRuntimeTerminalRequests, clearBrokerActiveScope, closeBrokerNaturalStopsForScope, createEmptyBrokerState, expireBrokerIndexedRequestsForScope, listTimedOutBrokerConnectionScopes, loadBrokerStateStoreSnapshot, markBrokerConnectionObserved, markBrokerConnectionOffline, markBrokerFullSyncCompleted, markBrokerReplayCompleted, markConnectionAckedEventSeq as markBrokerStateAckedEventSeq, markConnectionSentBrokerSeq as markBrokerStateSentBrokerSeq, persistBrokerStateStoreSnapshot, readBrokerAuthoritativeView, readBrokerControlRecord, reconcileBrokerActiveRequestHandlesWithVisibleNotifications, reconcileBrokerDisconnectedScopes, removeBrokerConnectionScope, requestBrokerFullSync, requestBrokerReplay, setBrokerStateMutationTarget, stageBrokerFullSyncEvent, upsertBrokerCommand as upsertBrokerStateCommand, upsertRetryErrorSummary, } from "./broker-state-store.js";
7
+ import { purgeDeadLettersBefore } from "./dead-letter-store.js";
8
+ import { createHandle } from "./handle.js";
9
+ import { upsertNotification } from "./notification-store.js";
10
+ import { readOperatorBinding } from "./operator-store.js";
11
+ import { createBridgeEventEnvelope, createBrokerAckEnvelope, createBrokerCommandEnvelope, createBrokerControlEnvelope, createErrorEnvelope, createHelloRegisterEnvelope, createRegisterAckEnvelope, parseEnvelopeLine, serializeEnvelope, } from "./protocol.js";
12
+ import { WECHAT_DIR_MODE, WECHAT_FILE_MODE, wechatBrokerDiagnosticsPath, } from "./state-paths.js";
13
+ import { buildAggregatedStatusInstancesFromBrokerView, formatAggregatedStatusReplyFromBrokerView, formatTodoReplyFromBrokerView, } from "./status-format.js";
14
+ import { markTokenStale, NOTIFICATION_DELIVERY_FAILED_STALE_REASON, } from "./token-store.js";
15
+ const FUTURE_MESSAGE_TYPES = new Set([
16
+ "replyQuestion",
17
+ "rejectQuestion",
18
+ "replyPermission",
19
+ ]);
20
+ export const DEFAULT_HEARTBEAT_TIMEOUT_MS = 30_000;
21
+ const DEFAULT_HEARTBEAT_SCAN_INTERVAL_MS = 1_000;
22
+ export const DEFAULT_STATUS_COLLECT_WINDOW_MS = 5_000;
23
+ const DEFAULT_REQUEST_CLEAN_AFTER_MS = 5 * 60 * 1000;
24
+ const DEFAULT_REQUEST_PURGE_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
25
+ const DEFAULT_REQUEST_CLEANUP_SCAN_INTERVAL_MS = 60_000;
26
+ const DEFAULT_DEAD_LETTER_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
27
+ const DEFAULT_DEAD_LETTER_SCAN_INTERVAL_MS = 60_000;
28
+ const DELIVERY_FAILURE_ACTION = "在微信发送 /status 重新激活";
29
+ const DELIVERY_FAILURE_SUMMARY = "微信通知发送失败,当前微信会话可能已失效";
30
+ const DELIVERY_FAILURE_SEVERITY = "建议尽快人工查看";
31
+ export const WECHAT_BROKER_WS_PROTOCOL_VERSION = 2;
32
+ export const WECHAT_BROKER_WS_STATE_GENERATION = "wechat-ws-v1";
33
+ const LEGACY_REMOVED_MESSAGE_TYPES = new Set([
34
+ "registerInstance",
35
+ "heartbeat",
36
+ "statusSnapshot",
37
+ "syncWechatNotifications",
38
+ "replyQuestionResult",
39
+ "replyPermissionResult",
40
+ "replyNaturalStopResult",
41
+ "showFallbackToast",
42
+ "collectStatus",
43
+ ]);
44
+ function isNonEmptyString(value) {
45
+ return typeof value === "string" && value.trim().length > 0;
46
+ }
47
+ function cloneWsValue(value) {
48
+ if (Array.isArray(value)) {
49
+ return [...value];
50
+ }
51
+ if (typeof value === "object" && value !== null) {
52
+ return { ...value };
53
+ }
54
+ return value;
55
+ }
56
+ function readRecordInstanceID(record) {
57
+ if (isNonEmptyString(record.instanceID)) {
58
+ return record.instanceID;
59
+ }
60
+ const value = record.target.instanceID;
61
+ return isNonEmptyString(value) ? value : undefined;
62
+ }
63
+ function getStateMaxBrokerSeq(state) {
64
+ let maxBrokerSeq = 0;
65
+ for (const incarnations of Object.values(state.connections)) {
66
+ for (const connection of Object.values(incarnations)) {
67
+ maxBrokerSeq = Math.max(maxBrokerSeq, connection.lastSentBrokerSeq);
68
+ }
69
+ }
70
+ for (const command of Object.values(state.commandLedger)) {
71
+ maxBrokerSeq = Math.max(maxBrokerSeq, command.brokerSeq);
72
+ }
73
+ for (const control of Object.values(state.controlLedger)) {
74
+ maxBrokerSeq = Math.max(maxBrokerSeq, control.brokerSeq);
75
+ }
76
+ return maxBrokerSeq;
77
+ }
78
+ function toBrokerWsCommand(record) {
79
+ return createBrokerCommandEnvelope({
80
+ brokerSeq: record.brokerSeq,
81
+ commandId: record.commandId,
82
+ type: record.type,
83
+ payload: cloneWsValue(record.payload ?? {}),
84
+ });
85
+ }
86
+ export function createBrokerWsCoordinator(input = {}) {
87
+ const protocolVersion = input.protocolVersion ?? WECHAT_BROKER_WS_PROTOCOL_VERSION;
88
+ const stateGeneration = input.stateGeneration ?? WECHAT_BROKER_WS_STATE_GENERATION;
89
+ const state = input.state ?? createEmptyBrokerState();
90
+ let nextBrokerSeq = getStateMaxBrokerSeq(state);
91
+ function allocateBrokerSeq() {
92
+ nextBrokerSeq += 1;
93
+ return nextBrokerSeq;
94
+ }
95
+ function registerBridge(hello) {
96
+ const accepted = hello.protocolVersion === protocolVersion &&
97
+ hello.stateGeneration === stateGeneration;
98
+ const connection = state.connections[hello.instanceID]?.[hello.instanceIncarnation];
99
+ const lastAckedEventSeq = connection?.lastAckedEventSeq ?? 0;
100
+ const lastSentEventSeq = hello.lastSentEventSeq ?? 0;
101
+ let control;
102
+ let needReplay = false;
103
+ let needFullSync = false;
104
+ if (!accepted || !connection) {
105
+ needFullSync = true;
106
+ const brokerSeq = allocateBrokerSeq();
107
+ const controlId = `ctl-full-sync-${brokerSeq}`;
108
+ requestBrokerFullSync(state, {
109
+ controlId,
110
+ brokerSeq,
111
+ instanceID: hello.instanceID,
112
+ instanceIncarnation: hello.instanceIncarnation,
113
+ reason: accepted ? "state-missing" : "protocol-mismatch",
114
+ });
115
+ control = createBrokerControlEnvelope({
116
+ brokerSeq,
117
+ controlId,
118
+ type: "requestFullSync",
119
+ payload: {
120
+ instanceID: hello.instanceID,
121
+ instanceIncarnation: hello.instanceIncarnation,
122
+ reason: accepted ? "state-missing" : "protocol-mismatch",
123
+ },
124
+ });
125
+ }
126
+ else if (lastSentEventSeq > lastAckedEventSeq) {
127
+ needReplay = true;
128
+ const brokerSeq = allocateBrokerSeq();
129
+ const controlId = `ctl-replay-${brokerSeq}`;
130
+ requestBrokerReplay(state, {
131
+ controlId,
132
+ brokerSeq,
133
+ instanceID: hello.instanceID,
134
+ instanceIncarnation: hello.instanceIncarnation,
135
+ fromEventSeq: lastAckedEventSeq + 1,
136
+ toEventSeq: lastSentEventSeq,
137
+ });
138
+ control = createBrokerControlEnvelope({
139
+ brokerSeq,
140
+ controlId,
141
+ type: "requestReplay",
142
+ payload: {
143
+ instanceID: hello.instanceID,
144
+ instanceIncarnation: hello.instanceIncarnation,
145
+ fromEventSeq: lastAckedEventSeq + 1,
146
+ toEventSeq: lastSentEventSeq,
147
+ },
148
+ });
149
+ }
150
+ const pendingCommands = accepted
151
+ ? Object.values(state.commandLedger)
152
+ .filter((record) => {
153
+ const recordInstanceID = readRecordInstanceID(record);
154
+ if (recordInstanceID !== hello.instanceID) {
155
+ return false;
156
+ }
157
+ if (record.instanceIncarnation &&
158
+ record.instanceIncarnation !== hello.instanceIncarnation) {
159
+ return false;
160
+ }
161
+ return record.status === "queued" || record.status === "delivered";
162
+ })
163
+ .sort((left, right) => left.brokerSeq - right.brokerSeq)
164
+ .map(toBrokerWsCommand)
165
+ : [];
166
+ return {
167
+ accepted,
168
+ ack: createRegisterAckEnvelope({
169
+ protocolVersion,
170
+ stateGeneration,
171
+ instanceIncarnation: hello.instanceIncarnation,
172
+ brokerSeq: nextBrokerSeq,
173
+ needReplay,
174
+ needFullSync,
175
+ }),
176
+ ...(control ? { control } : {}),
177
+ pendingCommands,
178
+ };
179
+ }
180
+ function handleBridgeEvent(event, context) {
181
+ const controlRecord = context.controlId
182
+ ? readBrokerControlRecord(state, context.controlId)
183
+ : undefined;
184
+ if (controlRecord?.type === "requestFullSync" &&
185
+ controlRecord.status === "inFlight") {
186
+ if (event.type === "fullSyncCompleted") {
187
+ markBrokerFullSyncCompleted(state, {
188
+ controlId: controlRecord.controlId,
189
+ instanceID: context.instanceID,
190
+ instanceIncarnation: event.instanceIncarnation,
191
+ eventSeq: event.eventSeq,
192
+ });
193
+ }
194
+ else {
195
+ stageBrokerFullSyncEvent(state, {
196
+ controlId: controlRecord.controlId,
197
+ event,
198
+ context: {
199
+ instanceID: context.instanceID,
200
+ },
201
+ });
202
+ }
203
+ }
204
+ else {
205
+ applyBrokerStateEvent(state, event, {
206
+ instanceID: context.instanceID,
207
+ });
208
+ if (controlRecord?.type === "requestReplay" &&
209
+ controlRecord.status === "inFlight" &&
210
+ event.eventSeq >= (controlRecord.toEventSeq ?? event.eventSeq)) {
211
+ markBrokerReplayCompleted(state, {
212
+ controlId: controlRecord.controlId,
213
+ completedEventSeq: event.eventSeq,
214
+ });
215
+ }
216
+ }
217
+ const ack = createBrokerAckEnvelope({
218
+ ackedEventSeq: event.eventSeq,
219
+ instanceIncarnation: event.instanceIncarnation,
220
+ });
221
+ markBrokerStateAckedEventSeq(state, {
222
+ instanceID: context.instanceID,
223
+ ...ack.payload,
224
+ });
225
+ return { ack };
226
+ }
227
+ function dispatchCommand(input) {
228
+ const current = state.commandLedger[input.commandId];
229
+ if (current &&
230
+ current.status !== "queued" &&
231
+ current.status !== "delivered") {
232
+ return null;
233
+ }
234
+ const brokerSeq = current?.brokerSeq ?? allocateBrokerSeq();
235
+ upsertBrokerStateCommand(state, {
236
+ commandId: input.commandId,
237
+ brokerSeq,
238
+ type: input.type,
239
+ status: "delivered",
240
+ target: { ...input.target },
241
+ payload: cloneWsValue(input.payload ?? {}),
242
+ instanceID: input.instanceID,
243
+ instanceIncarnation: input.instanceIncarnation,
244
+ });
245
+ markBrokerStateSentBrokerSeq(state, {
246
+ instanceID: input.instanceID,
247
+ instanceIncarnation: input.instanceIncarnation,
248
+ brokerSeq,
249
+ });
250
+ return createBrokerCommandEnvelope({
251
+ brokerSeq,
252
+ commandId: input.commandId,
253
+ type: input.type,
254
+ payload: cloneWsValue(input.payload ?? {}),
255
+ });
256
+ }
257
+ return {
258
+ getState: () => state,
259
+ registerBridge,
260
+ handleBridgeEvent,
261
+ dispatchCommand,
262
+ };
263
+ }
264
+ function getRequestId(envelope) {
265
+ return envelope.id;
266
+ }
267
+ function parseIncomingEnvelopeLine(line) {
268
+ try {
269
+ return parseEnvelopeLine(line);
270
+ }
271
+ catch {
272
+ if (typeof line !== "string" || line.length === 0 || !line.endsWith("\n")) {
273
+ throw new Error("invalid message line");
274
+ }
275
+ const body = line.slice(0, -1);
276
+ if (body.length === 0 || body.includes("\n") || body.includes("\r")) {
277
+ throw new Error("invalid message line");
278
+ }
279
+ const parsed = JSON.parse(body);
280
+ if (!isNonEmptyString(parsed.id) ||
281
+ !isNonEmptyString(parsed.type) ||
282
+ !Object.hasOwn(parsed, "payload")) {
283
+ throw new Error("invalid message line");
284
+ }
285
+ if (parsed.instanceID !== undefined &&
286
+ !isNonEmptyString(parsed.instanceID)) {
287
+ throw new Error("invalid message line");
288
+ }
289
+ if (parsed.sessionToken !== undefined &&
290
+ !isNonEmptyString(parsed.sessionToken)) {
291
+ throw new Error("invalid message line");
292
+ }
293
+ return parsed;
294
+ }
295
+ }
296
+ function writeEnvelope(socket, envelope) {
297
+ socket.write(serializeEnvelope(envelope));
298
+ }
299
+ function writeError(socket, code, message, requestId) {
300
+ writeEnvelope(socket, createErrorEnvelope(code, message, requestId));
301
+ }
302
+ const liveBridgeByInstanceID = new Map();
303
+ const liveBridgeBySocket = new Map();
304
+ const pendingWsReplyMutationsByCommandId = new Map();
305
+ let brokerMutationQueue = createBrokerMutationQueue();
306
+ let liveWsCoordinator = createBrokerWsCoordinator();
307
+ setBrokerStateMutationTarget(liveWsCoordinator.getState());
308
+ const LIVE_BRIDGE_EVENT_TYPES = new Set([
309
+ "instanceOnline",
310
+ "instanceOffline",
311
+ "sessionSnapshotChanged",
312
+ "questionOpened",
313
+ "questionUpdated",
314
+ "questionClosed",
315
+ "permissionOpened",
316
+ "permissionUpdated",
317
+ "permissionClosed",
318
+ "naturalStopOpened",
319
+ "naturalStopClosed",
320
+ "retryErrorUpdated",
321
+ "commandAccepted",
322
+ "commandResult",
323
+ "fullSyncCompleted",
324
+ ]);
325
+ function queueBrokerMutation(mutationType, task) {
326
+ return brokerMutationQueue.enqueue(mutationType, task);
327
+ }
328
+ async function appendBrokerDiagnostic(event) {
329
+ try {
330
+ await mkdir(path.dirname(wechatBrokerDiagnosticsPath()), {
331
+ recursive: true,
332
+ mode: WECHAT_DIR_MODE,
333
+ });
334
+ await appendFile(wechatBrokerDiagnosticsPath(), `${JSON.stringify({ at: Date.now(), ...event })}\n`, { encoding: "utf8", mode: WECHAT_FILE_MODE });
335
+ }
336
+ catch { }
337
+ }
338
+ function toIdempotencyPart(value) {
339
+ return (value
340
+ .trim()
341
+ .toLowerCase()
342
+ .replace(/[^a-z0-9]+/g, "-")
343
+ .replace(/^-+|-+$/g, "") || "unknown");
344
+ }
345
+ function readNotificationEventTimestamp(payload) {
346
+ return (readFiniteNumber(payload.createdAt) ??
347
+ readFiniteNumber(payload.updatedAt) ??
348
+ Date.now());
349
+ }
350
+ function readNaturalStopSessionID(payload) {
351
+ const directSessionID = readNonEmptyString(payload.sessionID);
352
+ if (directSessionID) {
353
+ return directSessionID;
354
+ }
355
+ return readNonEmptyString(asObject(payload.replyTarget).sessionID);
356
+ }
357
+ function buildNotificationIdempotencyKeyFromEvent(event, instanceID) {
358
+ const payload = asObject(event.payload);
359
+ if (isNonEmptyString(payload.idempotencyKey)) {
360
+ return payload.idempotencyKey;
361
+ }
362
+ if ((event.type === "questionOpened" || event.type === "questionUpdated") &&
363
+ isNonEmptyString(payload.requestID)) {
364
+ return `question-${toIdempotencyPart(instanceID)}-${toIdempotencyPart(payload.requestID)}`;
365
+ }
366
+ if ((event.type === "permissionOpened" || event.type === "permissionUpdated") &&
367
+ isNonEmptyString(payload.requestID)) {
368
+ return `permission-${toIdempotencyPart(instanceID)}-${toIdempotencyPart(payload.requestID)}`;
369
+ }
370
+ if (event.type === "naturalStopOpened") {
371
+ const sessionID = readNaturalStopSessionID(payload);
372
+ if (sessionID) {
373
+ return `natural-stop-${toIdempotencyPart(instanceID)}-${toIdempotencyPart(sessionID)}`;
374
+ }
375
+ }
376
+ if (event.type === "retryErrorUpdated") {
377
+ const retryIdentity = readNonEmptyString(payload.sessionID) ?? instanceID;
378
+ return `session-error-${toIdempotencyPart(instanceID)}-${toIdempotencyPart(retryIdentity)}`;
379
+ }
380
+ if ((event.type === "questionClosed" || event.type === "permissionClosed") &&
381
+ isNonEmptyString(payload.routeKey)) {
382
+ const requestKind = event.type === "questionClosed" ? "question" : "permission";
383
+ return `request-terminal-${requestKind}-${toIdempotencyPart(payload.routeKey)}`;
384
+ }
385
+ return undefined;
386
+ }
387
+ function resolveCanonicalRequestHandle(kind, routeKey, requestedHandle) {
388
+ const records = kind === "question"
389
+ ? liveWsCoordinator.getState().active.questions
390
+ : liveWsCoordinator.getState().active.permissions;
391
+ const current = asObject(records[routeKey]);
392
+ const currentHandle = readNonEmptyString(current.handle);
393
+ if (currentHandle) {
394
+ return currentHandle;
395
+ }
396
+ const existingHandles = Object.entries(records)
397
+ .filter(([key]) => key !== routeKey)
398
+ .map(([, record]) => readNonEmptyString(asObject(record).handle))
399
+ .filter((item) => typeof item === "string");
400
+ const requested = readNonEmptyString(requestedHandle);
401
+ const existing = new Set(existingHandles.map((item) => item.trim().toLowerCase()));
402
+ if (requested && !existing.has(requested.trim().toLowerCase())) {
403
+ return requested;
404
+ }
405
+ return createHandle(kind, existingHandles);
406
+ }
407
+ async function projectNotificationFromLiveBridgeEvent(event, instanceID) {
408
+ const binding = await readOperatorBinding().catch(() => undefined);
409
+ if (!binding) {
410
+ return;
411
+ }
412
+ const payload = asObject(event.payload);
413
+ const idempotencyKey = buildNotificationIdempotencyKeyFromEvent(event, instanceID);
414
+ if (!idempotencyKey) {
415
+ return;
416
+ }
417
+ if (event.type === "questionOpened" || event.type === "questionUpdated") {
418
+ const routeKey = readNonEmptyString(payload.routeKey);
419
+ const handle = routeKey
420
+ ? resolveCanonicalRequestHandle("question", routeKey, payload.handle)
421
+ : undefined;
422
+ if (!routeKey || !handle) {
423
+ return;
424
+ }
425
+ await upsertNotification({
426
+ idempotencyKey,
427
+ kind: "question",
428
+ wechatAccountId: binding.wechatAccountId,
429
+ userId: binding.userId,
430
+ routeKey,
431
+ handle,
432
+ ...(readNonEmptyString(payload.scopeKey)
433
+ ? { scopeKey: readNonEmptyString(payload.scopeKey) }
434
+ : { scopeKey: instanceID }),
435
+ ...(payload.prompt !== undefined && payload.prompt !== null
436
+ ? { prompt: payload.prompt }
437
+ : {}),
438
+ createdAt: readNotificationEventTimestamp(payload),
439
+ });
440
+ return;
441
+ }
442
+ if (event.type === "permissionOpened" || event.type === "permissionUpdated") {
443
+ const routeKey = readNonEmptyString(payload.routeKey);
444
+ const handle = routeKey
445
+ ? resolveCanonicalRequestHandle("permission", routeKey, payload.handle)
446
+ : undefined;
447
+ if (!routeKey || !handle) {
448
+ return;
449
+ }
450
+ await upsertNotification({
451
+ idempotencyKey,
452
+ kind: "permission",
453
+ wechatAccountId: binding.wechatAccountId,
454
+ userId: binding.userId,
455
+ routeKey,
456
+ handle,
457
+ ...(readNonEmptyString(payload.scopeKey)
458
+ ? { scopeKey: readNonEmptyString(payload.scopeKey) }
459
+ : { scopeKey: instanceID }),
460
+ ...(payload.prompt !== undefined && payload.prompt !== null
461
+ ? { prompt: payload.prompt }
462
+ : {}),
463
+ createdAt: readNotificationEventTimestamp(payload),
464
+ });
465
+ return;
466
+ }
467
+ if (event.type === "naturalStopOpened") {
468
+ if (!isNonEmptyString(payload.handle) ||
469
+ payload.replyTarget === null ||
470
+ typeof payload.replyTarget !== "object") {
471
+ return;
472
+ }
473
+ const replyTarget = payload.replyTarget;
474
+ const replyTargetInstanceID = readNonEmptyString(replyTarget.instanceID);
475
+ const replyTargetSessionID = readNonEmptyString(replyTarget.sessionID);
476
+ const sessionID = readNaturalStopSessionID(payload);
477
+ const directSessionID = readNonEmptyString(payload.sessionID);
478
+ if (!replyTargetInstanceID || !replyTargetSessionID || !sessionID) {
479
+ return;
480
+ }
481
+ if (replyTargetInstanceID !== instanceID ||
482
+ (directSessionID && directSessionID !== replyTargetSessionID)) {
483
+ return;
484
+ }
485
+ await upsertNotification({
486
+ idempotencyKey,
487
+ kind: "naturalStop",
488
+ wechatAccountId: binding.wechatAccountId,
489
+ userId: binding.userId,
490
+ handle: payload.handle,
491
+ scopeKey: replyTargetInstanceID,
492
+ sessionID,
493
+ replyTarget: {
494
+ instanceID: replyTargetInstanceID,
495
+ sessionID: replyTargetSessionID,
496
+ },
497
+ redactedSummary: readNonEmptyString(payload.redactedSummary) ?? "任务已停止",
498
+ severityAdvice: readNonEmptyString(payload.severityAdvice) ?? "已停止并等待你的回复",
499
+ createdAt: readNotificationEventTimestamp(payload),
500
+ });
501
+ return;
502
+ }
503
+ if (event.type === "retryErrorUpdated") {
504
+ await upsertNotification({
505
+ idempotencyKey,
506
+ kind: "sessionError",
507
+ source: "retryError",
508
+ wechatAccountId: binding.wechatAccountId,
509
+ userId: binding.userId,
510
+ ...(readNonEmptyString(payload.sessionID)
511
+ ? { sessionID: readNonEmptyString(payload.sessionID) }
512
+ : {}),
513
+ ...(readNonEmptyString(payload.action)
514
+ ? { action: readNonEmptyString(payload.action) }
515
+ : {}),
516
+ redactedSummary: readNonEmptyString(payload.redactedSummary) ?? DELIVERY_FAILURE_SUMMARY,
517
+ severityAdvice: readNonEmptyString(payload.severityAdvice) ?? DELIVERY_FAILURE_SEVERITY,
518
+ createdAt: readNotificationEventTimestamp(payload),
519
+ });
520
+ return;
521
+ }
522
+ if (event.type === "questionClosed" || event.type === "permissionClosed") {
523
+ const routeKey = readNonEmptyString(payload.routeKey);
524
+ const reason = readNonEmptyString(payload.reason);
525
+ const requestTerminalReason = reason === "answered" ||
526
+ reason === "handled" ||
527
+ reason === "rejected" ||
528
+ reason === "expired" ||
529
+ reason === "replaced"
530
+ ? reason
531
+ : undefined;
532
+ const terminalMetadata = routeKey
533
+ ? asObject(liveWsCoordinator.getState().terminalMetadata[routeKey])
534
+ : {};
535
+ if (terminalMetadata.terminalResultSent === true) {
536
+ return;
537
+ }
538
+ const handle = readNonEmptyString(terminalMetadata.handle) ??
539
+ readNonEmptyString(payload.handle);
540
+ if (!routeKey || !handle || !requestTerminalReason) {
541
+ return;
542
+ }
543
+ const scopeKey = readNonEmptyString(terminalMetadata.scopeKey) ??
544
+ readNonEmptyString(payload.scopeKey) ??
545
+ instanceID;
546
+ const replacementHandle = readNonEmptyString(terminalMetadata.replacementHandle) ??
547
+ readNonEmptyString(payload.replacementHandle);
548
+ await upsertNotification({
549
+ idempotencyKey,
550
+ kind: "requestTerminal",
551
+ wechatAccountId: binding.wechatAccountId,
552
+ userId: binding.userId,
553
+ requestKind: event.type === "questionClosed" ? "question" : "permission",
554
+ routeKey,
555
+ handle,
556
+ scopeKey,
557
+ terminalReason: requestTerminalReason,
558
+ ...(replacementHandle ? { replacementHandle } : {}),
559
+ createdAt: readNotificationEventTimestamp(payload),
560
+ });
561
+ }
562
+ }
563
+ function clearRuntimeState() {
564
+ liveBridgeByInstanceID.clear();
565
+ liveBridgeBySocket.clear();
566
+ for (const pending of pendingWsReplyMutationsByCommandId.values()) {
567
+ clearTimeout(pending.timer);
568
+ }
569
+ pendingWsReplyMutationsByCommandId.clear();
570
+ brokerMutationQueue = createBrokerMutationQueue();
571
+ liveWsCoordinator = createBrokerWsCoordinator();
572
+ setBrokerStateMutationTarget(liveWsCoordinator.getState());
573
+ }
574
+ async function markAuthoritativeStaleConnections(now, heartbeatTimeoutMs) {
575
+ const timedOutScopes = listTimedOutBrokerConnectionScopes(liveWsCoordinator.getState(), {
576
+ now,
577
+ timeoutMs: heartbeatTimeoutMs,
578
+ });
579
+ for (const scope of timedOutScopes) {
580
+ markBrokerConnectionOffline(liveWsCoordinator.getState(), {
581
+ ...scope,
582
+ disconnectedAt: now,
583
+ reason: "instanceStale",
584
+ });
585
+ const expiredRequests = expireBrokerIndexedRequestsForScope(liveWsCoordinator.getState(), {
586
+ scopeKey: scope.instanceID,
587
+ expiredAt: now,
588
+ });
589
+ closeBrokerNaturalStopsForScope(liveWsCoordinator.getState(), {
590
+ scopeKey: scope.instanceID,
591
+ terminalReason: "expired",
592
+ });
593
+ await persistBrokerStateStoreSnapshot(liveWsCoordinator.getState());
594
+ await appendBrokerDiagnostic({
595
+ type: "instanceStale",
596
+ code: "instanceStale",
597
+ instanceID: scope.instanceID,
598
+ });
599
+ for (const expired of expiredRequests) {
600
+ await appendBrokerDiagnostic({
601
+ type: "requestExpired",
602
+ code: "requestExpired",
603
+ instanceID: scope.instanceID,
604
+ kind: expired.kind,
605
+ routeKey: expired.routeKey,
606
+ });
607
+ }
608
+ }
609
+ }
610
+ function toPositiveNumber(rawValue, fallback) {
611
+ if (!isNonEmptyString(rawValue)) {
612
+ return fallback;
613
+ }
614
+ const parsed = Number(rawValue);
615
+ if (!Number.isFinite(parsed) || parsed <= 0) {
616
+ return fallback;
617
+ }
618
+ return parsed;
619
+ }
620
+ function asObject(value) {
621
+ if (typeof value !== "object" || value === null) {
622
+ return {};
623
+ }
624
+ return value;
625
+ }
626
+ function isFiniteNumber(value) {
627
+ return typeof value === "number" && Number.isFinite(value);
628
+ }
629
+ function readNonEmptyString(value) {
630
+ return isNonEmptyString(value) ? value.trim() : undefined;
631
+ }
632
+ function readFiniteNumber(value) {
633
+ return isFiniteNumber(value) ? value : undefined;
634
+ }
635
+ function shouldReconcileVisibleRequestHandles(eventType) {
636
+ return (eventType === "questionOpened" ||
637
+ eventType === "questionUpdated" ||
638
+ eventType === "permissionOpened" ||
639
+ eventType === "permissionUpdated" ||
640
+ eventType === "fullSyncCompleted");
641
+ }
642
+ function isSafeInstanceID(instanceID) {
643
+ if (!isNonEmptyString(instanceID)) {
644
+ return false;
645
+ }
646
+ if (instanceID.includes("/") || instanceID.includes("\\")) {
647
+ return false;
648
+ }
649
+ if (instanceID.includes("..")) {
650
+ return false;
651
+ }
652
+ return true;
653
+ }
654
+ async function cleanupTerminalRequests(now, cleanAfterMs, purgeRetentionMs) {
655
+ const cleanup = cleanupBrokerRuntimeTerminalRequests(liveWsCoordinator.getState(), {
656
+ now,
657
+ cleanAfterMs,
658
+ purgeRetentionMs,
659
+ });
660
+ if (cleanup.cleanedRequests.length > 0 || cleanup.purgedRequests.length > 0) {
661
+ await persistBrokerStateStoreSnapshot(liveWsCoordinator.getState());
662
+ }
663
+ for (const cleaned of cleanup.cleanedRequests) {
664
+ await appendBrokerDiagnostic({
665
+ type: "requestCleaned",
666
+ code: "requestCleaned",
667
+ instanceID: cleaned.scopeKey ?? "unknown",
668
+ kind: cleaned.kind,
669
+ routeKey: cleaned.routeKey,
670
+ });
671
+ }
672
+ for (const request of cleanup.purgedRequests) {
673
+ await appendBrokerDiagnostic({
674
+ type: "requestPurged",
675
+ code: "requestPurged",
676
+ instanceID: request.scopeKey ?? "unknown",
677
+ kind: request.kind,
678
+ routeKey: request.routeKey,
679
+ });
680
+ }
681
+ }
682
+ async function cleanupDeadLetters(now, retentionMs) {
683
+ const purged = await purgeDeadLettersBefore(now - retentionMs);
684
+ for (const record of purged) {
685
+ await appendBrokerDiagnostic({
686
+ type: "deadLetterPurged",
687
+ code: "deadLetterPurged",
688
+ instanceID: record.instanceID ?? record.scopeKey ?? "unknown",
689
+ kind: record.kind,
690
+ routeKey: record.routeKey,
691
+ });
692
+ }
693
+ }
694
+ async function cleanupSocketRegistrations(socket, reason) {
695
+ const live = liveBridgeBySocket.get(socket);
696
+ if (live?.socket === socket) {
697
+ const state = liveWsCoordinator.getState();
698
+ const current = liveBridgeByInstanceID.get(live.instanceID);
699
+ if (current?.socket === socket) {
700
+ clearBrokerActiveScope(state, {
701
+ scopeKey: live.instanceID,
702
+ });
703
+ liveBridgeByInstanceID.delete(live.instanceID);
704
+ }
705
+ else {
706
+ markBrokerConnectionOffline(state, {
707
+ instanceID: live.instanceID,
708
+ instanceIncarnation: live.instanceIncarnation,
709
+ disconnectedAt: Date.now(),
710
+ reason,
711
+ });
712
+ }
713
+ removeBrokerConnectionScope(state, {
714
+ instanceID: live.instanceID,
715
+ instanceIncarnation: live.instanceIncarnation,
716
+ });
717
+ liveBridgeBySocket.delete(socket);
718
+ await persistBrokerStateStoreSnapshot(state);
719
+ }
720
+ }
721
+ function hasLiveBridgeEventType(type) {
722
+ return LIVE_BRIDGE_EVENT_TYPES.has(type);
723
+ }
724
+ function toReplyMutationResultFromEventPayload(payload, mutationId) {
725
+ const status = payload.status;
726
+ const failure = asObject(payload.failure);
727
+ if (status === "completed") {
728
+ return { mutationId, ok: true };
729
+ }
730
+ return {
731
+ mutationId,
732
+ ok: false,
733
+ ...(isNonEmptyString(failure.message)
734
+ ? { errorMessage: failure.message }
735
+ : { errorMessage: "command failed" }),
736
+ };
737
+ }
738
+ async function handleMessage(envelope, socket) {
739
+ const requestId = getRequestId(envelope);
740
+ if (envelope.type === "ping") {
741
+ const liveRegistration = liveBridgeBySocket.get(socket);
742
+ if (liveRegistration) {
743
+ await queueBrokerMutation("ping", async () => {
744
+ markBrokerConnectionObserved(liveWsCoordinator.getState(), {
745
+ instanceID: liveRegistration.instanceID,
746
+ instanceIncarnation: liveRegistration.instanceIncarnation,
747
+ observedAt: Date.now(),
748
+ });
749
+ await persistBrokerStateStoreSnapshot(liveWsCoordinator.getState());
750
+ }).catch(() => { });
751
+ }
752
+ writeEnvelope(socket, {
753
+ id: `pong-${requestId}`,
754
+ type: "pong",
755
+ payload: { message: "pong" },
756
+ });
757
+ return;
758
+ }
759
+ if (envelope.type === "hello/register") {
760
+ let hello;
761
+ try {
762
+ hello = createHelloRegisterEnvelope(envelope.payload).payload;
763
+ }
764
+ catch {
765
+ writeError(socket, "invalidMessage", "hello/register payload is invalid", requestId);
766
+ return;
767
+ }
768
+ if (!isSafeInstanceID(hello.instanceID)) {
769
+ writeError(socket, "invalidMessage", "instanceID is required", requestId);
770
+ return;
771
+ }
772
+ const registerResult = await queueBrokerMutation("hello/register", async () => {
773
+ const observedAt = Date.now();
774
+ const current = liveBridgeByInstanceID.get(hello.instanceID);
775
+ const nextRegistration = {
776
+ instanceID: hello.instanceID,
777
+ instanceIncarnation: hello.instanceIncarnation,
778
+ socket,
779
+ };
780
+ liveBridgeByInstanceID.set(hello.instanceID, nextRegistration);
781
+ liveBridgeBySocket.set(socket, nextRegistration);
782
+ if (current && current.socket !== socket) {
783
+ liveBridgeBySocket.delete(current.socket);
784
+ }
785
+ const registerResult = liveWsCoordinator.registerBridge(hello);
786
+ markBrokerConnectionObserved(liveWsCoordinator.getState(), {
787
+ instanceID: hello.instanceID,
788
+ instanceIncarnation: hello.instanceIncarnation,
789
+ observedAt,
790
+ connectedAt: observedAt,
791
+ });
792
+ await persistBrokerStateStoreSnapshot(liveWsCoordinator.getState());
793
+ return registerResult;
794
+ });
795
+ writeEnvelope(socket, {
796
+ id: `registerAck-${requestId}`,
797
+ type: "registerAck",
798
+ instanceID: hello.instanceID,
799
+ payload: {
800
+ ...registerResult.ack.payload,
801
+ ...(registerResult.control ? { control: registerResult.control } : {}),
802
+ pendingCommands: registerResult.pendingCommands,
803
+ },
804
+ });
805
+ return;
806
+ }
807
+ if (hasLiveBridgeEventType(envelope.type)) {
808
+ const instanceID = envelope.instanceID;
809
+ const liveRegistration = liveBridgeBySocket.get(socket);
810
+ if (!isNonEmptyString(instanceID) ||
811
+ !liveRegistration ||
812
+ liveRegistration.instanceID !== instanceID) {
813
+ writeError(socket, "unauthorized", "live bridge is not registered", requestId);
814
+ return;
815
+ }
816
+ let event;
817
+ try {
818
+ event = createBridgeEventEnvelope(envelope.payload);
819
+ }
820
+ catch {
821
+ writeError(socket, "invalidMessage", `${envelope.type} payload is invalid`, requestId);
822
+ return;
823
+ }
824
+ if (event.type !== envelope.type) {
825
+ writeError(socket, "invalidMessage", `${envelope.type} payload type mismatch`, requestId);
826
+ return;
827
+ }
828
+ const result = await queueBrokerMutation(`bridgeEvent:${event.type}`, async () => {
829
+ const next = liveWsCoordinator.handleBridgeEvent(event, {
830
+ instanceID,
831
+ controlId: event.controlId,
832
+ });
833
+ if (shouldReconcileVisibleRequestHandles(event.type)) {
834
+ await reconcileBrokerActiveRequestHandlesWithVisibleNotifications(liveWsCoordinator.getState());
835
+ }
836
+ await projectNotificationFromLiveBridgeEvent(event, instanceID);
837
+ if (shouldReconcileVisibleRequestHandles(event.type)) {
838
+ await reconcileBrokerActiveRequestHandlesWithVisibleNotifications(liveWsCoordinator.getState());
839
+ }
840
+ const currentConnection = liveWsCoordinator.getState().connections[instanceID]?.[event.instanceIncarnation];
841
+ if (currentConnection?.online !== false) {
842
+ markBrokerConnectionObserved(liveWsCoordinator.getState(), {
843
+ instanceID,
844
+ instanceIncarnation: event.instanceIncarnation,
845
+ observedAt: Date.now(),
846
+ });
847
+ }
848
+ await persistBrokerStateStoreSnapshot(liveWsCoordinator.getState());
849
+ return next;
850
+ });
851
+ if (event.type === "commandResult") {
852
+ const payload = asObject(event.payload);
853
+ const commandId = isNonEmptyString(payload.commandId)
854
+ ? payload.commandId
855
+ : undefined;
856
+ if (commandId) {
857
+ const pending = pendingWsReplyMutationsByCommandId.get(commandId);
858
+ if (pending) {
859
+ pendingWsReplyMutationsByCommandId.delete(commandId);
860
+ clearTimeout(pending.timer);
861
+ pending.resolve(toReplyMutationResultFromEventPayload(payload, pending.mutationId));
862
+ }
863
+ }
864
+ }
865
+ writeEnvelope(socket, {
866
+ id: `ack-${requestId}`,
867
+ type: "ack",
868
+ instanceID,
869
+ payload: result.ack.payload,
870
+ });
871
+ return;
872
+ }
873
+ if (envelope.type === "registerInstance") {
874
+ writeError(socket, "notImplemented", "legacy path removed: registerInstance is unsupported", requestId);
875
+ return;
876
+ }
877
+ if (envelope.type === "heartbeat") {
878
+ writeError(socket, "notImplemented", "legacy path removed: heartbeat is unsupported", requestId);
879
+ return;
880
+ }
881
+ if (envelope.type === "statusSnapshot") {
882
+ writeError(socket, "notImplemented", "legacy path removed: statusSnapshot is unsupported", requestId);
883
+ return;
884
+ }
885
+ if (envelope.type === "replyQuestionResult" ||
886
+ envelope.type === "replyPermissionResult" ||
887
+ envelope.type === "replyNaturalStopResult") {
888
+ writeError(socket, "notImplemented", `legacy path removed: ${envelope.type} is unsupported`, requestId);
889
+ return;
890
+ }
891
+ if (envelope.type === "syncWechatNotifications") {
892
+ writeError(socket, "notImplemented", "legacy path removed: syncWechatNotifications is unsupported", requestId);
893
+ return;
894
+ }
895
+ if (LEGACY_REMOVED_MESSAGE_TYPES.has(envelope.type)) {
896
+ writeError(socket, "notImplemented", `legacy path removed: ${envelope.type} is unsupported`, requestId);
897
+ return;
898
+ }
899
+ if (FUTURE_MESSAGE_TYPES.has(envelope.type)) {
900
+ writeError(socket, "notImplemented", "future message is not implemented", requestId);
901
+ return;
902
+ }
903
+ writeError(socket, "notImplemented", `${envelope.type} is not implemented`, requestId);
904
+ }
905
+ async function tightenEndpointPermission(endpoint) {
906
+ if (process.platform === "win32" || isTcpBrokerEndpoint(endpoint)) {
907
+ return;
908
+ }
909
+ await chmod(endpoint, WECHAT_FILE_MODE);
910
+ const info = await stat(endpoint);
911
+ if ((info.mode & 0o777) !== WECHAT_FILE_MODE) {
912
+ throw new Error("failed to enforce broker endpoint permission");
913
+ }
914
+ }
915
+ async function ensureCurrentUserCanAccess(endpoint) {
916
+ await new Promise((resolve, reject) => {
917
+ const probe = createBrokerSocket(endpoint);
918
+ probe.once("connect", () => {
919
+ probe.end();
920
+ resolve();
921
+ });
922
+ probe.once("error", reject);
923
+ });
924
+ }
925
+ async function prepareEndpoint(endpoint) {
926
+ if (process.platform === "win32" || isTcpBrokerEndpoint(endpoint)) {
927
+ return;
928
+ }
929
+ await mkdir(path.dirname(endpoint), {
930
+ recursive: true,
931
+ mode: WECHAT_DIR_MODE,
932
+ });
933
+ await rm(endpoint, { force: true });
934
+ }
935
+ export async function startBrokerServer(endpoint) {
936
+ await prepareEndpoint(endpoint);
937
+ const persistedBrokerState = await loadBrokerStateStoreSnapshot();
938
+ if (persistedBrokerState) {
939
+ reconcileBrokerDisconnectedScopes(persistedBrokerState, {
940
+ disconnectedAt: Date.now(),
941
+ });
942
+ await reconcileBrokerActiveRequestHandlesWithVisibleNotifications(persistedBrokerState);
943
+ await persistBrokerStateStoreSnapshot(persistedBrokerState);
944
+ }
945
+ liveWsCoordinator = createBrokerWsCoordinator({
946
+ state: persistedBrokerState ?? createEmptyBrokerState(),
947
+ });
948
+ setBrokerStateMutationTarget(liveWsCoordinator.getState());
949
+ const heartbeatTimeoutMs = toPositiveNumber(process.env.WECHAT_BROKER_HEARTBEAT_TIMEOUT_MS, DEFAULT_HEARTBEAT_TIMEOUT_MS);
950
+ const heartbeatScanIntervalMs = toPositiveNumber(process.env.WECHAT_BROKER_HEARTBEAT_SCAN_INTERVAL_MS, DEFAULT_HEARTBEAT_SCAN_INTERVAL_MS);
951
+ const statusCollectWindowMs = toPositiveNumber(process.env.WECHAT_BROKER_STATUS_COLLECT_WINDOW_MS, DEFAULT_STATUS_COLLECT_WINDOW_MS);
952
+ void statusCollectWindowMs;
953
+ const requestCleanAfterMs = toPositiveNumber(process.env.WECHAT_BROKER_REQUEST_CLEAN_AFTER_MS, DEFAULT_REQUEST_CLEAN_AFTER_MS);
954
+ const requestPurgeRetentionMs = toPositiveNumber(process.env.WECHAT_BROKER_REQUEST_PURGE_RETENTION_MS, DEFAULT_REQUEST_PURGE_RETENTION_MS);
955
+ const requestCleanupScanIntervalMs = toPositiveNumber(process.env.WECHAT_BROKER_REQUEST_CLEANUP_SCAN_INTERVAL_MS, DEFAULT_REQUEST_CLEANUP_SCAN_INTERVAL_MS);
956
+ const deadLetterRetentionMs = toPositiveNumber(process.env.WECHAT_BROKER_DEAD_LETTER_RETENTION_MS, DEFAULT_DEAD_LETTER_RETENTION_MS);
957
+ const deadLetterScanIntervalMs = toPositiveNumber(process.env.WECHAT_BROKER_DEAD_LETTER_SCAN_INTERVAL_MS, DEFAULT_DEAD_LETTER_SCAN_INTERVAL_MS);
958
+ const maintenanceTasks = new Set();
959
+ const trackMaintenanceTask = (task) => {
960
+ maintenanceTasks.add(task);
961
+ void task.finally(() => {
962
+ maintenanceTasks.delete(task);
963
+ });
964
+ };
965
+ const socketMessageChains = new Set();
966
+ const allSockets = new Set();
967
+ let closed = false;
968
+ let closePromise;
969
+ const server = net.createServer((socket) => {
970
+ if (closed) {
971
+ socket.destroy();
972
+ return;
973
+ }
974
+ allSockets.add(socket);
975
+ let buffer = "";
976
+ let messageChain = Promise.resolve();
977
+ const trackSocketMessageChain = (chain) => {
978
+ socketMessageChains.add(chain);
979
+ void chain.finally(() => {
980
+ socketMessageChains.delete(chain);
981
+ });
982
+ };
983
+ const enqueueSocketTask = (task) => {
984
+ const next = messageChain.then(task).catch(() => {
985
+ // errors are converted to response envelopes in handleMessage
986
+ });
987
+ messageChain = next;
988
+ trackSocketMessageChain(next);
989
+ };
990
+ const enqueueSocketCleanup = (reason) => {
991
+ enqueueSocketTask(async () => {
992
+ await queueBrokerMutation("cleanupSocketRegistrations", async () => {
993
+ await cleanupSocketRegistrations(socket, reason);
994
+ }).catch(() => { });
995
+ });
996
+ };
997
+ socket.on("close", () => {
998
+ allSockets.delete(socket);
999
+ enqueueSocketCleanup("socketClosed");
1000
+ });
1001
+ socket.on("error", () => {
1002
+ enqueueSocketCleanup("socketError");
1003
+ });
1004
+ socket.on("data", (chunk) => {
1005
+ buffer += chunk.toString("utf8");
1006
+ while (true) {
1007
+ const newlineIndex = buffer.indexOf("\n");
1008
+ if (newlineIndex === -1) {
1009
+ break;
1010
+ }
1011
+ const line = buffer.slice(0, newlineIndex);
1012
+ buffer = buffer.slice(newlineIndex + 1);
1013
+ try {
1014
+ const envelope = parseIncomingEnvelopeLine(`${line}\n`);
1015
+ enqueueSocketTask(() => handleMessage(envelope, socket));
1016
+ }
1017
+ catch {
1018
+ writeError(socket, "invalidMessage", "invalid message line", "unknown");
1019
+ }
1020
+ }
1021
+ });
1022
+ });
1023
+ const boundEndpoint = await listenOnBrokerEndpoint(server, endpoint);
1024
+ try {
1025
+ await tightenEndpointPermission(boundEndpoint);
1026
+ await ensureCurrentUserCanAccess(boundEndpoint);
1027
+ }
1028
+ catch (error) {
1029
+ await new Promise((resolve) => {
1030
+ server.close(() => resolve());
1031
+ });
1032
+ throw error;
1033
+ }
1034
+ await markAuthoritativeStaleConnections(Date.now(), heartbeatTimeoutMs);
1035
+ await cleanupTerminalRequests(Date.now(), requestCleanAfterMs, requestPurgeRetentionMs);
1036
+ await cleanupDeadLetters(Date.now(), deadLetterRetentionMs);
1037
+ const staleConnectionTimer = setInterval(() => {
1038
+ trackMaintenanceTask(queueBrokerMutation("markAuthoritativeStaleConnections", async () => {
1039
+ await markAuthoritativeStaleConnections(Date.now(), heartbeatTimeoutMs);
1040
+ }).catch((error) => {
1041
+ console.error("[wechat-broker] failed to mark stale authoritative connections", error);
1042
+ }));
1043
+ }, heartbeatScanIntervalMs);
1044
+ const requestCleanupTimer = setInterval(() => {
1045
+ trackMaintenanceTask(queueBrokerMutation("cleanupTerminalRequests", async () => {
1046
+ await cleanupTerminalRequests(Date.now(), requestCleanAfterMs, requestPurgeRetentionMs);
1047
+ }).catch((error) => {
1048
+ console.error("[wechat-broker] failed to clean terminal requests", error);
1049
+ }));
1050
+ }, requestCleanupScanIntervalMs);
1051
+ const deadLetterCleanupTimer = setInterval(() => {
1052
+ trackMaintenanceTask(cleanupDeadLetters(Date.now(), deadLetterRetentionMs).catch((error) => {
1053
+ console.error("[wechat-broker] failed to purge dead letters", error);
1054
+ }));
1055
+ }, deadLetterScanIntervalMs);
1056
+ const collectStatus = async () => {
1057
+ const requestId = `collect-${Date.now()}-${Math.random().toString(16).slice(2)}`;
1058
+ const view = readBrokerAuthoritativeView(liveWsCoordinator.getState());
1059
+ const instances = buildAggregatedStatusInstancesFromBrokerView(view);
1060
+ return {
1061
+ requestId,
1062
+ instances,
1063
+ reply: formatAggregatedStatusReplyFromBrokerView(view),
1064
+ };
1065
+ };
1066
+ const handleWechatSlashCommand = async (command) => {
1067
+ if (command.type === "status") {
1068
+ const result = await collectStatus();
1069
+ return result.reply;
1070
+ }
1071
+ if (command.type === "todo") {
1072
+ const view = readBrokerAuthoritativeView(liveWsCoordinator.getState());
1073
+ return formatTodoReplyFromBrokerView(view);
1074
+ }
1075
+ if (command.type === "reply") {
1076
+ return "命令暂未实现:/reply";
1077
+ }
1078
+ if (command.type === "allow") {
1079
+ return "命令暂未实现:/allow";
1080
+ }
1081
+ return "未知命令";
1082
+ };
1083
+ const handleNotificationDeliveryFailure = async (input) => {
1084
+ await queueBrokerMutation("authoritativeRetryError", async () => {
1085
+ await Promise.resolve(markTokenStale({
1086
+ wechatAccountId: input.wechatAccountId,
1087
+ userId: input.userId,
1088
+ staleReason: NOTIFICATION_DELIVERY_FAILED_STALE_REASON,
1089
+ })).catch(() => { });
1090
+ const liveRegistration = liveBridgeByInstanceID.get(input.instanceID);
1091
+ upsertRetryErrorSummary(liveWsCoordinator.getState(), {
1092
+ instanceID: input.instanceID,
1093
+ action: DELIVERY_FAILURE_ACTION,
1094
+ redactedSummary: DELIVERY_FAILURE_SUMMARY,
1095
+ severityAdvice: DELIVERY_FAILURE_SEVERITY,
1096
+ updatedAt: Date.now(),
1097
+ ...(liveRegistration
1098
+ ? { instanceIncarnation: liveRegistration.instanceIncarnation }
1099
+ : {}),
1100
+ });
1101
+ await persistBrokerStateStoreSnapshot(liveWsCoordinator.getState());
1102
+ }).catch(() => { });
1103
+ };
1104
+ const dispatchReplyQuestionToInstance = async (input) => {
1105
+ const liveRegistration = liveBridgeByInstanceID.get(input.instanceID);
1106
+ if (liveRegistration && !liveRegistration.socket.destroyed) {
1107
+ const command = await queueBrokerMutation("dispatchWsReplyQuestion", async () => {
1108
+ const next = liveWsCoordinator.dispatchCommand({
1109
+ instanceID: input.instanceID,
1110
+ instanceIncarnation: liveRegistration.instanceIncarnation,
1111
+ commandId: input.mutationId,
1112
+ type: "replyQuestion",
1113
+ payload: {
1114
+ mutationId: input.mutationId,
1115
+ requestID: input.requestID,
1116
+ answers: input.answers,
1117
+ },
1118
+ target: {
1119
+ instanceID: input.instanceID,
1120
+ requestID: input.requestID,
1121
+ },
1122
+ });
1123
+ await persistBrokerStateStoreSnapshot(liveWsCoordinator.getState());
1124
+ return next;
1125
+ });
1126
+ if (!command) {
1127
+ return {
1128
+ mutationId: input.mutationId,
1129
+ ok: false,
1130
+ errorMessage: `replyQuestion unavailable: ${input.mutationId}`,
1131
+ };
1132
+ }
1133
+ return new Promise((resolve) => {
1134
+ const timer = setTimeout(() => {
1135
+ pendingWsReplyMutationsByCommandId.delete(command.commandId);
1136
+ resolve({
1137
+ mutationId: input.mutationId,
1138
+ ok: false,
1139
+ errorMessage: `replyQuestion timeout: ${input.mutationId}`,
1140
+ });
1141
+ }, 10_000);
1142
+ pendingWsReplyMutationsByCommandId.set(command.commandId, {
1143
+ mutationId: input.mutationId,
1144
+ resolve,
1145
+ timer,
1146
+ });
1147
+ writeEnvelope(liveRegistration.socket, {
1148
+ id: command.commandId,
1149
+ type: command.type,
1150
+ instanceID: input.instanceID,
1151
+ payload: command,
1152
+ });
1153
+ });
1154
+ }
1155
+ return {
1156
+ mutationId: input.mutationId,
1157
+ ok: false,
1158
+ errorMessage: `bridge unavailable: ${input.instanceID}`,
1159
+ };
1160
+ };
1161
+ const dispatchReplyPermissionToInstance = async (input) => {
1162
+ const liveRegistration = liveBridgeByInstanceID.get(input.instanceID);
1163
+ if (liveRegistration && !liveRegistration.socket.destroyed) {
1164
+ const command = await queueBrokerMutation("dispatchWsReplyPermission", async () => {
1165
+ const next = liveWsCoordinator.dispatchCommand({
1166
+ instanceID: input.instanceID,
1167
+ instanceIncarnation: liveRegistration.instanceIncarnation,
1168
+ commandId: input.mutationId,
1169
+ type: "replyPermission",
1170
+ payload: {
1171
+ mutationId: input.mutationId,
1172
+ requestID: input.requestID,
1173
+ reply: input.reply,
1174
+ ...(input.message ? { message: input.message } : {}),
1175
+ },
1176
+ target: {
1177
+ instanceID: input.instanceID,
1178
+ requestID: input.requestID,
1179
+ },
1180
+ });
1181
+ await persistBrokerStateStoreSnapshot(liveWsCoordinator.getState());
1182
+ return next;
1183
+ });
1184
+ if (!command) {
1185
+ return {
1186
+ mutationId: input.mutationId,
1187
+ ok: false,
1188
+ errorMessage: `replyPermission unavailable: ${input.mutationId}`,
1189
+ };
1190
+ }
1191
+ return new Promise((resolve) => {
1192
+ const timer = setTimeout(() => {
1193
+ pendingWsReplyMutationsByCommandId.delete(command.commandId);
1194
+ resolve({
1195
+ mutationId: input.mutationId,
1196
+ ok: false,
1197
+ errorMessage: `replyPermission timeout: ${input.mutationId}`,
1198
+ });
1199
+ }, 10_000);
1200
+ pendingWsReplyMutationsByCommandId.set(command.commandId, {
1201
+ mutationId: input.mutationId,
1202
+ resolve,
1203
+ timer,
1204
+ });
1205
+ writeEnvelope(liveRegistration.socket, {
1206
+ id: command.commandId,
1207
+ type: command.type,
1208
+ instanceID: input.instanceID,
1209
+ payload: command,
1210
+ });
1211
+ });
1212
+ }
1213
+ return {
1214
+ mutationId: input.mutationId,
1215
+ ok: false,
1216
+ errorMessage: `bridge unavailable: ${input.instanceID}`,
1217
+ };
1218
+ };
1219
+ const dispatchReplyNaturalStopToInstance = async (input) => {
1220
+ const liveRegistration = liveBridgeByInstanceID.get(input.instanceID);
1221
+ if (liveRegistration && !liveRegistration.socket.destroyed) {
1222
+ const command = await queueBrokerMutation("dispatchWsReplyNaturalStop", async () => {
1223
+ const next = liveWsCoordinator.dispatchCommand({
1224
+ instanceID: input.instanceID,
1225
+ instanceIncarnation: liveRegistration.instanceIncarnation,
1226
+ commandId: input.mutationId,
1227
+ type: "replyNaturalStop",
1228
+ payload: {
1229
+ mutationId: input.mutationId,
1230
+ sessionID: input.sessionID,
1231
+ text: input.text,
1232
+ },
1233
+ target: {
1234
+ instanceID: input.instanceID,
1235
+ sessionID: input.sessionID,
1236
+ },
1237
+ });
1238
+ await persistBrokerStateStoreSnapshot(liveWsCoordinator.getState());
1239
+ return next;
1240
+ });
1241
+ if (!command) {
1242
+ return {
1243
+ mutationId: input.mutationId,
1244
+ ok: false,
1245
+ errorMessage: `replyNaturalStop unavailable: ${input.mutationId}`,
1246
+ };
1247
+ }
1248
+ return new Promise((resolve) => {
1249
+ const timer = setTimeout(() => {
1250
+ pendingWsReplyMutationsByCommandId.delete(command.commandId);
1251
+ resolve({
1252
+ mutationId: input.mutationId,
1253
+ ok: false,
1254
+ errorMessage: `replyNaturalStop timeout: ${input.mutationId}`,
1255
+ });
1256
+ }, 10_000);
1257
+ pendingWsReplyMutationsByCommandId.set(command.commandId, {
1258
+ mutationId: input.mutationId,
1259
+ resolve,
1260
+ timer,
1261
+ });
1262
+ writeEnvelope(liveRegistration.socket, {
1263
+ id: command.commandId,
1264
+ type: command.type,
1265
+ instanceID: input.instanceID,
1266
+ payload: command,
1267
+ });
1268
+ });
1269
+ }
1270
+ return {
1271
+ mutationId: input.mutationId,
1272
+ ok: false,
1273
+ errorMessage: `bridge unavailable: ${input.instanceID}`,
1274
+ };
1275
+ };
1276
+ const close = async () => {
1277
+ if (closePromise) {
1278
+ return closePromise;
1279
+ }
1280
+ closePromise = (async () => {
1281
+ closed = true;
1282
+ clearInterval(staleConnectionTimer);
1283
+ clearInterval(requestCleanupTimer);
1284
+ clearInterval(deadLetterCleanupTimer);
1285
+ const serverClosed = new Promise((resolve) => {
1286
+ server.close(() => resolve());
1287
+ });
1288
+ while (maintenanceTasks.size > 0) {
1289
+ await Promise.allSettled([...maintenanceTasks]);
1290
+ }
1291
+ const sockets = [...allSockets];
1292
+ const socketClosePromises = sockets.map((socket) => {
1293
+ if (socket.destroyed) {
1294
+ return Promise.resolve();
1295
+ }
1296
+ return new Promise((resolve) => {
1297
+ socket.once("close", () => resolve());
1298
+ });
1299
+ });
1300
+ for (const socket of sockets) {
1301
+ if (!socket.destroyed) {
1302
+ socket.destroy();
1303
+ }
1304
+ }
1305
+ await Promise.allSettled(socketClosePromises);
1306
+ await serverClosed;
1307
+ while (socketMessageChains.size > 0) {
1308
+ await Promise.allSettled([...socketMessageChains]);
1309
+ }
1310
+ await brokerMutationQueue.drain();
1311
+ if (process.platform !== "win32" && !isTcpBrokerEndpoint(endpoint)) {
1312
+ await rm(endpoint, { force: true });
1313
+ }
1314
+ clearRuntimeState();
1315
+ })();
1316
+ return closePromise;
1317
+ };
1318
+ const hasBlockingActivity = async () => {
1319
+ for (const record of liveBridgeByInstanceID.values()) {
1320
+ if (!record.socket.destroyed) {
1321
+ return true;
1322
+ }
1323
+ }
1324
+ const brokerView = readBrokerAuthoritativeView(liveWsCoordinator.getState());
1325
+ return (Object.keys(brokerView.active.questions).length > 0 ||
1326
+ Object.keys(brokerView.active.permissions).length > 0);
1327
+ };
1328
+ return {
1329
+ endpoint: boundEndpoint,
1330
+ startedAt: Date.now(),
1331
+ collectStatus,
1332
+ handleWechatSlashCommand,
1333
+ handleNotificationDeliveryFailure,
1334
+ dispatchReplyQuestionToInstance,
1335
+ dispatchReplyPermissionToInstance,
1336
+ dispatchReplyNaturalStopToInstance,
1337
+ hasBlockingActivity,
1338
+ close,
1339
+ };
1340
+ }