opencode-copilot-account-switcher 0.13.6 → 0.14.1

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 (58) hide show
  1. package/dist/common-settings-actions.d.ts +1 -1
  2. package/dist/common-settings-actions.js +54 -0
  3. package/dist/common-settings-store.d.ts +25 -0
  4. package/dist/common-settings-store.js +81 -0
  5. package/dist/menu-runtime.js +12 -1
  6. package/dist/plugin-hooks.d.ts +8 -0
  7. package/dist/plugin-hooks.js +96 -0
  8. package/dist/providers/codex-menu-adapter.js +14 -1
  9. package/dist/providers/copilot-menu-adapter.js +13 -0
  10. package/dist/store-paths.d.ts +1 -0
  11. package/dist/store-paths.js +3 -0
  12. package/dist/ui/menu.d.ts +73 -34
  13. package/dist/ui/menu.js +195 -0
  14. package/dist/wechat/bind-flow.d.ts +25 -0
  15. package/dist/wechat/bind-flow.js +101 -0
  16. package/dist/wechat/bridge.d.ts +69 -0
  17. package/dist/wechat/bridge.js +180 -0
  18. package/dist/wechat/broker-client.d.ts +33 -0
  19. package/dist/wechat/broker-client.js +257 -0
  20. package/dist/wechat/broker-entry.d.ts +17 -0
  21. package/dist/wechat/broker-entry.js +182 -0
  22. package/dist/wechat/broker-launcher.d.ts +27 -0
  23. package/dist/wechat/broker-launcher.js +191 -0
  24. package/dist/wechat/broker-server.d.ts +25 -0
  25. package/dist/wechat/broker-server.js +540 -0
  26. package/dist/wechat/command-parser.d.ts +7 -0
  27. package/dist/wechat/command-parser.js +16 -0
  28. package/dist/wechat/compat/openclaw-guided-smoke.d.ts +178 -0
  29. package/dist/wechat/compat/openclaw-guided-smoke.js +1133 -0
  30. package/dist/wechat/compat/openclaw-public-helpers.d.ts +111 -0
  31. package/dist/wechat/compat/openclaw-public-helpers.js +262 -0
  32. package/dist/wechat/compat/openclaw-smoke.d.ts +48 -0
  33. package/dist/wechat/compat/openclaw-smoke.js +100 -0
  34. package/dist/wechat/compat/slash-guard.d.ts +11 -0
  35. package/dist/wechat/compat/slash-guard.js +24 -0
  36. package/dist/wechat/handle.d.ts +8 -0
  37. package/dist/wechat/handle.js +46 -0
  38. package/dist/wechat/ipc-auth.d.ts +6 -0
  39. package/dist/wechat/ipc-auth.js +39 -0
  40. package/dist/wechat/openclaw-account-adapter.d.ts +30 -0
  41. package/dist/wechat/openclaw-account-adapter.js +70 -0
  42. package/dist/wechat/operator-store.d.ts +9 -0
  43. package/dist/wechat/operator-store.js +69 -0
  44. package/dist/wechat/protocol.d.ts +29 -0
  45. package/dist/wechat/protocol.js +75 -0
  46. package/dist/wechat/request-store.d.ts +41 -0
  47. package/dist/wechat/request-store.js +215 -0
  48. package/dist/wechat/session-digest.d.ts +41 -0
  49. package/dist/wechat/session-digest.js +134 -0
  50. package/dist/wechat/state-paths.d.ts +14 -0
  51. package/dist/wechat/state-paths.js +45 -0
  52. package/dist/wechat/status-format.d.ts +14 -0
  53. package/dist/wechat/status-format.js +174 -0
  54. package/dist/wechat/token-store.d.ts +18 -0
  55. package/dist/wechat/token-store.js +100 -0
  56. package/dist/wechat/wechat-status-runtime.d.ts +24 -0
  57. package/dist/wechat/wechat-status-runtime.js +238 -0
  58. package/package.json +8 -3
package/dist/ui/menu.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import { ANSI } from "./ansi.js";
2
2
  import { select } from "./select.js";
3
3
  import { confirm } from "./confirm.js";
