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,1321 @@
1
+ import { readFileSync, rmSync } from "node:fs";
2
+ import { appendFile, mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import process from "node:process";
5
+ import { fileURLToPath } from "node:url";
6
+ import { createBrokerMutationQueue, executeRecoveryMutation, } from "./broker-mutation-queue.js";
7
+ import { startBrokerServer, WECHAT_BROKER_WS_PROTOCOL_VERSION, WECHAT_BROKER_WS_STATE_GENERATION, } from "./broker-server.js";
8
+ import { loadBrokerStateStoreForMutation, persistBrokerStateStoreSnapshot, prepareBrokerStateStoreForStartup, readBrokerIndexedRequest, readBrokerStateUpgradeCloseReason, readLegacyHandleClosure, readBrokerAuthoritativeView as readLiveBrokerAuthoritativeView, readBrokerCommandStateByAction as readLiveBrokerCommandStateByAction, upsertBrokerIndexedRequest, writeLegacyHandleClosure, } from "./broker-state-store.js";
9
+ import { listDeadLetters, listDeadLettersByHandle, listRecoverableDeadLetters, listRecoverableDeadLettersByHandle, listRecoveryChainHandles, markDeadLetterRecovered, markDeadLetterRecoveryFailed, readDeadLetter, writeDeadLetter, } from "./dead-letter-store.js";
10
+ import { createWechatNotificationDispatcher, suppressPreparedPendingNotifications, } from "./notification-dispatcher.js";
11
+ import { formatBrokerLegacyHandleClosureText } from "./notification-format.js";
12
+ import { findSentNotificationByRequest, listPendingNotifications, markNotificationResolved, } from "./notification-store.js";
13
+ import { buildQuestionAnswersFromReply } from "./question-interaction.js";
14
+ import { commitPreparedRecoveryRequestReopen, findOpenRequestByHandle, findRequestByRouteKey, markRequestAnswered, markRequestRejected, prepareRecoveryRequestReopen, rollbackPreparedRecoveryRequestReopen, } from "./request-store.js";
15
+ import { WECHAT_FILE_MODE, wechatStateRoot, wechatStatusRuntimeDiagnosticsPath, } from "./state-paths.js";
16
+ import { formatAggregatedStatusReplyFromBrokerView, formatTodoReplyFromBrokerView, } from "./status-format.js";
17
+ import { createWechatStatusRuntime, } from "./wechat-status-runtime.js";
18
+ const BROKER_WECHAT_RUNTIME_AUTOSTART_DELAY_MS = 1_000;
19
+ const DEFAULT_BROKER_IDLE_TIMEOUT_MS = 5 * 60 * 1000;
20
+ const DEFAULT_BROKER_IDLE_SCAN_INTERVAL_MS = 1_000;
21
+ const DEFAULT_BROKER_OWNERSHIP_SCAN_INTERVAL_MS = 1_000;
22
+ async function readPackageVersion() {
23
+ const packageJsonPath = new URL("../../package.json", import.meta.url);
24
+ return readFile(packageJsonPath, "utf8")
25
+ .then((raw) => {
26
+ const parsed = JSON.parse(raw);
27
+ if (typeof parsed.version === "string" &&
28
+ parsed.version.trim().length > 0) {
29
+ return parsed.version;
30
+ }
31
+ return "unknown";
32
+ })
33
+ .catch(() => "unknown");
34
+ }
35
+ function parseEndpointArg(argv) {
36
+ const prefix = "--endpoint=";
37
+ const endpointArg = argv.find((item) => item.startsWith(prefix));
38
+ if (!endpointArg) {
39
+ throw new Error("missing --endpoint argument");
40
+ }
41
+ const endpoint = endpointArg.slice(prefix.length);
42
+ if (!endpoint) {
43
+ throw new Error("missing --endpoint argument");
44
+ }
45
+ return endpoint;
46
+ }
47
+ function parseStateRootArg(argv) {
48
+ const prefix = "--state-root=";
49
+ const arg = argv.find((item) => item.startsWith(prefix));
50
+ if (!arg) {
51
+ return wechatStateRoot();
52
+ }
53
+ const stateRoot = arg.slice(prefix.length);
54
+ if (!stateRoot) {
55
+ throw new Error("missing --state-root argument");
56
+ }
57
+ return stateRoot;
58
+ }
59
+ function brokerStatePathForRoot(stateRoot) {
60
+ return path.join(stateRoot, "broker.json");
61
+ }
62
+ function toPositiveNumber(rawValue, fallback) {
63
+ if (typeof rawValue !== "string" || rawValue.trim().length === 0) {
64
+ return fallback;
65
+ }
66
+ const parsed = Number(rawValue);
67
+ if (!Number.isFinite(parsed) || parsed <= 0) {
68
+ return fallback;
69
+ }
70
+ return parsed;
71
+ }
72
+ async function writeBrokerState(state, stateRoot) {
73
+ await mkdir(stateRoot, { recursive: true, mode: 0o700 });
74
+ const filePath = brokerStatePathForRoot(stateRoot);
75
+ await writeFile(filePath, JSON.stringify(state, null, 2), {
76
+ mode: WECHAT_FILE_MODE,
77
+ });
78
+ }
79
+ function isBrokerStateOwnedBy(candidate, ownership) {
80
+ return (candidate.pid === ownership.pid &&
81
+ candidate.startedAt === ownership.startedAt &&
82
+ candidate.version === ownership.version &&
83
+ candidate.endpoint === ownership.endpoint);
84
+ }
85
+ async function brokerStateStillOwnedBy(input) {
86
+ try {
87
+ const raw = await readFile(brokerStatePathForRoot(input.stateRoot), "utf8");
88
+ const parsed = JSON.parse(raw);
89
+ return isBrokerStateOwnedBy(parsed, input);
90
+ }
91
+ catch {
92
+ return undefined;
93
+ }
94
+ }
95
+ function shouldExitForLostBrokerOwnership(input) {
96
+ return input.ownerEstablished && input.stillOwned === false;
97
+ }
98
+ function createWechatStatusRuntimeDiagnosticsFileWriter(input) {
99
+ return async (event) => {
100
+ try {
101
+ await mkdir(input.stateRoot, { recursive: true, mode: 0o700 });
102
+ const filePath = wechatStatusRuntimeDiagnosticsPath(input.stateRoot);
103
+ const line = `${JSON.stringify({
104
+ timestamp: Date.now(),
105
+ ...event,
106
+ })}\n`;
107
+ await appendFile(filePath, line, {
108
+ encoding: "utf8",
109
+ mode: WECHAT_FILE_MODE,
110
+ });
111
+ }
112
+ catch (error) {
113
+ input.onRuntimeError(error);
114
+ }
115
+ };
116
+ }
117
+ export function shouldEnableBrokerWechatStatusRuntime(env = process.env) {
118
+ void env;
119
+ return true;
120
+ }
121
+ function withOptionalDirectory(input, directory) {
122
+ if (typeof directory === "string" && directory.trim().length > 0) {
123
+ return {
124
+ ...input,
125
+ directory,
126
+ };
127
+ }
128
+ return input;
129
+ }
130
+ function isInvalidHandleError(error) {
131
+ if (!(error instanceof Error)) {
132
+ return false;
133
+ }
134
+ return /invalid handle format|raw requestID cannot be used as handle/i.test(error.message);
135
+ }
136
+ function toErrorMessage(error) {
137
+ if (error instanceof Error) {
138
+ return error.message;
139
+ }
140
+ if (typeof error === "string") {
141
+ return error;
142
+ }
143
+ return String(error);
144
+ }
145
+ function asRecord(value) {
146
+ if (typeof value !== "object" || value === null) {
147
+ return {};
148
+ }
149
+ return value;
150
+ }
151
+ function readNonEmptyString(value) {
152
+ return typeof value === "string" && value.trim().length > 0
153
+ ? value.trim()
154
+ : undefined;
155
+ }
156
+ function readFiniteNumber(value) {
157
+ return typeof value === "number" && Number.isFinite(value)
158
+ ? value
159
+ : undefined;
160
+ }
161
+ function getSdkMutationError(result) {
162
+ if (!result || typeof result !== "object") {
163
+ return undefined;
164
+ }
165
+ const error = result.error;
166
+ if (!error) {
167
+ return undefined;
168
+ }
169
+ return toErrorMessage(error);
170
+ }
171
+ function createRecoveryFailureToken() {
172
+ return `recovery-failure-${Date.now()}-${Math.random().toString(16).slice(2)}`;
173
+ }
174
+ const brokerEntryMutationQueue = createBrokerMutationQueue();
175
+ export function createBrokerWechatSlashCommandHandler(input) {
176
+ const mutationQueue = input.mutationQueue ?? brokerEntryMutationQueue;
177
+ const markDeadLetterRecoveryFailedImpl = input.markDeadLetterRecoveryFailedImpl ?? markDeadLetterRecoveryFailed;
178
+ const readBrokerAuthoritativeView = input.readBrokerAuthoritativeView ??
179
+ (() => readLiveBrokerAuthoritativeView());
180
+ const readBrokerCommandStateByAction = input.readBrokerCommandStateByAction ??
181
+ ((action) => readLiveBrokerCommandStateByAction(action));
182
+ const handleStatusCommand = input.handleStatusCommand ??
183
+ (async () => {
184
+ const brokerView = await readBrokerAuthoritativeView();
185
+ return formatAggregatedStatusReplyFromBrokerView(brokerView);
186
+ });
187
+ const persistRecoveryFailureWrites = async (records) => {
188
+ const recoveryFailureToken = createRecoveryFailureToken();
189
+ const originals = await Promise.all(records.map(async (record) => {
190
+ const original = await readDeadLetter(record.kind, record.routeKey);
191
+ if (!original) {
192
+ throw new Error(`dead-letter missing during failure persistence: ${record.routeKey}`);
193
+ }
194
+ return original;
195
+ }));
196
+ for (const record of records) {
197
+ try {
198
+ await markDeadLetterRecoveryFailedImpl({
199
+ kind: record.kind,
200
+ routeKey: record.routeKey,
201
+ recoveryErrorCode: record.recoveryErrorCode,
202
+ recoveryErrorMessage: record.recoveryErrorMessage,
203
+ recoveryFailureToken,
204
+ });
205
+ }
206
+ catch (error) {
207
+ const rollbackErrors = [];
208
+ for (const original of originals) {
209
+ try {
210
+ const current = await readDeadLetter(original.kind, original.routeKey);
211
+ if (!current ||
212
+ current.recoveryFailureToken !== recoveryFailureToken) {
213
+ continue;
214
+ }
215
+ await writeDeadLetter(original);
216
+ }
217
+ catch (rollbackError) {
218
+ rollbackErrors.push(`${original.routeKey}: ${toErrorMessage(rollbackError)}`);
219
+ }
220
+ }
221
+ if (rollbackErrors.length > 0) {
222
+ throw new Error(`failed to persist recovery failure metadata and rollback prior updates: ${toErrorMessage(error)}; rollback errors: ${rollbackErrors.join("; ")}`);
223
+ }
224
+ throw new Error(`failed to persist recovery failure metadata: ${toErrorMessage(error)}`);
225
+ }
226
+ }
227
+ };
228
+ const persistRecoveryFailure = async (records, recoveryErrorCode, recoveryErrorMessage) => {
229
+ await persistRecoveryFailureWrites(records.map((record) => ({
230
+ kind: record.kind,
231
+ routeKey: record.routeKey,
232
+ recoveryErrorCode,
233
+ recoveryErrorMessage,
234
+ })));
235
+ return recoveryErrorMessage;
236
+ };
237
+ const persistRecoveryFailures = async (records) => {
238
+ await persistRecoveryFailureWrites(records);
239
+ };
240
+ const classifyRecoveryHandle = async (handle) => {
241
+ const matchedDeadLetters = await listDeadLettersByHandleSafely(handle);
242
+ const classifiedMatches = await classifyMatchedDeadLetters(matchedDeadLetters);
243
+ const recoverableMatches = classifiedMatches.filter((item) => item.state === "valid");
244
+ const invalidMatches = classifiedMatches.filter((item) => item.state === "invalid");
245
+ return {
246
+ matchedDeadLetters,
247
+ classifiedMatches,
248
+ recoverableMatches,
249
+ invalidMatches,
250
+ };
251
+ };
252
+ const createQueuedInvalidRecoveryResult = async (input) => {
253
+ if (input.invalidMatches.length > 0) {
254
+ await persistRecoveryFailures(input.invalidMatches.map((item) => ({
255
+ kind: item.record.kind,
256
+ routeKey: item.record.routeKey,
257
+ recoveryErrorCode: item.failure.recoveryErrorCode,
258
+ recoveryErrorMessage: item.failure.recoveryErrorMessage,
259
+ })));
260
+ if (input.invalidMatches.length === 1 &&
261
+ input.invalidMatches[0].returnDetailedMessage) {
262
+ return {
263
+ ok: false,
264
+ message: input.invalidMatches[0].failure.recoveryErrorMessage,
265
+ };
266
+ }
267
+ }
268
+ return {
269
+ ok: false,
270
+ message: `未找到可恢复的请求:${input.handle}`,
271
+ };
272
+ };
273
+ const readCurrentBrokerView = async () => {
274
+ const brokerView = await Promise.resolve(readBrokerAuthoritativeView());
275
+ if (brokerView) {
276
+ return brokerView;
277
+ }
278
+ return {
279
+ connections: {},
280
+ active: {
281
+ instances: {},
282
+ sessions: {},
283
+ questions: {},
284
+ permissions: {},
285
+ naturalStops: {},
286
+ retryErrors: {},
287
+ },
288
+ terminalMetadata: {},
289
+ retainedOccupancy: {},
290
+ commandLedger: {},
291
+ legacyHandleClosures: {},
292
+ };
293
+ };
294
+ const findActiveRequestByHandle = async (kind, handle) => {
295
+ if (typeof handle !== "string" || handle.trim().length === 0) {
296
+ return undefined;
297
+ }
298
+ const brokerView = await readCurrentBrokerView();
299
+ const records = kind === "question"
300
+ ? brokerView.active.questions
301
+ : brokerView.active.permissions;
302
+ return Object.values(records)
303
+ .map((item) => asRecord(item))
304
+ .find((item) => readNonEmptyString(item.handle) === handle);
305
+ };
306
+ const findLegacyRequestClosureByHandle = async (kind, handle) => {
307
+ if (typeof handle !== "string" || handle.trim().length === 0) {
308
+ return undefined;
309
+ }
310
+ const brokerView = await readCurrentBrokerView();
311
+ const closure = brokerView.legacyHandleClosures[handle] ??
312
+ readLegacyHandleClosure(undefined, { kind, handle });
313
+ if (closure?.kind === kind) {
314
+ return closure;
315
+ }
316
+ return undefined;
317
+ };
318
+ const findActiveNaturalStopByHandle = async (handle) => {
319
+ if (typeof handle !== "string" || handle.trim().length === 0) {
320
+ return undefined;
321
+ }
322
+ const brokerView = await readCurrentBrokerView();
323
+ const naturalStop = brokerView.active.naturalStops[handle];
324
+ return naturalStop ? asRecord(naturalStop) : undefined;
325
+ };
326
+ const findLegacyNaturalStopClosureByHandle = async (handle) => {
327
+ if (typeof handle !== "string" || handle.trim().length === 0) {
328
+ return undefined;
329
+ }
330
+ const brokerView = await readCurrentBrokerView();
331
+ const closure = brokerView.legacyHandleClosures[handle] ??
332
+ readLegacyHandleClosure(undefined, { kind: "naturalStop", handle });
333
+ if (closure?.kind === "naturalStop") {
334
+ return closure;
335
+ }
336
+ return undefined;
337
+ };
338
+ const persistAuthoritativeRequestTerminal = async (input) => {
339
+ const finalizedAt = Date.now();
340
+ const handle = readNonEmptyString(input.openRecord.handle);
341
+ if (!handle) {
342
+ return;
343
+ }
344
+ const openRequest = await findOpenRequestByHandle({
345
+ kind: input.kind,
346
+ handle,
347
+ });
348
+ if (openRequest?.routeKey === input.routeKey) {
349
+ if (input.status === "answered") {
350
+ await markRequestAnswered({
351
+ kind: input.kind,
352
+ routeKey: input.routeKey,
353
+ answeredAt: finalizedAt,
354
+ });
355
+ }
356
+ else {
357
+ await markRequestRejected({
358
+ kind: input.kind,
359
+ routeKey: input.routeKey,
360
+ rejectedAt: finalizedAt,
361
+ });
362
+ }
363
+ }
364
+ const sentNotification = await findSentNotificationByRequest({
365
+ kind: input.kind,
366
+ routeKey: input.routeKey,
367
+ handle,
368
+ });
369
+ if (sentNotification) {
370
+ try {
371
+ await markNotificationResolved({
372
+ idempotencyKey: sentNotification.idempotencyKey,
373
+ resolvedAt: finalizedAt,
374
+ });
375
+ }
376
+ catch (error) {
377
+ if (!(error instanceof Error &&
378
+ error.message === "notification is not sent")) {
379
+ throw error;
380
+ }
381
+ }
382
+ }
383
+ const brokerState = await loadBrokerStateStoreForMutation();
384
+ const current = await readBrokerIndexedRequest({ kind: input.kind, routeKey: input.routeKey }, brokerState);
385
+ const requestID = current?.requestID ?? readNonEmptyString(input.openRecord.requestID);
386
+ const wechatAccountId = current?.wechatAccountId ??
387
+ readNonEmptyString(input.openRecord.wechatAccountId);
388
+ const userId = current?.userId ?? readNonEmptyString(input.openRecord.userId);
389
+ const createdAt = current?.createdAt ?? readFiniteNumber(input.openRecord.createdAt);
390
+ if (!requestID || !wechatAccountId || !userId || createdAt === undefined) {
391
+ return;
392
+ }
393
+ upsertBrokerIndexedRequest(brokerState, {
394
+ kind: input.kind,
395
+ requestID,
396
+ routeKey: input.routeKey,
397
+ handle,
398
+ ...(current?.scopeKey
399
+ ? { scopeKey: current.scopeKey }
400
+ : readNonEmptyString(input.openRecord.scopeKey)
401
+ ? { scopeKey: readNonEmptyString(input.openRecord.scopeKey) }
402
+ : {}),
403
+ ...(current?.prompt !== undefined
404
+ ? { prompt: current.prompt }
405
+ : Object.hasOwn(input.openRecord, "prompt")
406
+ ? { prompt: input.openRecord.prompt }
407
+ : {}),
408
+ wechatAccountId,
409
+ userId,
410
+ status: input.status,
411
+ createdAt,
412
+ ...(input.status === "answered"
413
+ ? { answeredAt: finalizedAt }
414
+ : { rejectedAt: finalizedAt }),
415
+ terminalReason: input.status,
416
+ terminalResultSent: true,
417
+ });
418
+ await persistBrokerStateStoreSnapshot(brokerState);
419
+ };
420
+ const persistAuthoritativeNaturalStopReply = async (input) => {
421
+ const brokerState = await loadBrokerStateStoreForMutation();
422
+ delete brokerState.active.naturalStops[input.handle];
423
+ writeLegacyHandleClosure(brokerState, {
424
+ kind: "naturalStop",
425
+ handle: input.handle,
426
+ reason: "replied",
427
+ });
428
+ await persistBrokerStateStoreSnapshot(brokerState);
429
+ };
430
+ const commandStatusMessage = (status) => {
431
+ if (status === "queued") {
432
+ return "命令尚未送达实例,仍在排队";
433
+ }
434
+ if (status === "delivered") {
435
+ return "命令已送达实例,等待实例接受";
436
+ }
437
+ if (status === "accepted") {
438
+ return "命令已被实例接受,正在处理中";
439
+ }
440
+ return undefined;
441
+ };
442
+ const readCommandFailureMessage = (record) => {
443
+ const message = record.failure?.message;
444
+ if (typeof message === "string" && message.trim().length > 0) {
445
+ return message.trim();
446
+ }
447
+ return "unknown";
448
+ };
449
+ const finalizeQuestionReply = async (openQuestion) => {
450
+ await persistAuthoritativeRequestTerminal({
451
+ kind: "question",
452
+ routeKey: openQuestion.routeKey,
453
+ openRecord: asRecord(openQuestion),
454
+ status: "answered",
455
+ });
456
+ return `已回复问题:${openQuestion.handle}`;
457
+ };
458
+ const finalizePermissionReply = async (openPermission, reply) => {
459
+ await persistAuthoritativeRequestTerminal({
460
+ kind: "permission",
461
+ routeKey: openPermission.routeKey,
462
+ openRecord: asRecord(openPermission),
463
+ status: reply === "reject" ? "rejected" : "answered",
464
+ });
465
+ return `已处理权限请求:${openPermission.handle} (${reply})`;
466
+ };
467
+ const finalizeNaturalStopReply = async (openNaturalStop, handle) => {
468
+ await persistAuthoritativeNaturalStopReply({
469
+ handle: readNonEmptyString(openNaturalStop.handle) ?? handle,
470
+ });
471
+ return `已回复中止通知:${openNaturalStop.handle ?? handle}`;
472
+ };
473
+ const listDeadLettersByHandleSafely = async (handle) => {
474
+ try {
475
+ return await listDeadLettersByHandle(handle);
476
+ }
477
+ catch (error) {
478
+ if (isInvalidHandleError(error)) {
479
+ return [];
480
+ }
481
+ throw error;
482
+ }
483
+ };
484
+ const listRecoverableDeadLettersByHandleSafely = async (handle) => {
485
+ try {
486
+ return await listRecoverableDeadLettersByHandle(handle);
487
+ }
488
+ catch (error) {
489
+ if (isInvalidHandleError(error)) {
490
+ return [];
491
+ }
492
+ throw error;
493
+ }
494
+ };
495
+ const mapRecoveryFailure = (handle, error) => {
496
+ if (error instanceof Error) {
497
+ if (/request missing for recovery/i.test(error.message)) {
498
+ return {
499
+ recoveryErrorCode: "requestMissing",
500
+ recoveryErrorMessage: `无法恢复请求,原始记录不存在:${handle}`,
501
+ };
502
+ }
503
+ if (/request is not recoverable from current status/i.test(error.message)) {
504
+ return {
505
+ recoveryErrorCode: "requestNotRecoverable",
506
+ recoveryErrorMessage: `无法恢复请求,原始记录状态不可恢复:${handle}`,
507
+ };
508
+ }
509
+ if (/failed to allocate recovery routekey/i.test(error.message)) {
510
+ return {
511
+ recoveryErrorCode: "routeAllocationFailed",
512
+ recoveryErrorMessage: `无法恢复请求,无法分配新的路由:${handle}`,
513
+ };
514
+ }
515
+ }
516
+ return {
517
+ recoveryErrorCode: "recoveryFailed",
518
+ recoveryErrorMessage: `无法恢复请求:${handle}`,
519
+ };
520
+ };
521
+ const classifyMatchedDeadLetters = async (records) => {
522
+ const recoverableRouteKeys = new Set((await Promise.resolve(records.length > 0
523
+ ? listRecoverableDeadLettersByHandleSafely(records[0].handle)
524
+ : [])).map((record) => record.routeKey));
525
+ return Promise.all(records.map(async (record) => {
526
+ if (record.recoveryStatus === "recovered") {
527
+ return {
528
+ state: "ignored",
529
+ record,
530
+ };
531
+ }
532
+ if (!recoverableRouteKeys.has(record.routeKey)) {
533
+ return {
534
+ state: "invalid",
535
+ record,
536
+ returnDetailedMessage: false,
537
+ failure: {
538
+ recoveryErrorCode: "deadLetterNotRecoverable",
539
+ recoveryErrorMessage: `无法恢复请求,记录状态不可恢复:${record.handle}`,
540
+ },
541
+ };
542
+ }
543
+ const request = await findRequestByRouteKey({
544
+ kind: record.kind,
545
+ routeKey: record.routeKey,
546
+ });
547
+ if (!request) {
548
+ return {
549
+ state: "invalid",
550
+ record,
551
+ returnDetailedMessage: true,
552
+ failure: {
553
+ recoveryErrorCode: "requestMissing",
554
+ recoveryErrorMessage: `无法恢复请求,原始记录不存在:${record.handle}`,
555
+ },
556
+ };
557
+ }
558
+ if (request.status !== "expired" && request.status !== "cleaned") {
559
+ return {
560
+ state: "invalid",
561
+ record,
562
+ returnDetailedMessage: true,
563
+ failure: {
564
+ recoveryErrorCode: "requestNotRecoverable",
565
+ recoveryErrorMessage: `无法恢复请求,原始记录状态不可恢复:${record.handle}`,
566
+ },
567
+ };
568
+ }
569
+ return {
570
+ state: "valid",
571
+ record,
572
+ request,
573
+ };
574
+ }));
575
+ };
576
+ const prepareRecoveryMutation = async (handle) => {
577
+ const { matchedDeadLetters, recoverableMatches, invalidMatches } = await classifyRecoveryHandle(handle);
578
+ if (matchedDeadLetters.length === 0) {
579
+ return {
580
+ kind: "error",
581
+ message: `未找到可恢复的请求:${handle}`,
582
+ };
583
+ }
584
+ if (recoverableMatches.length === 0) {
585
+ if (invalidMatches.length > 0) {
586
+ await persistRecoveryFailures(invalidMatches.map((item) => ({
587
+ kind: item.record.kind,
588
+ routeKey: item.record.routeKey,
589
+ recoveryErrorCode: item.failure.recoveryErrorCode,
590
+ recoveryErrorMessage: item.failure.recoveryErrorMessage,
591
+ })));
592
+ }
593
+ if (invalidMatches.length === 1 &&
594
+ invalidMatches[0].returnDetailedMessage) {
595
+ return {
596
+ kind: "error",
597
+ message: invalidMatches[0].failure.recoveryErrorMessage,
598
+ };
599
+ }
600
+ return {
601
+ kind: "error",
602
+ message: `未找到可恢复的请求:${handle}`,
603
+ };
604
+ }
605
+ if (recoverableMatches.length > 1) {
606
+ return {
607
+ kind: "error",
608
+ message: await persistRecoveryFailure(recoverableMatches.map((item) => item.record), "ambiguousHandle", `找到多个可恢复的请求:${handle}`),
609
+ };
610
+ }
611
+ const recoverable = recoverableMatches[0];
612
+ const excludedHandles = await listRecoveryChainHandles({
613
+ kind: recoverable.record.kind,
614
+ requestID: recoverable.record.requestID,
615
+ wechatAccountId: recoverable.record.wechatAccountId,
616
+ userId: recoverable.record.userId,
617
+ });
618
+ const pendingNotifications = (await listPendingNotifications()).filter((record) => record.kind === recoverable.record.kind &&
619
+ record.routeKey === recoverable.record.routeKey);
620
+ return {
621
+ kind: "ready",
622
+ mutation: {
623
+ type: "recoveryMutation",
624
+ requestedHandle: handle,
625
+ deadLetter: recoverable.record,
626
+ originalRequest: recoverable.request,
627
+ pendingNotifications,
628
+ recoveryChainHandles: excludedHandles,
629
+ },
630
+ };
631
+ };
632
+ const findBareRecoveryHandle = async () => {
633
+ const recoverableRecords = await listRecoverableDeadLetters();
634
+ const classifiedRecoverableRecords = (await Promise.all(recoverableRecords.map(async (record) => ({
635
+ record,
636
+ classified: await classifyRecoveryHandle(record.handle),
637
+ })))).filter((item) => item.classified.recoverableMatches.some((match) => match.record.routeKey === item.record.routeKey));
638
+ if (classifiedRecoverableRecords.length === 1) {
639
+ return {
640
+ kind: "ready",
641
+ handle: classifiedRecoverableRecords[0].record.handle,
642
+ };
643
+ }
644
+ if (classifiedRecoverableRecords.length > 1) {
645
+ return {
646
+ kind: "error",
647
+ message: `找到多个可恢复的请求:${classifiedRecoverableRecords.map((item) => item.record.handle).join("、")}`,
648
+ };
649
+ }
650
+ const closedRecords = await listDeadLetters();
651
+ const failedRecord = closedRecords.find((record) => record.recoveryStatus === "failed" && record.recoveryErrorMessage);
652
+ if (failedRecord?.recoveryErrorMessage) {
653
+ return { kind: "error", message: failedRecord.recoveryErrorMessage };
654
+ }
655
+ return { kind: "error", message: "没有可恢复的请求" };
656
+ };
657
+ return async (command) => {
658
+ if (command.type === "status") {
659
+ return handleStatusCommand();
660
+ }
661
+ if (command.type === "todo") {
662
+ const brokerView = await readBrokerAuthoritativeView();
663
+ return formatTodoReplyFromBrokerView(brokerView);
664
+ }
665
+ if (command.type === "reply") {
666
+ const openQuestion = await findActiveRequestByHandle("question", command.handle);
667
+ if (!openQuestion) {
668
+ const terminalQuestion = await findLegacyRequestClosureByHandle("question", command.handle);
669
+ if (terminalQuestion) {
670
+ return formatBrokerLegacyHandleClosureText(terminalQuestion);
671
+ }
672
+ const openNaturalStop = await findActiveNaturalStopByHandle(command.handle);
673
+ if (openNaturalStop) {
674
+ const replyTarget = asRecord(openNaturalStop.replyTarget);
675
+ const instanceID = readNonEmptyString(replyTarget.instanceID) ??
676
+ readNonEmptyString(openNaturalStop.scopeKey) ??
677
+ readNonEmptyString(openNaturalStop.instanceID);
678
+ const sessionID = readNonEmptyString(replyTarget.sessionID) ??
679
+ readNonEmptyString(openNaturalStop.sessionID);
680
+ if (!instanceID || !sessionID) {
681
+ return `回复中止通知失败:bridge unavailable`;
682
+ }
683
+ const commandState = await readBrokerCommandStateByAction({
684
+ type: "replyNaturalStop",
685
+ target: {
686
+ instanceID,
687
+ sessionID,
688
+ },
689
+ payload: {
690
+ sessionID,
691
+ text: command.text,
692
+ },
693
+ });
694
+ const pendingMessage = commandState
695
+ ? commandStatusMessage(commandState.status)
696
+ : undefined;
697
+ if (pendingMessage) {
698
+ return pendingMessage;
699
+ }
700
+ if (commandState?.status === "completed") {
701
+ return finalizeNaturalStopReply(openNaturalStop, command.handle);
702
+ }
703
+ if (commandState?.status === "failed") {
704
+ return `回复中止通知失败:${readCommandFailureMessage(commandState)}`;
705
+ }
706
+ const mutationId = `reply-natural-stop-${Date.now()}-${Math.random().toString(16).slice(2)}`;
707
+ let result;
708
+ if (input.sendReplyNaturalStopRpc) {
709
+ try {
710
+ result = await input.sendReplyNaturalStopRpc({
711
+ instanceID,
712
+ mutationId,
713
+ sessionID,
714
+ handle: readNonEmptyString(openNaturalStop.handle) ?? command.handle,
715
+ text: command.text,
716
+ });
717
+ }
718
+ catch (error) {
719
+ result = {
720
+ mutationId,
721
+ ok: false,
722
+ errorMessage: toErrorMessage(error),
723
+ };
724
+ }
725
+ }
726
+ else {
727
+ result = {
728
+ mutationId,
729
+ ok: false,
730
+ errorMessage: "natural-stop reply unavailable",
731
+ };
732
+ }
733
+ if (result.ok !== true) {
734
+ return `回复中止通知失败:${result.errorMessage ?? "unknown"}`;
735
+ }
736
+ return finalizeNaturalStopReply(openNaturalStop, command.handle);
737
+ }
738
+ const terminalNaturalStop = await findLegacyNaturalStopClosureByHandle(command.handle);
739
+ if (terminalNaturalStop) {
740
+ return formatBrokerLegacyHandleClosureText(terminalNaturalStop);
741
+ }
742
+ const upgradeCloseReason = await readBrokerStateUpgradeCloseReason(command.handle);
743
+ if (upgradeCloseReason) {
744
+ return upgradeCloseReason;
745
+ }
746
+ return `未找到待回复问题:${command.handle}`;
747
+ }
748
+ const questionRequestID = readNonEmptyString(openQuestion.requestID);
749
+ const questionRouteKey = readNonEmptyString(openQuestion.routeKey);
750
+ const questionHandle = readNonEmptyString(openQuestion.handle) ?? command.handle;
751
+ const questionInstanceID = readNonEmptyString(openQuestion.scopeKey) ??
752
+ readNonEmptyString(openQuestion.instanceID);
753
+ let answers;
754
+ try {
755
+ const prompt = openQuestion.prompt;
756
+ answers = buildQuestionAnswersFromReply(prompt &&
757
+ typeof prompt === "object" &&
758
+ prompt !== null &&
759
+ "mode" in prompt
760
+ ? prompt
761
+ : undefined, command.text);
762
+ }
763
+ catch (error) {
764
+ return error instanceof Error ? error.message : "问题回复格式无效";
765
+ }
766
+ if (!questionRequestID) {
767
+ return `回复问题失败:bridge unavailable`;
768
+ }
769
+ const commandState = questionInstanceID && questionRequestID
770
+ ? await readBrokerCommandStateByAction({
771
+ type: "replyQuestion",
772
+ target: {
773
+ instanceID: questionInstanceID,
774
+ requestID: questionRequestID,
775
+ },
776
+ payload: {
777
+ requestID: questionRequestID,
778
+ answers,
779
+ },
780
+ })
781
+ : undefined;
782
+ const pendingMessage = commandState
783
+ ? commandStatusMessage(commandState.status)
784
+ : undefined;
785
+ if (pendingMessage) {
786
+ return pendingMessage;
787
+ }
788
+ if (commandState?.status === "completed") {
789
+ if (!questionRouteKey) {
790
+ return `已回复问题:${questionHandle}`;
791
+ }
792
+ return finalizeQuestionReply({
793
+ ...openQuestion,
794
+ routeKey: questionRouteKey,
795
+ handle: questionHandle,
796
+ });
797
+ }
798
+ if (commandState?.status === "failed") {
799
+ return `回复问题失败:${readCommandFailureMessage(commandState)}`;
800
+ }
801
+ const mutationId = `reply-question-${Date.now()}-${Math.random().toString(16).slice(2)}`;
802
+ let result;
803
+ if (input.sendReplyQuestionRpc) {
804
+ if (!questionInstanceID || !questionRequestID) {
805
+ return `回复问题失败:bridge unavailable`;
806
+ }
807
+ try {
808
+ result = await input.sendReplyQuestionRpc({
809
+ instanceID: questionInstanceID,
810
+ mutationId,
811
+ requestID: questionRequestID,
812
+ answers,
813
+ });
814
+ }
815
+ catch (error) {
816
+ result = {
817
+ mutationId,
818
+ ok: false,
819
+ errorMessage: toErrorMessage(error),
820
+ };
821
+ }
822
+ }
823
+ else {
824
+ const replyResult = await input.client?.question?.reply?.(withOptionalDirectory({
825
+ requestID: questionRequestID,
826
+ answers,
827
+ }, input.directory));
828
+ const replyError = getSdkMutationError(replyResult);
829
+ result = replyError
830
+ ? { mutationId, ok: false, errorMessage: replyError }
831
+ : { mutationId, ok: true };
832
+ }
833
+ if (result.ok !== true) {
834
+ return `回复问题失败:${result.errorMessage ?? "unknown"}`;
835
+ }
836
+ if (!questionRouteKey) {
837
+ return `已回复问题:${questionHandle}`;
838
+ }
839
+ return finalizeQuestionReply({
840
+ ...openQuestion,
841
+ routeKey: questionRouteKey,
842
+ handle: questionHandle,
843
+ });
844
+ }
845
+ if (command.type === "recover") {
846
+ const recoverHandle = command.handle
847
+ ? { kind: "ready", handle: command.handle }
848
+ : await findBareRecoveryHandle();
849
+ if (recoverHandle.kind === "error") {
850
+ return recoverHandle.message;
851
+ }
852
+ const prepared = await prepareRecoveryMutation(recoverHandle.handle);
853
+ if (prepared.kind === "error") {
854
+ return prepared.message;
855
+ }
856
+ const result = await mutationQueue.enqueue("recoveryMutation", () => executeRecoveryMutation(prepared.mutation, {
857
+ revalidate: async (mutation) => {
858
+ const { recoverableMatches, invalidMatches } = await classifyRecoveryHandle(mutation.requestedHandle);
859
+ if (recoverableMatches.length > 1) {
860
+ return {
861
+ ok: false,
862
+ message: await persistRecoveryFailure(recoverableMatches.map((item) => item.record), "ambiguousHandle", `找到多个可恢复的请求:${mutation.requestedHandle}`),
863
+ };
864
+ }
865
+ if (recoverableMatches.length === 0) {
866
+ return createQueuedInvalidRecoveryResult({
867
+ handle: mutation.requestedHandle,
868
+ invalidMatches,
869
+ });
870
+ }
871
+ if (recoverableMatches[0].record.routeKey !==
872
+ mutation.deadLetter.routeKey) {
873
+ return createQueuedInvalidRecoveryResult({
874
+ handle: mutation.requestedHandle,
875
+ invalidMatches,
876
+ });
877
+ }
878
+ const currentDeadLetter = await readDeadLetter(mutation.deadLetter.kind, mutation.deadLetter.routeKey);
879
+ if (!currentDeadLetter ||
880
+ currentDeadLetter.recoveryStatus === "recovered" ||
881
+ currentDeadLetter.reason !== "instanceStale" ||
882
+ !currentDeadLetter.wechatAccountId ||
883
+ !currentDeadLetter.userId) {
884
+ return {
885
+ ok: false,
886
+ message: `未找到可恢复的请求:${mutation.requestedHandle}`,
887
+ };
888
+ }
889
+ const currentRequest = await findRequestByRouteKey({
890
+ kind: mutation.originalRequest.kind,
891
+ routeKey: mutation.originalRequest.routeKey,
892
+ });
893
+ if (!currentRequest) {
894
+ throw new Error("request missing for recovery");
895
+ }
896
+ if (currentRequest.status !== "expired" &&
897
+ currentRequest.status !== "cleaned") {
898
+ throw new Error("request is not recoverable from current status");
899
+ }
900
+ return undefined;
901
+ },
902
+ suppressPendingNotifications: async (mutation) => {
903
+ await suppressPreparedPendingNotifications(mutation.pendingNotifications);
904
+ },
905
+ prepareFreshRecovery: async (mutation, recoveredAt) => prepareRecoveryRequestReopen({
906
+ kind: mutation.deadLetter.kind,
907
+ routeKey: mutation.deadLetter.routeKey,
908
+ recoveredAt,
909
+ bannedHandles: mutation.recoveryChainHandles,
910
+ }),
911
+ commitPreparedRecovery: async (preparedRecovery) => commitPreparedRecoveryRequestReopen(preparedRecovery),
912
+ rollbackPreparedRecovery: async (preparedRecovery) => rollbackPreparedRecoveryRequestReopen(preparedRecovery),
913
+ markRecovered: async ({ kind, routeKey, recoveredAt }) => {
914
+ await markDeadLetterRecovered({ kind, routeKey, recoveredAt });
915
+ },
916
+ markFailed: async ({ kind, routeKey, failure }) => {
917
+ await markDeadLetterRecoveryFailed({
918
+ kind,
919
+ routeKey,
920
+ recoveryErrorCode: failure.recoveryErrorCode,
921
+ recoveryErrorMessage: failure.recoveryErrorMessage,
922
+ });
923
+ },
924
+ mapFailure: (error) => mapRecoveryFailure(prepared.mutation.requestedHandle, error),
925
+ testHooks: input.recoveryTestHooks,
926
+ }));
927
+ if (!result.ok) {
928
+ return result.message;
929
+ }
930
+ return `已恢复请求:${result.recovered.handle}`;
931
+ }
932
+ if (command.type !== "allow") {
933
+ return "未知命令";
934
+ }
935
+ const openPermission = await findActiveRequestByHandle("permission", command.handle);
936
+ if (!openPermission) {
937
+ const terminalPermission = await findLegacyRequestClosureByHandle("permission", command.handle);
938
+ if (terminalPermission) {
939
+ return formatBrokerLegacyHandleClosureText(terminalPermission);
940
+ }
941
+ const upgradeCloseReason = await readBrokerStateUpgradeCloseReason(command.handle);
942
+ if (upgradeCloseReason) {
943
+ return upgradeCloseReason;
944
+ }
945
+ return `未找到待处理权限请求:${command.handle}`;
946
+ }
947
+ const permissionRequestID = readNonEmptyString(openPermission.requestID);
948
+ const permissionRouteKey = readNonEmptyString(openPermission.routeKey);
949
+ const permissionHandle = readNonEmptyString(openPermission.handle) ?? command.handle;
950
+ const permissionInstanceID = readNonEmptyString(openPermission.scopeKey) ??
951
+ readNonEmptyString(openPermission.instanceID);
952
+ if (!permissionRequestID) {
953
+ return `处理权限请求失败:bridge unavailable`;
954
+ }
955
+ const commandState = permissionInstanceID && permissionRequestID
956
+ ? await readBrokerCommandStateByAction({
957
+ type: "replyPermission",
958
+ target: {
959
+ instanceID: permissionInstanceID,
960
+ requestID: permissionRequestID,
961
+ },
962
+ payload: {
963
+ requestID: permissionRequestID,
964
+ reply: command.reply,
965
+ ...(command.message ? { message: command.message } : {}),
966
+ },
967
+ })
968
+ : undefined;
969
+ const pendingMessage = commandState
970
+ ? commandStatusMessage(commandState.status)
971
+ : undefined;
972
+ if (pendingMessage) {
973
+ return pendingMessage;
974
+ }
975
+ if (commandState?.status === "completed") {
976
+ if (!permissionRouteKey) {
977
+ return `已处理权限请求:${permissionHandle} (${command.reply})`;
978
+ }
979
+ return finalizePermissionReply({
980
+ ...openPermission,
981
+ routeKey: permissionRouteKey,
982
+ handle: permissionHandle,
983
+ }, command.reply);
984
+ }
985
+ if (commandState?.status === "failed") {
986
+ return `处理权限请求失败:${readCommandFailureMessage(commandState)}`;
987
+ }
988
+ const mutationId = `reply-permission-${Date.now()}-${Math.random().toString(16).slice(2)}`;
989
+ let result;
990
+ if (input.sendReplyPermissionRpc) {
991
+ if (!permissionInstanceID || !permissionRequestID) {
992
+ return `处理权限请求失败:bridge unavailable`;
993
+ }
994
+ try {
995
+ result = await input.sendReplyPermissionRpc({
996
+ instanceID: permissionInstanceID,
997
+ mutationId,
998
+ requestID: permissionRequestID,
999
+ reply: command.reply,
1000
+ ...(command.message ? { message: command.message } : {}),
1001
+ });
1002
+ }
1003
+ catch (error) {
1004
+ result = { mutationId, ok: false, errorMessage: toErrorMessage(error) };
1005
+ }
1006
+ }
1007
+ else {
1008
+ const permissionReplyResult = await input.client?.permission?.reply?.(withOptionalDirectory({
1009
+ requestID: permissionRequestID,
1010
+ reply: command.reply,
1011
+ ...(command.message ? { message: command.message } : {}),
1012
+ }, input.directory));
1013
+ const permissionReplyError = getSdkMutationError(permissionReplyResult);
1014
+ result = permissionReplyError
1015
+ ? { mutationId, ok: false, errorMessage: permissionReplyError }
1016
+ : { mutationId, ok: true };
1017
+ }
1018
+ if (result.ok !== true) {
1019
+ return `处理权限请求失败:${result.errorMessage ?? "unknown"}`;
1020
+ }
1021
+ await input.permissionMutationTestHooks?.beforeFinalizePermission?.({
1022
+ routeKey: permissionRouteKey ?? "",
1023
+ handle: permissionHandle,
1024
+ });
1025
+ if (!permissionRouteKey) {
1026
+ return `已处理权限请求:${permissionHandle} (${command.reply})`;
1027
+ }
1028
+ return finalizePermissionReply({
1029
+ ...openPermission,
1030
+ routeKey: permissionRouteKey,
1031
+ handle: permissionHandle,
1032
+ }, command.reply);
1033
+ };
1034
+ }
1035
+ export function createBrokerWechatStatusRuntimeLifecycle(deps = {}) {
1036
+ const onRuntimeError = deps.onRuntimeError ?? ((error) => console.error(error));
1037
+ const stateRoot = deps.stateRoot ?? wechatStateRoot();
1038
+ const onDiagnosticEvent = deps.onDiagnosticEvent ??
1039
+ createWechatStatusRuntimeDiagnosticsFileWriter({
1040
+ stateRoot,
1041
+ onRuntimeError,
1042
+ });
1043
+ const handleWechatSlashCommand = deps.handleWechatSlashCommand ??
1044
+ createBrokerWechatSlashCommandHandler({
1045
+ sendReplyQuestionRpc: deps.sendReplyQuestionRpc,
1046
+ sendReplyPermissionRpc: deps.sendReplyPermissionRpc,
1047
+ sendReplyNaturalStopRpc: deps.sendReplyNaturalStopRpc,
1048
+ directory: process.cwd(),
1049
+ });
1050
+ const createStatusRuntime = deps.createStatusRuntime ??
1051
+ ((statusRuntimeDeps) => createWechatStatusRuntime({
1052
+ onSlashCommand: async ({ command }) => statusRuntimeDeps.onSlashCommand({ command }),
1053
+ onRuntimeError,
1054
+ onDiagnosticEvent: statusRuntimeDeps.onDiagnosticEvent,
1055
+ drainOutboundMessages: async (drainInput) => {
1056
+ await statusRuntimeDeps.drainOutboundMessages({
1057
+ sendMessage: async (message) => {
1058
+ await drainInput.sendMessage(message);
1059
+ },
1060
+ });
1061
+ },
1062
+ }));
1063
+ const createNotificationDispatcher = deps.createNotificationDispatcher ?? createWechatNotificationDispatcher;
1064
+ let runtime = null;
1065
+ let dispatcher = null;
1066
+ return {
1067
+ start: async () => {
1068
+ if (runtime) {
1069
+ return;
1070
+ }
1071
+ let runtimeSendMessage = null;
1072
+ dispatcher = createNotificationDispatcher({
1073
+ sendMessage: async (message) => {
1074
+ if (!runtimeSendMessage) {
1075
+ throw new Error("wechat runtime send helper unavailable");
1076
+ }
1077
+ await runtimeSendMessage(message);
1078
+ },
1079
+ onDeliveryFailed: async (failure) => {
1080
+ if (!deps.handleNotificationDeliveryFailure) {
1081
+ return;
1082
+ }
1083
+ if (failure.kind === "sessionError") {
1084
+ return;
1085
+ }
1086
+ const requestKind = failure.kind === "requestTerminal"
1087
+ ? failure.requestKind
1088
+ : failure.kind === "question" || failure.kind === "permission"
1089
+ ? failure.kind
1090
+ : undefined;
1091
+ const immutableScopeKey = typeof failure.scopeKey === "string" &&
1092
+ failure.scopeKey.trim().length > 0
1093
+ ? failure.scopeKey
1094
+ : undefined;
1095
+ const brokerView = !immutableScopeKey &&
1096
+ requestKind &&
1097
+ typeof failure.routeKey === "string" &&
1098
+ failure.routeKey.trim().length > 0
1099
+ ? readLiveBrokerAuthoritativeView()
1100
+ : undefined;
1101
+ const authoritativeRequest = brokerView
1102
+ ? Object.values(requestKind === "question"
1103
+ ? brokerView.active.questions
1104
+ : brokerView.active.permissions)
1105
+ .map((item) => asRecord(item))
1106
+ .find((item) => readNonEmptyString(item.routeKey) === failure.routeKey)
1107
+ : undefined;
1108
+ const instanceID = immutableScopeKey ??
1109
+ readNonEmptyString(authoritativeRequest?.scopeKey) ??
1110
+ readNonEmptyString(authoritativeRequest?.instanceID);
1111
+ if (!instanceID) {
1112
+ return;
1113
+ }
1114
+ await deps.handleNotificationDeliveryFailure({
1115
+ instanceID,
1116
+ wechatAccountId: failure.wechatAccountId,
1117
+ userId: failure.userId,
1118
+ registrationEpoch: failure.registrationEpoch,
1119
+ });
1120
+ },
1121
+ });
1122
+ const created = createStatusRuntime({
1123
+ onSlashCommand: async ({ command }) => handleWechatSlashCommand(command),
1124
+ onDiagnosticEvent,
1125
+ drainOutboundMessages: async (runtimeDrainInput) => {
1126
+ if (runtimeDrainInput?.sendMessage) {
1127
+ runtimeSendMessage = runtimeDrainInput.sendMessage;
1128
+ }
1129
+ if (!dispatcher) {
1130
+ return;
1131
+ }
1132
+ await dispatcher.drainOutboundMessages();
1133
+ },
1134
+ });
1135
+ runtime = created;
1136
+ try {
1137
+ await created.start();
1138
+ }
1139
+ catch (error) {
1140
+ onRuntimeError(error);
1141
+ }
1142
+ },
1143
+ close: async () => {
1144
+ if (!runtime) {
1145
+ return;
1146
+ }
1147
+ const active = runtime;
1148
+ runtime = null;
1149
+ dispatcher = null;
1150
+ await active.close().catch((error) => {
1151
+ onRuntimeError(error);
1152
+ });
1153
+ },
1154
+ };
1155
+ }
1156
+ function removeOwnedBrokerStateFileSync(ownership, stateRoot) {
1157
+ try {
1158
+ const filePath = brokerStatePathForRoot(stateRoot);
1159
+ const raw = readFileSync(filePath, "utf8");
1160
+ const parsed = JSON.parse(raw);
1161
+ if (!isBrokerStateOwnedBy(parsed, ownership)) {
1162
+ return;
1163
+ }
1164
+ rmSync(filePath, { force: true });
1165
+ }
1166
+ catch {
1167
+ // ignore cleanup errors on shutdown
1168
+ }
1169
+ }
1170
+ async function run() {
1171
+ const args = process.argv.slice(2);
1172
+ const endpoint = parseEndpointArg(args);
1173
+ const stateRoot = parseStateRootArg(args);
1174
+ process.env.WECHAT_STATE_ROOT_OVERRIDE = stateRoot;
1175
+ await prepareBrokerStateStoreForStartup({
1176
+ protocolVersion: WECHAT_BROKER_WS_PROTOCOL_VERSION,
1177
+ stateGeneration: WECHAT_BROKER_WS_STATE_GENERATION,
1178
+ });
1179
+ const server = await startBrokerServer(endpoint);
1180
+ const version = await readPackageVersion();
1181
+ const state = {
1182
+ pid: process.pid,
1183
+ endpoint: server.endpoint,
1184
+ startedAt: server.startedAt,
1185
+ version,
1186
+ };
1187
+ await writeBrokerState(state, stateRoot);
1188
+ const wechatRuntimeLifecycle = createBrokerWechatStatusRuntimeLifecycle({
1189
+ handleWechatSlashCommand: createBrokerWechatSlashCommandHandler({
1190
+ sendReplyQuestionRpc: server.dispatchReplyQuestionToInstance,
1191
+ sendReplyPermissionRpc: server.dispatchReplyPermissionToInstance,
1192
+ sendReplyNaturalStopRpc: server.dispatchReplyNaturalStopToInstance,
1193
+ directory: stateRoot,
1194
+ }),
1195
+ sendReplyQuestionRpc: server.dispatchReplyQuestionToInstance,
1196
+ sendReplyPermissionRpc: server.dispatchReplyPermissionToInstance,
1197
+ sendReplyNaturalStopRpc: server.dispatchReplyNaturalStopToInstance,
1198
+ handleNotificationDeliveryFailure: server.handleNotificationDeliveryFailure,
1199
+ });
1200
+ const ownership = {
1201
+ pid: state.pid,
1202
+ startedAt: state.startedAt,
1203
+ version: state.version,
1204
+ endpoint: state.endpoint,
1205
+ };
1206
+ const idleTimeoutMs = toPositiveNumber(process.env.WECHAT_BROKER_IDLE_TIMEOUT_MS, DEFAULT_BROKER_IDLE_TIMEOUT_MS);
1207
+ const idleScanIntervalMs = toPositiveNumber(process.env.WECHAT_BROKER_IDLE_SCAN_INTERVAL_MS, DEFAULT_BROKER_IDLE_SCAN_INTERVAL_MS);
1208
+ const ownershipScanIntervalMs = toPositiveNumber(process.env.WECHAT_BROKER_OWNERSHIP_SCAN_INTERVAL_MS, DEFAULT_BROKER_OWNERSHIP_SCAN_INTERVAL_MS);
1209
+ let shuttingDown = false;
1210
+ const ownerEstablished = true;
1211
+ let ownershipScanInFlight = false;
1212
+ let runtimeStartTimer;
1213
+ const shutdown = async (exitCode = 0) => {
1214
+ if (shuttingDown) {
1215
+ return;
1216
+ }
1217
+ shuttingDown = true;
1218
+ clearTimeout(runtimeStartTimer);
1219
+ clearInterval(idleTimer);
1220
+ clearInterval(ownershipTimer);
1221
+ removeOwnedBrokerStateFileSync(ownership, stateRoot);
1222
+ await wechatRuntimeLifecycle.close();
1223
+ await server.close();
1224
+ process.exit(exitCode);
1225
+ };
1226
+ const shutdownIfBrokerOwnershipLost = async () => {
1227
+ const stillOwned = await brokerStateStillOwnedBy({
1228
+ stateRoot,
1229
+ ...ownership,
1230
+ });
1231
+ if (shuttingDown) {
1232
+ return true;
1233
+ }
1234
+ if (!shouldExitForLostBrokerOwnership({
1235
+ ownerEstablished,
1236
+ stillOwned,
1237
+ })) {
1238
+ return false;
1239
+ }
1240
+ await shutdown(0);
1241
+ return true;
1242
+ };
1243
+ if (shouldEnableBrokerWechatStatusRuntime()) {
1244
+ runtimeStartTimer = setTimeout(() => {
1245
+ if (shuttingDown) {
1246
+ return;
1247
+ }
1248
+ void shutdownIfBrokerOwnershipLost().then((ownershipLost) => {
1249
+ if (ownershipLost || shuttingDown) {
1250
+ return;
1251
+ }
1252
+ void wechatRuntimeLifecycle.start();
1253
+ });
1254
+ }, BROKER_WECHAT_RUNTIME_AUTOSTART_DELAY_MS);
1255
+ }
1256
+ const ownershipTimer = setInterval(() => {
1257
+ if (!ownerEstablished || ownershipScanInFlight) {
1258
+ return;
1259
+ }
1260
+ ownershipScanInFlight = true;
1261
+ void shutdownIfBrokerOwnershipLost().finally(() => {
1262
+ ownershipScanInFlight = false;
1263
+ });
1264
+ }, ownershipScanIntervalMs);
1265
+ let idleSince;
1266
+ const idleTimer = setInterval(() => {
1267
+ void server
1268
+ .hasBlockingActivity()
1269
+ .then((hasBlockingActivity) => {
1270
+ if (hasBlockingActivity) {
1271
+ idleSince = undefined;
1272
+ return;
1273
+ }
1274
+ const now = Date.now();
1275
+ if (idleSince === undefined) {
1276
+ idleSince = now;
1277
+ return;
1278
+ }
1279
+ if (now - idleSince >= idleTimeoutMs) {
1280
+ void shutdown(0);
1281
+ }
1282
+ })
1283
+ .catch(() => { });
1284
+ }, idleScanIntervalMs);
1285
+ process.once("SIGINT", () => {
1286
+ void shutdown(0);
1287
+ });
1288
+ process.once("SIGTERM", () => {
1289
+ void shutdown(0);
1290
+ });
1291
+ if (process.env.WECHAT_BROKER_EXIT_ON_STDIN_EOF === "1") {
1292
+ process.stdin.on("end", () => {
1293
+ void shutdown(0);
1294
+ });
1295
+ process.stdin.resume();
1296
+ }
1297
+ process.once("uncaughtException", (error) => {
1298
+ console.error(error);
1299
+ void shutdown(1);
1300
+ });
1301
+ process.once("unhandledRejection", (error) => {
1302
+ console.error(error);
1303
+ void shutdown(1);
1304
+ });
1305
+ process.on("exit", () => {
1306
+ removeOwnedBrokerStateFileSync(ownership, stateRoot);
1307
+ });
1308
+ }
1309
+ function isDirectRun() {
1310
+ if (!process.argv[1]) {
1311
+ return false;
1312
+ }
1313
+ return (path.resolve(process.argv[1]) ===
1314
+ path.resolve(fileURLToPath(import.meta.url)));
1315
+ }
1316
+ if (isDirectRun()) {
1317
+ void run().catch((error) => {
1318
+ console.error(error);
1319
+ process.exit(1);
1320
+ });
1321
+ }