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,518 @@
1
+ import { loadOpenClawWeixinPublicHelpers, } from "./compat/openclaw-public-helpers.js";
2
+ import { STAGE_A_SLASH_ONLY_MESSAGE } from "./compat/slash-guard.js";
3
+ import { parseWechatSlashCommand } from "./command-parser.js";
4
+ import { redactDebugBundleText } from "./debug-bundle-redaction.js";
5
+ import { upsertInboundToken } from "./token-store.js";
6
+ const DEFAULT_RETRY_DELAY_MS = 1_000;
7
+ const DEFAULT_LONG_POLL_TIMEOUT_MS = 25_000;
8
+ const MAX_HELPER_FAILURE_BACKOFF_MS = 30_000;
9
+ export const DEFAULT_NON_SLASH_REPLY_TEXT = STAGE_A_SLASH_ONLY_MESSAGE;
10
+ export const DEFAULT_SLASH_HANDLER_ERROR_REPLY_TEXT = "命令处理失败,请稍后重试。";
11
+ function createAbortError() {
12
+ const error = new Error("wechat status runtime stopped");
13
+ error.name = "AbortError";
14
+ return error;
15
+ }
16
+ function isAbortError(error) {
17
+ return error instanceof Error && error.name === "AbortError";
18
+ }
19
+ function withAbort(promise, signal) {
20
+ if (signal.aborted) {
21
+ return Promise.reject(createAbortError());
22
+ }
23
+ return new Promise((resolve, reject) => {
24
+ let settled = false;
25
+ const cleanup = () => {
26
+ signal.removeEventListener("abort", onAbort);
27
+ };
28
+ const onAbort = () => {
29
+ if (settled) {
30
+ return;
31
+ }
32
+ settled = true;
33
+ cleanup();
34
+ reject(createAbortError());
35
+ };
36
+ signal.addEventListener("abort", onAbort, { once: true });
37
+ promise.then((value) => {
38
+ if (settled) {
39
+ return;
40
+ }
41
+ settled = true;
42
+ cleanup();
43
+ resolve(value);
44
+ }, (error) => {
45
+ if (settled) {
46
+ return;
47
+ }
48
+ settled = true;
49
+ cleanup();
50
+ reject(error);
51
+ });
52
+ });
53
+ }
54
+ function sleep(ms, signal) {
55
+ if (signal.aborted) {
56
+ return Promise.reject(createAbortError());
57
+ }
58
+ return new Promise((resolve, reject) => {
59
+ const timer = setTimeout(() => {
60
+ signal.removeEventListener("abort", onAbort);
61
+ resolve();
62
+ }, ms);
63
+ const onAbort = () => {
64
+ clearTimeout(timer);
65
+ signal.removeEventListener("abort", onAbort);
66
+ reject(createAbortError());
67
+ };
68
+ signal.addEventListener("abort", onAbort, { once: true });
69
+ });
70
+ }
71
+ function normalizePositiveInteger(value, fallback) {
72
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
73
+ return fallback;
74
+ }
75
+ return Math.floor(value);
76
+ }
77
+ function computeHelperFailureBackoffMs(retryDelayMs, consecutiveFailures) {
78
+ return Math.min(retryDelayMs * 2 ** Math.max(0, consecutiveFailures - 1), MAX_HELPER_FAILURE_BACKOFF_MS);
79
+ }
80
+ function cloneHelperFailureState(state) {
81
+ if (!state) {
82
+ return null;
83
+ }
84
+ return { ...state };
85
+ }
86
+ function extractMessageText(message) {
87
+ for (const item of message.item_list ?? []) {
88
+ if (item?.type !== 1) {
89
+ continue;
90
+ }
91
+ if (typeof item.text_item?.text === "string" && item.text_item.text.trim().length > 0) {
92
+ return item.text_item.text;
93
+ }
94
+ }
95
+ return "";
96
+ }
97
+ function toNonEmptyString(value) {
98
+ if (typeof value !== "string") {
99
+ return null;
100
+ }
101
+ const trimmed = value.trim();
102
+ return trimmed.length > 0 ? trimmed : null;
103
+ }
104
+ function toErrorMessage(error) {
105
+ if (error instanceof Error) {
106
+ return error.message;
107
+ }
108
+ if (typeof error === "string") {
109
+ return error;
110
+ }
111
+ return String(error);
112
+ }
113
+ function sanitizeRuntimeDiagnosticError(error) {
114
+ return redactDebugBundleText(error, "diagnostics/wechat-status-runtime.runtime-error.txt");
115
+ }
116
+ function toRuntimeDiagnosticError(error) {
117
+ return sanitizeRuntimeDiagnosticError(toErrorMessage(error));
118
+ }
119
+ function toInboundTokenSource(command) {
120
+ if (command?.type === "reply") {
121
+ return "question";
122
+ }
123
+ if (command?.type === "allow") {
124
+ return "permission";
125
+ }
126
+ return "message";
127
+ }
128
+ export function createWechatStatusRuntime(input = {}) {
129
+ const loadPublicHelpers = input.loadPublicHelpers ?? loadOpenClawWeixinPublicHelpers;
130
+ const onSlashCommand = input.onSlashCommand ??
131
+ (async () => {
132
+ return "/status 处理中";
133
+ });
134
+ const onRuntimeError = input.onRuntimeError ?? (() => { });
135
+ const onDiagnosticEvent = input.onDiagnosticEvent ?? (() => { });
136
+ const retryDelayMs = normalizePositiveInteger(input.retryDelayMs, DEFAULT_RETRY_DELAY_MS);
137
+ const longPollTimeoutMs = normalizePositiveInteger(input.longPollTimeoutMs, DEFAULT_LONG_POLL_TIMEOUT_MS);
138
+ const now = input.now ?? Date.now;
139
+ const sleepImpl = input.sleepImpl ?? sleep;
140
+ const onFailureStateChange = input.onFailureStateChange ?? (() => { });
141
+ const shouldReloadState = input.shouldReloadState ?? (() => false);
142
+ const drainOutboundMessages = input.drainOutboundMessages;
143
+ let started = false;
144
+ let closed = false;
145
+ let stopController = null;
146
+ let pollingTask = null;
147
+ let outboundDrainTask = null;
148
+ let helperFailureState = null;
149
+ let activeRuntimeState = null;
150
+ let inFlightDrain = null;
151
+ const retainedHelperFailureObjects = new Set();
152
+ const emitDiagnosticEvent = (event) => {
153
+ void Promise.resolve()
154
+ .then(() => onDiagnosticEvent(event))
155
+ .catch((error) => {
156
+ onRuntimeError(error);
157
+ });
158
+ };
159
+ const emitFailureStateChange = (state) => {
160
+ const snapshot = cloneHelperFailureState(state);
161
+ void Promise.resolve()
162
+ .then(() => onFailureStateChange(snapshot))
163
+ .catch((error) => {
164
+ onRuntimeError(error);
165
+ });
166
+ };
167
+ const replaceHelperFailureState = (nextState) => {
168
+ if (helperFailureState) {
169
+ retainedHelperFailureObjects.delete(helperFailureState);
170
+ }
171
+ helperFailureState = nextState;
172
+ if (helperFailureState) {
173
+ retainedHelperFailureObjects.add(helperFailureState);
174
+ }
175
+ emitFailureStateChange(helperFailureState);
176
+ };
177
+ const clearHelperFailureState = () => {
178
+ if (!helperFailureState && retainedHelperFailureObjects.size === 0) {
179
+ return;
180
+ }
181
+ replaceHelperFailureState(null);
182
+ };
183
+ const drainOutboundWithState = async (initialized, signal) => {
184
+ if (!drainOutboundMessages) {
185
+ return;
186
+ }
187
+ await withAbort(drainOutboundMessages({
188
+ sendMessage: async (message) => {
189
+ await initialized.helpers.sendMessageWeixin({
190
+ to: message.to,
191
+ text: message.text,
192
+ opts: {
193
+ baseUrl: initialized.baseUrl,
194
+ token: initialized.token,
195
+ ...(typeof message.contextToken === "string" && message.contextToken.trim().length > 0
196
+ ? { contextToken: message.contextToken }
197
+ : {}),
198
+ },
199
+ });
200
+ },
201
+ }), signal);
202
+ };
203
+ const drainCurrentOutboundMessages = async (signal) => {
204
+ if (!drainOutboundMessages || !activeRuntimeState) {
205
+ return;
206
+ }
207
+ if (inFlightDrain) {
208
+ return inFlightDrain;
209
+ }
210
+ const currentState = activeRuntimeState;
211
+ const currentDrain = drainOutboundWithState(currentState, signal)
212
+ .catch((error) => {
213
+ if (isAbortError(error)) {
214
+ return;
215
+ }
216
+ emitRuntimeErrorDiagnostic("drainOutboundMessages", error);
217
+ onRuntimeError(error);
218
+ })
219
+ .finally(() => {
220
+ if (inFlightDrain === currentDrain) {
221
+ inFlightDrain = null;
222
+ }
223
+ });
224
+ inFlightDrain = currentDrain;
225
+ return currentDrain;
226
+ };
227
+ const pollOutboundMessages = async (signal) => {
228
+ if (!drainOutboundMessages) {
229
+ return;
230
+ }
231
+ while (!signal.aborted) {
232
+ await drainCurrentOutboundMessages(signal);
233
+ try {
234
+ await sleepImpl(retryDelayMs, signal);
235
+ }
236
+ catch (error) {
237
+ if (isAbortError(error)) {
238
+ return;
239
+ }
240
+ onRuntimeError(error);
241
+ }
242
+ }
243
+ };
244
+ const noteLoadPublicHelpersFailure = () => {
245
+ const consecutiveFailures = (helperFailureState?.consecutiveFailures ?? 0) + 1;
246
+ const currentBackoffMs = computeHelperFailureBackoffMs(retryDelayMs, consecutiveFailures);
247
+ const lastFailureAtMs = now();
248
+ const nextState = {
249
+ stage: "loadPublicHelpers",
250
+ consecutiveFailures,
251
+ currentBackoffMs,
252
+ retryState: "backing-off",
253
+ reachedGetUpdates: false,
254
+ lastFailureAtMs,
255
+ nextRetryAtMs: lastFailureAtMs + currentBackoffMs,
256
+ };
257
+ replaceHelperFailureState(nextState);
258
+ return nextState;
259
+ };
260
+ const emitRuntimeErrorDiagnostic = (stage, error, options) => {
261
+ const runtimeError = toRuntimeDiagnosticError(error);
262
+ if (stage === "loadPublicHelpers") {
263
+ emitDiagnosticEvent({
264
+ type: "runtimeError",
265
+ stage,
266
+ error: runtimeError,
267
+ consecutiveFailures: options?.consecutiveFailures ?? 1,
268
+ backoffMs: options?.backoffMs ?? retryDelayMs,
269
+ retryState: options?.retryState ?? "backing-off",
270
+ reachedGetUpdates: false,
271
+ });
272
+ return;
273
+ }
274
+ emitDiagnosticEvent({
275
+ type: "runtimeError",
276
+ stage,
277
+ error: runtimeError,
278
+ reachedGetUpdates: true,
279
+ });
280
+ };
281
+ const poll = async (signal) => {
282
+ let initialized = null;
283
+ while (!signal.aborted) {
284
+ let nextRetryDelayMs = retryDelayMs;
285
+ try {
286
+ let justInitialized = false;
287
+ if (!initialized) {
288
+ try {
289
+ const helpers = await withAbort(loadPublicHelpers(input.publicHelpersOptions), signal);
290
+ const latestAccountState = helpers.latestAccountState;
291
+ if (!latestAccountState) {
292
+ throw new Error("missing wechat account state");
293
+ }
294
+ initialized = {
295
+ helpers,
296
+ accountId: latestAccountState.accountId,
297
+ baseUrl: latestAccountState.baseUrl,
298
+ token: latestAccountState.token,
299
+ getUpdatesBuf: typeof latestAccountState.getUpdatesBuf === "string" ? latestAccountState.getUpdatesBuf : "",
300
+ };
301
+ activeRuntimeState = initialized;
302
+ clearHelperFailureState();
303
+ justInitialized = true;
304
+ }
305
+ catch (error) {
306
+ if (isAbortError(error)) {
307
+ return;
308
+ }
309
+ const failureState = noteLoadPublicHelpersFailure();
310
+ nextRetryDelayMs = failureState.currentBackoffMs;
311
+ emitRuntimeErrorDiagnostic("loadPublicHelpers", error, {
312
+ consecutiveFailures: failureState.consecutiveFailures,
313
+ backoffMs: failureState.currentBackoffMs,
314
+ retryState: failureState.retryState,
315
+ });
316
+ throw error;
317
+ }
318
+ }
319
+ if (!justInitialized && initialized && shouldReloadState({
320
+ accountId: initialized.accountId,
321
+ baseUrl: initialized.baseUrl,
322
+ token: initialized.token,
323
+ getUpdatesBuf: initialized.getUpdatesBuf,
324
+ })) {
325
+ initialized = null;
326
+ activeRuntimeState = null;
327
+ continue;
328
+ }
329
+ let response;
330
+ try {
331
+ response = await withAbort(initialized.helpers.getUpdates({
332
+ baseUrl: initialized.baseUrl,
333
+ token: initialized.token,
334
+ get_updates_buf: initialized.getUpdatesBuf,
335
+ timeoutMs: longPollTimeoutMs,
336
+ }), signal);
337
+ }
338
+ catch (error) {
339
+ if (isAbortError(error)) {
340
+ return;
341
+ }
342
+ emitRuntimeErrorDiagnostic("getUpdates", error);
343
+ throw error;
344
+ }
345
+ // 语义锁定:一旦服务端返回新的 get_updates_buf,立即推进游标;
346
+ // 后续轮询即便失败,也不会回滚到旧 buf。
347
+ if (typeof response.get_updates_buf === "string") {
348
+ initialized.getUpdatesBuf = response.get_updates_buf;
349
+ if (typeof initialized.helpers.persistGetUpdatesBuf === "function") {
350
+ try {
351
+ await withAbort(initialized.helpers.persistGetUpdatesBuf({
352
+ accountId: initialized.accountId,
353
+ getUpdatesBuf: response.get_updates_buf,
354
+ }), signal);
355
+ }
356
+ catch (error) {
357
+ if (isAbortError(error)) {
358
+ return;
359
+ }
360
+ emitRuntimeErrorDiagnostic("persistGetUpdatesBuf", error);
361
+ onRuntimeError(error);
362
+ }
363
+ }
364
+ }
365
+ const messages = Array.isArray(response.msgs) ? response.msgs : [];
366
+ await drainCurrentOutboundMessages(signal);
367
+ for (const message of messages) {
368
+ if (signal.aborted) {
369
+ return;
370
+ }
371
+ const to = toNonEmptyString(message.from_user_id);
372
+ const text = extractMessageText(message);
373
+ const hasText = text.trim().length > 0;
374
+ if (!to) {
375
+ emitDiagnosticEvent({
376
+ type: "messageSkipped",
377
+ reason: "missingFromUserId",
378
+ hasFromUserId: false,
379
+ hasText,
380
+ });
381
+ continue;
382
+ }
383
+ if (!hasText) {
384
+ emitDiagnosticEvent({
385
+ type: "messageSkipped",
386
+ reason: "missingText",
387
+ hasFromUserId: true,
388
+ hasText: false,
389
+ });
390
+ continue;
391
+ }
392
+ const parsedCommand = parseWechatSlashCommand(text);
393
+ const inboundContextToken = toNonEmptyString(message.context_token) ?? undefined;
394
+ let replyText = DEFAULT_NON_SLASH_REPLY_TEXT;
395
+ if (parsedCommand) {
396
+ if (inboundContextToken) {
397
+ try {
398
+ await upsertInboundToken({
399
+ wechatAccountId: initialized.accountId,
400
+ userId: to,
401
+ contextToken: inboundContextToken,
402
+ updatedAt: Date.now(),
403
+ source: toInboundTokenSource(parsedCommand),
404
+ });
405
+ }
406
+ catch (error) {
407
+ onRuntimeError(error);
408
+ }
409
+ }
410
+ emitDiagnosticEvent({
411
+ type: "slashCommandRecognized",
412
+ command: parsedCommand,
413
+ text,
414
+ to,
415
+ });
416
+ try {
417
+ replyText = await onSlashCommand({
418
+ command: parsedCommand,
419
+ text,
420
+ message,
421
+ });
422
+ }
423
+ catch (error) {
424
+ onRuntimeError(error);
425
+ replyText = DEFAULT_SLASH_HANDLER_ERROR_REPLY_TEXT;
426
+ }
427
+ }
428
+ try {
429
+ await withAbort(initialized.helpers.sendMessageWeixin({
430
+ to,
431
+ text: replyText,
432
+ opts: {
433
+ baseUrl: initialized.baseUrl,
434
+ token: initialized.token,
435
+ contextToken: toNonEmptyString(message.context_token) ?? undefined,
436
+ },
437
+ }), signal);
438
+ }
439
+ catch (error) {
440
+ if (isAbortError(error)) {
441
+ return;
442
+ }
443
+ emitDiagnosticEvent({
444
+ type: "replySendFailed",
445
+ to,
446
+ error: toErrorMessage(error),
447
+ commandType: parsedCommand?.type ?? null,
448
+ });
449
+ emitRuntimeErrorDiagnostic("sendReplyMessage", error);
450
+ onRuntimeError(error);
451
+ }
452
+ }
453
+ }
454
+ catch (error) {
455
+ if (isAbortError(error)) {
456
+ return;
457
+ }
458
+ onRuntimeError(error);
459
+ if (signal.aborted || closed) {
460
+ return;
461
+ }
462
+ try {
463
+ await sleepImpl(nextRetryDelayMs, signal);
464
+ }
465
+ catch (sleepError) {
466
+ if (isAbortError(sleepError)) {
467
+ return;
468
+ }
469
+ onRuntimeError(sleepError);
470
+ }
471
+ }
472
+ }
473
+ };
474
+ return {
475
+ start: async () => {
476
+ if (started) {
477
+ return;
478
+ }
479
+ clearHelperFailureState();
480
+ started = true;
481
+ closed = false;
482
+ const controller = new AbortController();
483
+ stopController = controller;
484
+ pollingTask = poll(controller.signal);
485
+ outboundDrainTask = pollOutboundMessages(controller.signal);
486
+ },
487
+ close: async () => {
488
+ if (!started) {
489
+ clearHelperFailureState();
490
+ return;
491
+ }
492
+ closed = true;
493
+ started = false;
494
+ const controller = stopController;
495
+ stopController = null;
496
+ controller?.abort();
497
+ const task = pollingTask;
498
+ pollingTask = null;
499
+ const drainTask = outboundDrainTask;
500
+ outboundDrainTask = null;
501
+ if (task) {
502
+ await task.catch(() => { });
503
+ }
504
+ if (drainTask) {
505
+ await drainTask.catch(() => { });
506
+ }
507
+ activeRuntimeState = null;
508
+ inFlightDrain = null;
509
+ clearHelperFailureState();
510
+ },
511
+ getDebugFailureStateForTest: () => {
512
+ return {
513
+ helperFailureState: cloneHelperFailureState(helperFailureState),
514
+ retainedFailureObjectCount: retainedHelperFailureObjects.size,
515
+ };
516
+ },
517
+ };
518
+ }
package/package.json ADDED
@@ -0,0 +1,74 @@
1
+ {
2
+ "name": "opencode-oncall",
3
+ "version": "0.1.0",
4
+ "description": "Remote on-call UX plugin for OpenCode over WeChat",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "default": "./dist/index.js"
11
+ }
12
+ },
13
+ "type": "module",
14
+ "license": "MPL-2.0",
15
+ "author": "jiwangyihao",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/jiwangyihao/opencode-oncall.git"
19
+ },
20
+ "homepage": "https://github.com/jiwangyihao/opencode-oncall#readme",
21
+ "bugs": {
22
+ "url": "https://github.com/jiwangyihao/opencode-oncall/issues"
23
+ },
24
+ "keywords": [
25
+ "opencode",
26
+ "plugin",
27
+ "oncall",
28
+ "wechat",
29
+ "remote-control"
30
+ ],
31
+ "engines": {
32
+ "node": ">=24.0.0"
33
+ },
34
+ "files": [
35
+ "dist/",
36
+ "README.md",
37
+ "LICENSE"
38
+ ],
39
+ "scripts": {
40
+ "prebuild": "node scripts/clean-dist.mjs",
41
+ "build": "tsc -p tsconfig.build.json",
42
+ "wechat:smoke:self-test": "npm run build && node --input-type=module -e \"import('./dist/wechat/compat/openclaw-smoke.js').then(async (m) => { const results = await m.runOpenClawSmoke('self-test'); console.log(JSON.stringify(results, null, 2)); })\"",
43
+ "wechat:smoke:real-account": "npm run build && node --input-type=module -e \"import('./dist/wechat/compat/openclaw-smoke.js').then(async (m) => { const dryRun = process.argv.includes('--dry-run'); const results = await m.runOpenClawSmoke('real-account', { dryRun }); console.log(JSON.stringify(results, null, 2)); })\" --",
44
+ "wechat:smoke:guided": "npm run build && node dist/wechat/compat/openclaw-guided-smoke.js",
45
+ "test:wechat-real-host-gate": "npm run build && node --test test/wechat-opencode-real-host-gate.test.js",
46
+ "test:serial:wechat-status-flow:early": "node scripts/run-status-flow-phase.mjs early",
47
+ "test:serial:wechat-ws-core": "node --test --test-concurrency=1 test/wechat-ws-protocol.test.js test/wechat-broker-state-store.test.js test/wechat-broker-ws-lifecycle.test.js",
48
+ "test:serial:wechat-broker-lifecycle": "node --test --test-concurrency=1 test/wechat-broker-lifecycle.test.js",
49
+ "test:serial:wechat-notification-flow": "node --test --test-concurrency=1 test/wechat-notification-flow.test.js test/wechat-notification-dispatcher.test.js test/wechat-notification-store.test.js",
50
+ "test:serial:wechat-openclaw-task3": "node --test --test-concurrency=1 test/wechat-openclaw-task3.test.js",
51
+ "test:serial:plugin": "node --test --test-concurrency=1 test/index-exports.test.js test/wechat-plugin-entry.test.js test/wechat-plugin-hooks-status.test.js",
52
+ "test:serial:wechat-status-flow:late": "node scripts/run-status-flow-phase.mjs late",
53
+ "test:parallel:shard": "node --test test/package-boundary.test.js test/wechat-bind-flow.test.js test/wechat-debug-bundle.test.js test/wechat-dead-letter-store.test.js test/wechat-jiti-loader.test.js test/wechat-menu-actions.test.js test/wechat-migration.test.js test/wechat-openclaw-account-helpers.test.js test/wechat-openclaw-guided-smoke.test.js test/wechat-openclaw-public-helpers.test.js test/wechat-openclaw-qr-gateway.test.js test/wechat-openclaw-smoke.test.js test/wechat-openclaw-sync-buf.test.js test/wechat-openclaw-updates-send.test.js test/wechat-operator-store.test.js test/wechat-plugin-hooks-status.test.js test/wechat-qrcode-terminal-loader.test.js test/wechat-request-store.test.js test/wechat-session-digest.test.js test/wechat-state-paths.test.js test/wechat-state-root.test.js test/wechat-surface-driver.test.js test/wechat-token-store-atomic-write.test.js test/wechat-token-store.test.js",
54
+ "test": "npm run build && npm run test:serial:wechat-status-flow:early && npm run test:serial:wechat-ws-core && npm run test:serial:wechat-broker-lifecycle && npm run test:serial:wechat-notification-flow && npm run test:serial:wechat-openclaw-task3 && npm run test:serial:plugin && npm run test:serial:wechat-status-flow:late && npm run test:parallel:shard",
55
+ "typecheck": "tsc --noEmit",
56
+ "prepublishOnly": "npm run build"
57
+ },
58
+ "dependencies": {
59
+ "@opencode-ai/plugin": "^1.2.26",
60
+ "@opencode-ai/sdk": "^1.2.26",
61
+ "@tencent-weixin/openclaw-weixin": "2.0.1",
62
+ "fflate": "^0.8.2",
63
+ "openclaw": "2026.3.22",
64
+ "xdg-basedir": "^5.1.0"
65
+ },
66
+ "devDependencies": {
67
+ "@types/node": "^24.10.1",
68
+ "typescript": "^5.0.0"
69
+ },
70
+ "directories": {
71
+ "doc": "docs",
72
+ "test": "test"
73
+ }
74
+ }