4
+ import { readCommonSettingsStore } from "../common-settings-store.js";
5
+ import { readOperatorBinding } from "../wechat/operator-store.js";
4
6
  function defaultMenuCapabilities(provider) {
5
7
  if (provider === "codex") {
6
8
  return {
@@ -15,6 +17,7 @@ function defaultMenuCapabilities(provider) {
15
17
  experimentalSlashCommands: true,
16
18
  networkRetry: true,
17
19
  syntheticAgentInitiator: false,
20
+ wechatNotificationsMenu: true,
18
21
  };
19
22
  }
20
23
  return {
@@ -29,6 +32,7 @@ function defaultMenuCapabilities(provider) {
29
32
  experimentalSlashCommands: true,
30
33
  networkRetry: true,
31
34
  syntheticAgentInitiator: true,
35
+ wechatNotificationsMenu: true,
32
36
  };
33
37
  }
34
38
  export function getMenuCopy(language = "zh", provider = "copilot") {
@@ -67,6 +71,16 @@ export function getMenuCopy(language = "zh", provider = "copilot") {
67
71
  syntheticInitiatorOn: "Send synthetic messages as agent: On",
68
72
  syntheticInitiatorOff: "Send synthetic messages as agent: Off",
69
73
  syntheticInitiatorHint: "Changes upstream behavior; misuse may increase billing risk or trigger abuse signals",
74
+ wechatNotificationsHeading: "WeChat notifications",
75
+ wechatBind: "Bind / Rebind WeChat",
76
+ wechatNotificationsOn: "WeChat notifications: On",
77
+ wechatNotificationsOff: "WeChat notifications: Off",
78
+ wechatQuestionNotifyOn: "Question notifications: On",
79
+ wechatQuestionNotifyOff: "Question notifications: Off",
80
+ wechatPermissionNotifyOn: "Permission notifications: On",
81
+ wechatPermissionNotifyOff: "Permission notifications: Off",
82
+ wechatSessionErrorNotifyOn: "Session error notifications: On",
83
+ wechatSessionErrorNotifyOff: "Session error notifications: Off",
70
84
  accountsHeading: "Accounts",
71
85
  dangerHeading: "Danger zone",
72
86
  removeAll: "Remove all accounts",
@@ -105,6 +119,16 @@ export function getMenuCopy(language = "zh", provider = "copilot") {
105
119
  syntheticInitiatorOn: "synthetic 消息按 agent 身份发送:已开启",
106
120
  syntheticInitiatorOff: "synthetic 消息按 agent 身份发送:已关闭",
107
121
  syntheticInitiatorHint: "会改变与 upstream 的默认行为;误用可能带来异常计费或 abuse 风险",
122
+ wechatNotificationsHeading: "微信通知",
123
+ wechatBind: "绑定 / 重绑微信",
124
+ wechatNotificationsOn: "微信通知总开关:已开启",
125
+ wechatNotificationsOff: "微信通知总开关:已关闭",
126
+ wechatQuestionNotifyOn: "问题通知:已开启",
127
+ wechatQuestionNotifyOff: "问题通知:已关闭",
128
+ wechatPermissionNotifyOn: "权限通知:已开启",
129
+ wechatPermissionNotifyOff: "权限通知:已关闭",
130
+ wechatSessionErrorNotifyOn: "会话错误通知:已开启",
131
+ wechatSessionErrorNotifyOff: "会话错误通知:已关闭",
108
132
  accountsHeading: "账号",
109
133
  dangerHeading: "危险操作",
110
134
  removeAll: "删除全部账号",
@@ -144,6 +168,16 @@ export function getMenuCopy(language = "zh", provider = "copilot") {
144
168
  syntheticInitiatorOn: "Send synthetic messages as agent: On",
145
169
  syntheticInitiatorOff: "Send synthetic messages as agent: Off",
146
170
  syntheticInitiatorHint: "Changes upstream behavior; misuse may increase billing risk or trigger abuse signals",
171
+ wechatNotificationsHeading: "WeChat notifications",
172
+ wechatBind: "Bind / Rebind WeChat",
173
+ wechatNotificationsOn: "WeChat notifications: On",
174
+ wechatNotificationsOff: "WeChat notifications: Off",
175
+ wechatQuestionNotifyOn: "Question notifications: On",
176
+ wechatQuestionNotifyOff: "Question notifications: Off",
177
+ wechatPermissionNotifyOn: "Permission notifications: On",
178
+ wechatPermissionNotifyOff: "Permission notifications: Off",
179
+ wechatSessionErrorNotifyOn: "Session error notifications: On",
180
+ wechatSessionErrorNotifyOff: "Session error notifications: Off",
147
181
  accountsHeading: "Accounts",
148
182
  dangerHeading: "Danger zone",
149
183
  removeAll: "Remove all accounts",
@@ -182,6 +216,16 @@ export function getMenuCopy(language = "zh", provider = "copilot") {
182
216
  syntheticInitiatorOn: "synthetic 消息按 agent 身份发送:已开启",
183
217
  syntheticInitiatorOff: "synthetic 消息按 agent 身份发送:已关闭",
184
218
  syntheticInitiatorHint: "会改变与 upstream 的默认行为;误用可能带来异常计费或 abuse 风险",
219
+ wechatNotificationsHeading: "微信通知",
220
+ wechatBind: "绑定 / 重绑微信",
221
+ wechatNotificationsOn: "微信通知总开关:已开启",
222
+ wechatNotificationsOff: "微信通知总开关:已关闭",
223
+ wechatQuestionNotifyOn: "问题通知:已开启",
224
+ wechatQuestionNotifyOff: "问题通知:已关闭",
225
+ wechatPermissionNotifyOn: "权限通知:已开启",
226
+ wechatPermissionNotifyOff: "权限通知:已关闭",
227
+ wechatSessionErrorNotifyOn: "会话错误通知:已开启",
228
+ wechatSessionErrorNotifyOff: "会话错误通知:已关闭",
185
229
  accountsHeading: "账号",
186
230
  dangerHeading: "危险操作",
187
231
  removeAll: "删除全部账号",
@@ -228,6 +272,10 @@ export function buildMenuItems(input) {
228
272
  const quotaHint = input.lastQuotaRefresh ? `last ${formatRelativeTime(input.lastQuotaRefresh)}` : undefined;
229
273
  const loopSafetyProviderScope = input.loopSafetyProviderScope ?? "copilot-only";
230
274
  const experimentalSlashCommandsEnabled = input.experimentalSlashCommandsEnabled !== false;
275
+ const wechatNotificationsEnabled = input.wechatNotificationsEnabled !== false;
276
+ const wechatQuestionNotifyEnabled = input.wechatQuestionNotifyEnabled !== false;
277
+ const wechatPermissionNotifyEnabled = input.wechatPermissionNotifyEnabled !== false;
278
+ const wechatSessionErrorNotifyEnabled = input.wechatSessionErrorNotifyEnabled !== false;
231
279
  const providerActions = [
232
280
  { label: copy.actionsHeading, value: { type: "cancel" }, kind: "heading" },
233
281
  { label: copy.switchLanguageLabel, value: { type: "toggle-language" }, color: "cyan" },
@@ -291,6 +339,12 @@ export function buildMenuItems(input) {
291
339
  hint: copy.retryHint,
292
340
  disabled: !capabilities.networkRetry,
293
341
  },
342
+ {
343
+ label: copy.wechatNotificationsHeading,
344
+ value: { type: "wechat-menu" },
345
+ color: "cyan",
346
+ disabled: !capabilities.wechatNotificationsMenu,
347
+ },
294
348
  ];
295
349
  const providerSettings = [
296
350
  { label: copy.providerSettingsHeading, value: { type: "cancel" }, kind: "heading" },
@@ -346,9 +400,114 @@ export function buildMenuItems(input) {
346
400
  { label: copy.removeAll, value: { type: "remove-all" }, color: "red" },
347
401
  ];
348
402
  }
403
+ function buildWechatSubmenuItems(copy, input) {
404
+ const backLabel = input.language === "en" ? "Back" : "返回上级";
405
+ const effectiveBinding = input.wechatPrimaryBinding
406
+ ? {
407
+ accountId: input.wechatPrimaryBinding.accountId,
408
+ userId: input.wechatPrimaryBinding.userId,
409
+ name: input.wechatPrimaryBinding.name,
410
+ enabled: input.wechatPrimaryBinding.enabled,
411
+ configured: input.wechatPrimaryBinding.configured,
412
+ boundAt: input.wechatPrimaryBinding.boundAt,
413
+ }
414
+ : input.wechatOperatorBinding
415
+ ? {
416
+ accountId: input.wechatOperatorBinding.wechatAccountId,
417
+ userId: input.wechatOperatorBinding.userId,
418
+ boundAt: input.wechatOperatorBinding.boundAt,
419
+ }
420
+ : undefined;
421
+ const bindActionType = effectiveBinding ? "wechat-rebind" : "wechat-bind";
422
+ const bindingRows = [];
423
+ if (effectiveBinding) {
424
+ const boundAtText = effectiveBinding.boundAt
425
+ ? new Date(effectiveBinding.boundAt).toLocaleString()
426
+ : "unknown";
427
+ bindingRows.push({ label: input.language === "en" ? "Current binding" : "当前绑定账号", value: { type: "cancel" }, kind: "heading" }, {
428
+ label: `accountId: ${effectiveBinding.accountId}`,
429
+ value: { type: "cancel" },
430
+ disabled: true,
431
+ }, ...(effectiveBinding.name
432
+ ? [{ label: `name: ${effectiveBinding.name}`, value: { type: "cancel" }, disabled: true }]
433
+ : []), ...(effectiveBinding.userId
434
+ ? [{ label: `userId: ${effectiveBinding.userId}`, value: { type: "cancel" }, disabled: true }]
435
+ : []), {
436
+ label: `enabled: ${effectiveBinding.enabled === true ? "true" : "false"}`,
437
+ value: { type: "cancel" },
438
+ disabled: true,
439
+ }, {
440
+ label: `configured: ${effectiveBinding.configured === true ? "true" : "false"}`,
441
+ value: { type: "cancel" },
442
+ disabled: true,
443
+ }, {
444
+ label: `boundAt: ${boundAtText}`,
445
+ value: { type: "cancel" },
446
+ disabled: true,
447
+ }, { label: "", value: { type: "cancel" }, separator: true });
448
+ }
449
+ return [
450
+ { label: backLabel, value: { type: "cancel" } },
451
+ { label: "", value: { type: "cancel" }, separator: true },
452
+ ...bindingRows,
453
+ { label: copy.wechatNotificationsHeading, value: { type: "cancel" }, kind: "heading" },
454
+ {
455
+ label: copy.wechatBind,
456
+ value: { type: bindActionType },
457
+ color: "cyan",
458
+ disabled: !input.capabilities.wechatNotificationsMenu,
459
+ },
460
+ {
461
+ label: input.wechatNotificationsEnabled ? copy.wechatNotificationsOn : copy.wechatNotificationsOff,
462
+ value: { type: "toggle-wechat-notifications" },
463
+ color: "cyan",
464
+ disabled: !input.capabilities.wechatNotificationsMenu,
465
+ },
466
+ {
467
+ label: input.wechatQuestionNotifyEnabled ? copy.wechatQuestionNotifyOn : copy.wechatQuestionNotifyOff,
468
+ value: { type: "toggle-wechat-question-notify" },
469
+ color: "cyan",
470
+ disabled: !input.capabilities.wechatNotificationsMenu,
471
+ },
472
+ {
473
+ label: input.wechatPermissionNotifyEnabled ? copy.wechatPermissionNotifyOn : copy.wechatPermissionNotifyOff,
474
+ value: { type: "toggle-wechat-permission-notify" },
475
+ color: "cyan",
476
+ disabled: !input.capabilities.wechatNotificationsMenu,
477
+ },
478
+ {
479
+ label: input.wechatSessionErrorNotifyEnabled ? copy.wechatSessionErrorNotifyOn : copy.wechatSessionErrorNotifyOff,
480
+ value: { type: "toggle-wechat-session-error-notify" },
481
+ color: "cyan",
482
+ disabled: !input.capabilities.wechatNotificationsMenu,
483
+ },
484
+ ];
485
+ }
349
486
  export async function showMenu(accounts, input = {}) {
350
487
  return showMenuWithDeps(accounts, input);
351
488
  }
489
+ function pickPrimaryBindingFromSettings(settings) {
490
+ const primary = settings?.wechat?.primaryBinding;
491
+ if (!primary?.accountId)
492
+ return undefined;
493
+ return {
494
+ accountId: primary.accountId,
495
+ userId: primary.userId,
496
+ name: primary.name,
497
+ enabled: primary.enabled,
498
+ configured: primary.configured,
499
+ boundAt: primary.boundAt,
500
+ };
501
+ }
502
+ function pickOperatorBinding(binding) {
503
+ if (!binding)
504
+ return undefined;
505
+ return {
506
+ wechatAccountId: binding.wechatAccountId,
507
+ userId: binding.userId,
508
+ boundAt: binding.boundAt,
509
+ };
510
+ }
352
511
  export async function showMenuWithDeps(accounts, input = {}, deps = {}) {
353
512
  const selectMenu = deps.select ?? select;
354
513
  const confirmAction = deps.confirm ?? confirm;
@@ -367,6 +526,10 @@ export async function showMenuWithDeps(accounts, input = {}, deps = {}) {
367
526
  loopSafetyEnabled: input.loopSafetyEnabled === true,
368
527
  loopSafetyProviderScope: input.loopSafetyProviderScope,
369
528
  networkRetryEnabled: input.networkRetryEnabled === true,
529
+ wechatNotificationsEnabled: input.wechatNotificationsEnabled,
530
+ wechatQuestionNotifyEnabled: input.wechatQuestionNotifyEnabled,
531
+ wechatPermissionNotifyEnabled: input.wechatPermissionNotifyEnabled,
532
+ wechatSessionErrorNotifyEnabled: input.wechatSessionErrorNotifyEnabled,
370
533
  syntheticAgentInitiatorEnabled: input.syntheticAgentInitiatorEnabled === true,
371
534
  experimentalSlashCommandsEnabled: input.experimentalSlashCommandsEnabled,
372
535
  capabilities: input.capabilities,
@@ -379,6 +542,38 @@ export async function showMenuWithDeps(accounts, input = {}, deps = {}) {
379
542
  });
380
543
  if (!result)
381
544
  return { type: "cancel" };
545
+ if (result.type === "wechat-menu") {
546
+ const [commonSettings, operatorBinding] = await Promise.all([
547
+ input.wechatPrimaryBinding
548
+ ? Promise.resolve(undefined)
549
+ : (deps.readCommonSettings ?? readCommonSettingsStore)().catch(() => undefined),
550
+ input.wechatOperatorBinding
551
+ ? Promise.resolve(undefined)
552
+ : (deps.readOperatorBinding ?? readOperatorBinding)().catch(() => undefined),
553
+ ]);
554
+ const wechatItems = buildWechatSubmenuItems(copy, {
555
+ wechatNotificationsEnabled: input.wechatNotificationsEnabled !== false,
556
+ wechatQuestionNotifyEnabled: input.wechatQuestionNotifyEnabled !== false,
557
+ wechatPermissionNotifyEnabled: input.wechatPermissionNotifyEnabled !== false,
558
+ wechatSessionErrorNotifyEnabled: input.wechatSessionErrorNotifyEnabled !== false,
559
+ wechatPrimaryBinding: input.wechatPrimaryBinding ?? pickPrimaryBindingFromSettings(commonSettings),
560
+ wechatOperatorBinding: input.wechatOperatorBinding ?? pickOperatorBinding(operatorBinding),
561
+ capabilities: {
562
+ ...defaultMenuCapabilities(provider),
563
+ ...input.capabilities,
564
+ },
565
+ language: currentLanguage,
566
+ });
567
+ const wechatResult = await selectMenu(wechatItems, {
568
+ message: copy.wechatNotificationsHeading,
569
+ subtitle: copy.menuSubtitle,
570
+ clearScreen: true,
571
+ });
572
+ if (!wechatResult || wechatResult.type === "cancel") {
573
+ continue;
574
+ }
575
+ return wechatResult;
576
+ }
382
577
  if (result.type === "toggle-language") {
383
578
  currentLanguage = currentLanguage === "zh" ? "en" : "zh";
384
579
  continue;
@@ -0,0 +1,25 @@
1
+ import { bindOperator, readOperatorBinding, rebindOperator, resetOperatorBinding } from "./operator-store.js";
2
+ import { loadOpenClawWeixinPublicHelpers } from "./compat/openclaw-public-helpers.js";
3
+ import type { CommonSettingsStore } from "../common-settings-store.js";
4
+ type BindAction = "wechat-bind" | "wechat-rebind";
5
+ type WechatBindFlowResult = {
6
+ accountId: string;
7
+ userId: string;
8
+ name?: string;
9
+ enabled?: boolean;
10
+ configured?: boolean;
11
+ boundAt: number;
12
+ };
13
+ type WechatBindFlowInput = {
14
+ action: BindAction;
15
+ loadPublicHelpers?: typeof loadOpenClawWeixinPublicHelpers;
16
+ bindOperator?: typeof bindOperator;
17
+ rebindOperator?: typeof rebindOperator;
18
+ readOperatorBinding?: typeof readOperatorBinding;
19
+ resetOperatorBinding?: typeof resetOperatorBinding;
20
+ readCommonSettings: () => Promise<CommonSettingsStore>;
21
+ writeCommonSettings: (settings: CommonSettingsStore) => Promise<void>;
22
+ now?: () => number;
23
+ };
24
+ export declare function runWechatBindFlow(input: WechatBindFlowInput): Promise<WechatBindFlowResult>;
25
+ export {};
@@ -0,0 +1,101 @@
1
+ import { bindOperator, readOperatorBinding, rebindOperator, resetOperatorBinding } from "./operator-store.js";
2
+ import { loadOpenClawWeixinPublicHelpers } from "./compat/openclaw-public-helpers.js";
3
+ import { buildOpenClawMenuAccount } from "./openclaw-account-adapter.js";
4
+ function pickFirstNonEmptyString(...values) {
5
+ for (const value of values) {
6
+ if (typeof value === "string" && value.trim().length > 0) {
7
+ return value;
8
+ }
9
+ }
10
+ return undefined;
11
+ }
12
+ function toErrorMessage(error) {
13
+ if (error instanceof Error && error.message.trim().length > 0) {
14
+ return error.message;
15
+ }
16
+ return String(error);
17
+ }
18
+ export async function runWechatBindFlow(input) {
19
+ const now = input.now ?? Date.now;
20
+ const loadPublicHelpers = input.loadPublicHelpers ?? loadOpenClawWeixinPublicHelpers;
21
+ const persistOperatorBinding = input.bindOperator ?? bindOperator;
22
+ const persistOperatorRebinding = input.rebindOperator ?? rebindOperator;
23
+ const loadOperatorBinding = input.readOperatorBinding ?? readOperatorBinding;
24
+ const clearOperatorBinding = input.resetOperatorBinding ?? resetOperatorBinding;
25
+ try {
26
+ const helpers = await loadPublicHelpers();
27
+ const started = await Promise.resolve(helpers.qrGateway.loginWithQrStart({ source: "menu", action: input.action }));
28
+ const sessionKey = pickFirstNonEmptyString(started?.sessionKey, started?.key);
29
+ const waited = await Promise.resolve(helpers.qrGateway.loginWithQrWait({ timeoutMs: 120000, sessionKey }));
30
+ const accountId = pickFirstNonEmptyString(helpers.latestAccountState?.accountId, waited?.accountId, (await helpers.accountHelpers.listAccountIds()).at(-1));
31
+ const userId = pickFirstNonEmptyString(waited?.userId, waited?.openid, waited?.uid);
32
+ if (!accountId) {
33
+ throw new Error("missing accountId after qr login");
34
+ }
35
+ if (!userId) {
36
+ throw new Error("missing userId after qr login");
37
+ }
38
+ const boundAt = now();
39
+ const operatorBinding = {
40
+ wechatAccountId: accountId,
41
+ userId,
42
+ boundAt,
43
+ };
44
+ const previousOperatorBinding = input.action === "wechat-rebind" ? await loadOperatorBinding() : undefined;
45
+ if (input.action === "wechat-rebind") {
46
+ await persistOperatorRebinding(operatorBinding);
47
+ }
48
+ else {
49
+ await persistOperatorBinding(operatorBinding);
50
+ }
51
+ const menuAccount = await buildOpenClawMenuAccount({
52
+ latestAccountState: helpers.latestAccountState,
53
+ accountHelpers: helpers.accountHelpers,
54
+ });
55
+ const settings = await input.readCommonSettings();
56
+ const notifications = settings.wechat?.notifications ?? {
57
+ enabled: true,
58
+ question: true,
59
+ permission: true,
60
+ sessionError: true,
61
+ };
62
+ settings.wechat = {
63
+ ...settings.wechat,
64
+ notifications,
65
+ primaryBinding: {
66
+ accountId,
67
+ userId,
68
+ name: menuAccount?.name,
69
+ enabled: menuAccount?.enabled,
70
+ configured: menuAccount?.configured,
71
+ boundAt,
72
+ },
73
+ };
74
+ try {
75
+ await input.writeCommonSettings(settings);
76
+ }
77
+ catch (error) {
78
+ if (input.action === "wechat-rebind" && previousOperatorBinding) {
79
+ await persistOperatorRebinding(previousOperatorBinding).catch(() => { });
80
+ }
81
+ else {
82
+ await clearOperatorBinding().catch(() => { });
83
+ }
84
+ throw error;
85
+ }
86
+ return {
87
+ accountId,
88
+ userId,
89
+ name: menuAccount?.name,
90
+ enabled: menuAccount?.enabled,
91
+ configured: menuAccount?.configured,
92
+ boundAt,
93
+ };
94
+ }
95
+ catch (error) {
96
+ if (input.action === "wechat-rebind") {
97
+ throw new Error(`wechat rebind failed: ${toErrorMessage(error)}`);
98
+ }
99
+ throw new Error(`wechat bind failed: ${toErrorMessage(error)}`);
100
+ }
101
+ }
@@ -0,0 +1,69 @@
1
+ import type { Message, Part, PermissionRequest, QuestionRequest, Session, SessionStatus, Todo } from "@opencode-ai/sdk/v2";
2
+ import { connect } from "./broker-client.js";
3
+ import { connectOrSpawnBroker } from "./broker-launcher.js";
4
+ import { type SessionDigest } from "./session-digest.js";
5
+ type SessionMessages = Array<{
6
+ info: Message;
7
+ parts: Part[];
8
+ }>;
9
+ type SessionLite = Pick<Session, "id" | "title" | "directory" | "time">;
10
+ type WechatBridgeClient = {
11
+ session: {
12
+ list: () => Promise<SessionLite[]>;
13
+ status: () => Promise<Record<string, SessionStatus | undefined>>;
14
+ todo: (sessionID: string) => Promise<Todo[]>;
15
+ messages: (sessionID: string) => Promise<SessionMessages>;
16
+ };
17
+ question: {
18
+ list: () => Promise<QuestionRequest[]>;
19
+ };
20
+ permission: {
21
+ list: () => Promise<PermissionRequest[]>;
22
+ };
23
+ };
24
+ export type InstanceUnavailableKind = "sessionStatus" | "questionList" | "permissionList";
25
+ export type WechatInstanceStatusSnapshot = {
26
+ instanceID: string;
27
+ instanceName: string;
28
+ pid: number;
29
+ projectName?: string;
30
+ directory: string;
31
+ collectedAt: number;
32
+ sessions: SessionDigest[];
33
+ unavailable?: InstanceUnavailableKind[];
34
+ };
35
+ export type WechatBridgeInput = {
36
+ instanceID: string;
37
+ instanceName: string;
38
+ pid: number;
39
+ projectName?: string;
40
+ directory: string;
41
+ client: WechatBridgeClient;
42
+ liveReadTimeoutMs?: number;
43
+ };
44
+ export type WechatBridge = {
45
+ collectStatusSnapshot: () => Promise<WechatInstanceStatusSnapshot>;
46
+ };
47
+ export type WechatBridgeLifecycleInput = {
48
+ client: WechatBridgeClient;
49
+ project?: {
50
+ id?: string;
51
+ name?: string;
52
+ };
53
+ directory?: string;
54
+ serverUrl?: URL;
55
+ statusCollectionEnabled?: boolean;
56
+ heartbeatIntervalMs?: number;
57
+ };
58
+ export type WechatBridgeLifecycle = {
59
+ close: () => Promise<void>;
60
+ };
61
+ type WechatBridgeLifecycleDeps = {
62
+ connectOrSpawnBrokerImpl?: typeof connectOrSpawnBroker;
63
+ connectImpl?: typeof connect;
64
+ setIntervalImpl?: typeof setInterval;
65
+ clearIntervalImpl?: typeof clearInterval;
66
+ };
67
+ export declare function createWechatBridge(input: WechatBridgeInput): WechatBridge;
68
+ export declare function createWechatBridgeLifecycle(input: WechatBridgeLifecycleInput, deps?: WechatBridgeLifecycleDeps): Promise<WechatBridgeLifecycle>;
69
+ export {};
@@ -0,0 +1,180 @@
1
+ import { connect } from "./broker-client.js";
2
+ import { connectOrSpawnBroker } from "./broker-launcher.js";
3
+ import { buildSessionDigest, groupPermissionsBySession, groupQuestionsBySession, pickRecentSessions, } from "./session-digest.js";
4
+ const DEFAULT_LIVE_READ_TIMEOUT_MS = 2_000;
5
+ const DEFAULT_HEARTBEAT_INTERVAL_MS = 10_000;
6
+ function toSafeInstanceID(input) {
7
+ const normalized = input.replace(/[^a-zA-Z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
8
+ if (normalized.length === 0) {
9
+ return `wechat-${process.pid}`;
10
+ }
11
+ return normalized.slice(0, 64);
12
+ }
13
+ function toProjectName(project) {
14
+ if (typeof project?.name === "string" && project.name.trim().length > 0) {
15
+ return project.name.trim();
16
+ }
17
+ if (typeof project?.id === "string" && project.id.trim().length > 0) {
18
+ return project.id.trim();
19
+ }
20
+ return undefined;
21
+ }
22
+ function toDirectory(inputDirectory) {
23
+ if (typeof inputDirectory === "string" && inputDirectory.trim().length > 0) {
24
+ return inputDirectory;
25
+ }
26
+ return process.cwd();
27
+ }
28
+ function toInstanceName(projectName, directory) {
29
+ if (projectName) {
30
+ return projectName;
31
+ }
32
+ const parts = directory.split(/[\\/]+/).filter((part) => part.length > 0);
33
+ return parts.at(-1) ?? `wechat-${process.pid}`;
34
+ }
35
+ function toInstanceID(projectName, directory) {
36
+ const seed = projectName ?? directory;
37
+ return toSafeInstanceID(seed);
38
+ }
39
+ function withTimeout(task, timeoutMs, name) {
40
+ return new Promise((resolve, reject) => {
41
+ let settled = false;
42
+ const timer = setTimeout(() => {
43
+ if (settled) {
44
+ return;
45
+ }
46
+ settled = true;
47
+ reject(new Error(`${name} timed out in ${timeoutMs}ms`));
48
+ }, timeoutMs);
49
+ void Promise.resolve()
50
+ .then(task)
51
+ .then((value) => {
52
+ if (settled) {
53
+ return;
54
+ }
55
+ settled = true;
56
+ clearTimeout(timer);
57
+ resolve(value);
58
+ })
59
+ .catch((error) => {
60
+ if (settled) {
61
+ return;
62
+ }
63
+ settled = true;
64
+ clearTimeout(timer);
65
+ reject(error);
66
+ });
67
+ });
68
+ }
69
+ export function createWechatBridge(input) {
70
+ const collectStatusSnapshot = async () => {
71
+ const liveReadTimeoutMs = typeof input.liveReadTimeoutMs === "number" && Number.isFinite(input.liveReadTimeoutMs)
72
+ ? Math.max(1, Math.floor(input.liveReadTimeoutMs))
73
+ : DEFAULT_LIVE_READ_TIMEOUT_MS;
74
+ const unavailable = new Set();
75
+ const [sessionListResult, statusResult, questionResult, permissionResult] = await Promise.allSettled([
76
+ withTimeout(() => input.client.session.list(), liveReadTimeoutMs, "session.list"),
77
+ withTimeout(() => input.client.session.status(), liveReadTimeoutMs, "session.status"),
78
+ withTimeout(() => input.client.question.list(), liveReadTimeoutMs, "question.list"),
79
+ withTimeout(() => input.client.permission.list(), liveReadTimeoutMs, "permission.list"),
80
+ ]);
81
+ const sessions = sessionListResult.status === "fulfilled" ? sessionListResult.value : [];
82
+ const recentSessions = pickRecentSessions(sessions, 3);
83
+ if (sessionListResult.status === "rejected") {
84
+ unavailable.add("sessionStatus");
85
+ }
86
+ const statusBySession = statusResult.status === "fulfilled"
87
+ ? statusResult.value
88
+ : (unavailable.add("sessionStatus"), {});
89
+ const questionsBySession = questionResult.status === "fulfilled"
90
+ ? groupQuestionsBySession(questionResult.value)
91
+ : (unavailable.add("questionList"), groupQuestionsBySession([]));
92
+ const permissionsBySession = permissionResult.status === "fulfilled"
93
+ ? groupPermissionsBySession(permissionResult.value)
94
+ : (unavailable.add("permissionList"), groupPermissionsBySession([]));
95
+ const sessionDigests = await Promise.all(recentSessions.map(async (session) => {
96
+ const [todoResult, messagesResult] = await Promise.allSettled([
97
+ withTimeout(() => input.client.session.todo(session.id), liveReadTimeoutMs, `session.todo:${session.id}`),
98
+ withTimeout(() => input.client.session.messages(session.id), liveReadTimeoutMs, `session.messages:${session.id}`),
99
+ ]);
100
+ const sessionUnavailable = [];
101
+ const todos = todoResult.status === "fulfilled" ? todoResult.value : (sessionUnavailable.push("todo"), []);
102
+ const messages = messagesResult.status === "fulfilled"
103
+ ? messagesResult.value
104
+ : (sessionUnavailable.push("messages"), []);
105
+ return buildSessionDigest({
106
+ session,
107
+ statusBySession,
108
+ questionsBySession,
109
+ permissionsBySession,
110
+ todos,
111
+ messages,
112
+ unavailable: sessionUnavailable,
113
+ });
114
+ }));
115
+ return {
116
+ instanceID: input.instanceID,
117
+ instanceName: input.instanceName,
118
+ pid: input.pid,
119
+ projectName: input.projectName,
120
+ directory: input.directory,
121
+ collectedAt: Date.now(),
122
+ sessions: sessionDigests,
123
+ unavailable: unavailable.size > 0 ? [...unavailable] : undefined,
124
+ };
125
+ };
126
+ return {
127
+ collectStatusSnapshot,
128
+ };
129
+ }
130
+ export async function createWechatBridgeLifecycle(input, deps = {}) {
131
+ if (input.statusCollectionEnabled !== true) {
132
+ return {
133
+ close: async () => { },
134
+ };
135
+ }
136
+ const connectOrSpawnBrokerImpl = deps.connectOrSpawnBrokerImpl ?? connectOrSpawnBroker;
137
+ const connectImpl = deps.connectImpl ?? connect;
138
+ const setIntervalImpl = deps.setIntervalImpl ?? setInterval;
139
+ const clearIntervalImpl = deps.clearIntervalImpl ?? clearInterval;
140
+ const directory = toDirectory(input.directory);
141
+ const projectName = toProjectName(input.project);
142
+ const instanceID = toInstanceID(projectName, directory);
143
+ const bridge = createWechatBridge({
144
+ instanceID,
145
+ instanceName: toInstanceName(projectName, directory),
146
+ pid: process.pid,
147
+ projectName,
148
+ directory,
149
+ client: input.client,
150
+ });
151
+ const broker = await connectOrSpawnBrokerImpl();
152
+ const brokerClient = await connectImpl(broker.endpoint, { bridge });
153
+ try {
154
+ await brokerClient.registerInstance({
155
+ instanceID,
156
+ pid: process.pid,
157
+ });
158
+ }
159
+ catch (error) {
160
+ await brokerClient.close().catch(() => { });
161
+ throw error;
162
+ }
163
+ const heartbeatIntervalMs = typeof input.heartbeatIntervalMs === "number" && Number.isFinite(input.heartbeatIntervalMs)
164
+ ? Math.max(1_000, Math.floor(input.heartbeatIntervalMs))
165
+ : DEFAULT_HEARTBEAT_INTERVAL_MS;
166
+ const timer = setIntervalImpl(() => {
167
+ void brokerClient.heartbeat().catch(() => { });
168
+ }, heartbeatIntervalMs);
169
+ let closed = false;
170
+ return {
171
+ close: async () => {
172
+ if (closed) {
173
+ return;
174
+ }
175
+ closed = true;
176
+ clearIntervalImpl(timer);
177
+ await brokerClient.close().catch(() => { });
178
+ },
179
+ };
180
+ }