opencode-copilot-account-switcher 0.14.6 → 0.14.8

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.
@@ -1,6 +1,7 @@
1
1
  import { bindOperator, readOperatorBinding, rebindOperator, resetOperatorBinding } from "./operator-store.js";
2
2
  import { loadOpenClawWeixinPublicHelpers } from "./compat/openclaw-public-helpers.js";
3
3
  import { buildOpenClawMenuAccount } from "./openclaw-account-adapter.js";
4
+ import { normalizeAccountId } from "openclaw/plugin-sdk/account-id";
4
5
  import qrcodeTerminal from "qrcode-terminal";
5
6
  const DEFAULT_QR_WAIT_TIMEOUT_MS = 480000;
6
7
  function pickFirstNonEmptyString(...values) {
@@ -21,7 +22,7 @@ function pickQrTerminal(value) {
21
22
  return pickFirstNonEmptyString(value?.qrTerminal);
22
23
  }
23
24
  function pickQrUrl(value) {
24
- return pickFirstNonEmptyString(value?.qrDataUrl, value?.qrUrl);
25
+ return pickFirstNonEmptyString(value?.qrDataUrl);
25
26
  }
26
27
  function isTimeoutWaitResult(value) {
27
28
  return Boolean(value && typeof value === "object" && "status" in value && String(value.status) === "timeout");
@@ -33,6 +34,12 @@ async function rollbackBinding(action, previousOperatorBinding, persistOperatorR
33
34
  }
34
35
  await clearOperatorBinding().catch(() => { });
35
36
  }
37
+ function isSameOperatorBinding(left, right) {
38
+ if (!left || !right) {
39
+ return false;
40
+ }
41
+ return left.wechatAccountId === right.wechatAccountId && left.userId === right.userId && left.boundAt === right.boundAt;
42
+ }
36
43
  async function renderQrTerminalDefault(input) {
37
44
  return await new Promise((resolve) => {
38
45
  qrcodeTerminal.generate(input.value, { small: true }, (output) => {
@@ -81,49 +88,45 @@ export async function runWechatBindFlow(input) {
81
88
  if (waited && typeof waited === "object" && "connected" in waited && waited.connected === false) {
82
89
  throw new Error("qr login did not complete");
83
90
  }
84
- const accountId = pickFirstNonEmptyString(waited?.accountId, helpers.latestAccountState?.accountId, (await helpers.accountHelpers.listAccountIds()).at(-1));
85
- if (!accountId) {
91
+ const rawAccountId = pickFirstNonEmptyString(waited?.accountId);
92
+ if (!rawAccountId) {
86
93
  throw new Error("missing accountId after qr login");
87
94
  }
95
+ const accountId = normalizeAccountId(rawAccountId);
88
96
  const boundAt = now();
89
- const userIdFromWait = pickFirstNonEmptyString(waited?.userId);
90
97
  const previousOperatorBinding = input.action === "wechat-rebind" ? await loadOperatorBinding() : undefined;
98
+ const userIdFromWait = pickFirstNonEmptyString(waited?.userId);
91
99
  let menuAccount;
92
100
  let boundUserId = "";
93
101
  let shouldRollbackBinding = false;
102
+ let attemptedOperatorBinding;
94
103
  try {
95
- const menuAccountState = accountId
96
- ? {
97
- ...(helpers.latestAccountState ?? {
98
- accountId,
99
- token: "",
100
- baseUrl: "https://ilinkai.weixin.qq.com",
101
- }),
102
- accountId,
103
- ...(userIdFromWait ? { userId: userIdFromWait } : {}),
104
- boundAt,
105
- }
106
- : helpers.latestAccountState;
104
+ const menuAccountState = {
105
+ accountId,
106
+ token: "",
107
+ baseUrl: "https://ilinkai.weixin.qq.com",
108
+ };
107
109
  menuAccount = await buildOpenClawMenuAccount({
108
110
  latestAccountState: menuAccountState,
109
111
  accountHelpers: helpers.accountHelpers,
110
112
  });
111
- const userId = pickFirstNonEmptyString(userIdFromWait, menuAccount?.userId);
113
+ const userId = pickFirstNonEmptyString(menuAccount?.userId, userIdFromWait);
112
114
  if (!userId) {
113
115
  throw new Error("missing userId after qr login");
114
116
  }
115
117
  boundUserId = userId;
116
- const operatorBinding = {
118
+ attemptedOperatorBinding = {
117
119
  wechatAccountId: accountId,
118
120
  userId,
119
121
  boundAt,
120
122
  };
121
- shouldRollbackBinding = true;
122
123
  if (input.action === "wechat-rebind") {
123
- await persistOperatorRebinding(operatorBinding);
124
+ shouldRollbackBinding = true;
125
+ await persistOperatorRebinding(attemptedOperatorBinding);
124
126
  }
125
127
  else {
126
- await persistOperatorBinding(operatorBinding);
128
+ shouldRollbackBinding = true;
129
+ await persistOperatorBinding(attemptedOperatorBinding);
127
130
  }
128
131
  const settings = await input.readCommonSettings();
129
132
  const notifications = settings.wechat?.notifications ?? {
@@ -148,7 +151,15 @@ export async function runWechatBindFlow(input) {
148
151
  }
149
152
  catch (error) {
150
153
  if (shouldRollbackBinding) {
151
- await rollbackBinding(input.action, previousOperatorBinding, persistOperatorRebinding, clearOperatorBinding);
154
+ if (input.action === "wechat-bind") {
155
+ const currentOperatorBinding = await loadOperatorBinding().catch(() => undefined);
156
+ if (isSameOperatorBinding(currentOperatorBinding, attemptedOperatorBinding)) {
157
+ await rollbackBinding(input.action, previousOperatorBinding, persistOperatorRebinding, clearOperatorBinding);
158
+ }
159
+ }
160
+ else {
161
+ await rollbackBinding(input.action, previousOperatorBinding, persistOperatorRebinding, clearOperatorBinding);
162
+ }
152
163
  }
153
164
  throw error;
154
165
  }
@@ -15,33 +15,11 @@ function getAccountJiti() {
15
15
  function asObject(value) {
16
16
  return value && typeof value === "object" ? value : {};
17
17
  }
18
- function toBoolean(value) {
19
- if (typeof value === "boolean") {
20
- return value;
21
- }
22
- if (value === 1) {
23
- return true;
24
- }
25
- if (value === 0) {
26
- return false;
27
- }
28
- return undefined;
29
- }
30
- function deriveEnabled(resolved, stored) {
31
- const explicitEnabled = toBoolean(resolved.enabled) ?? toBoolean(resolved.isEnabled) ?? toBoolean(stored.enabled) ?? toBoolean(stored.isEnabled);
32
- if (explicitEnabled !== undefined) {
33
- return explicitEnabled;
34
- }
35
- if (resolved.disabled === true || stored.disabled === true) {
36
- return false;
37
- }
38
- return true;
18
+ function deriveEnabled(resolved) {
19
+ return resolved.enabled === true;
39
20
  }
40
- function isConfigured(resolved, stored) {
41
- if (typeof resolved.configured === "boolean") {
42
- return resolved.configured;
43
- }
44
- return typeof stored.token === "string" && stored.token.trim().length > 0;
21
+ function isConfigured(resolved) {
22
+ return resolved.configured === true;
45
23
  }
46
24
  function toAccountId(input) {
47
25
  return typeof input === "string" ? input : input.accountId;
@@ -52,8 +30,8 @@ export function createOpenClawAccountHelpers(input) {
52
30
  const stored = asObject(await input.loadAccount(accountId));
53
31
  return {
54
32
  accountId,
55
- enabled: deriveEnabled(resolved, stored),
56
- configured: isConfigured(resolved, stored),
33
+ enabled: deriveEnabled(resolved),
34
+ configured: isConfigured(resolved),
57
35
  name: typeof resolved.name === "string" ? resolved.name : undefined,
58
36
  userId: typeof stored.userId === "string" ? stored.userId : undefined,
59
37
  };
@@ -81,6 +59,13 @@ export async function loadOpenClawAccountHelpers(options = {}) {
81
59
  return createOpenClawAccountHelpers({
82
60
  listAccountIds: () => accountsModule.listIndexedWeixinAccountIds(),
83
61
  loadAccount: (accountId) => accountsModule.loadWeixinAccount(accountId),
84
- resolveAccount: (accountId) => ({ accountId }),
62
+ resolveAccount: async (accountId) => {
63
+ const stored = asObject(await accountsModule.loadWeixinAccount(accountId));
64
+ return {
65
+ accountId,
66
+ enabled: stored.enabled === false ? false : true,
67
+ configured: typeof stored.token === "string" && stored.token.trim().length > 0,
68
+ };
69
+ },
85
70
  });
86
71
  }
@@ -394,7 +394,7 @@ async function runQrLoginDefault(input) {
394
394
  verbose: false,
395
395
  }));
396
396
  const qrTerminal = pickFirstString(startResult, ["qrTerminal"]);
397
- const qrUrl = pickFirstString(startResult, ["qrDataUrl", "qrUrl"]);
397
+ const qrUrl = pickFirstString(startResult, ["qrDataUrl"]);
398
398
  const sessionKey = pickFirstString(startResult, ["sessionKey"]);
399
399
  const qrStartMessage = pickFirstString(startResult, ["message", "detail", "reason"]);
400
400
  if (!sessionKey) {
@@ -695,23 +695,13 @@ function normalizeQrLoginResult(result) {
695
695
  return null;
696
696
  }
697
697
  if (candidate.status === "success") {
698
- if (typeof candidate.connected === "boolean" && candidate.connected !== true) {
699
- return {
700
- status: "timeout",
701
- qrPrinted: candidate.qrPrinted === true,
702
- qrUrl: typeof candidate.qrDataUrl === "string" && candidate.qrDataUrl.trim().length > 0
703
- ? candidate.qrDataUrl
704
- : typeof candidate.qrUrl === "string"
705
- ? candidate.qrUrl
706
- : undefined,
707
- };
698
+ if (candidate.connected !== true) {
699
+ return null;
708
700
  }
709
701
  const hasPrintedQr = candidate.qrPrinted === true;
710
702
  const qrUrl = typeof candidate.qrDataUrl === "string" && candidate.qrDataUrl.trim().length > 0
711
703
  ? candidate.qrDataUrl
712
- : typeof candidate.qrUrl === "string" && candidate.qrUrl.trim().length > 0
713
- ? candidate.qrUrl
714
- : undefined;
704
+ : undefined;
715
705
  const hasQrUrl = Boolean(qrUrl);
716
706
  if (!hasPrintedQr && !hasQrUrl) {
717
707
  return null;
@@ -719,9 +709,7 @@ function normalizeQrLoginResult(result) {
719
709
  }
720
710
  const normalizedQrUrl = typeof candidate.qrDataUrl === "string" && candidate.qrDataUrl.trim().length > 0
721
711
  ? candidate.qrDataUrl
722
- : typeof candidate.qrUrl === "string"
723
- ? candidate.qrUrl
724
- : undefined;
712
+ : undefined;
725
713
  return {
726
714
  status: candidate.status,
727
715
  connected: candidate.connected === true ? true : undefined,
@@ -34,12 +34,6 @@ export async function loadOpenClawQrGateway(payloads, options = {}) {
34
34
  pluginId: resolvedPluginId,
35
35
  };
36
36
  }
37
- if (hasQrLoginMethods(payloadPlugin)) {
38
- return {
39
- gateway: createOpenClawQrGateway(payloadPlugin),
40
- pluginId: resolvedPluginId,
41
- };
42
- }
43
37
  }
44
38
  throw new Error("registerChannel did not expose weixin gateway loginWithQrStart/loginWithQrWait");
45
39
  }
@@ -11,7 +11,7 @@ export declare function createOpenClawSyncBufHelper(input: {
11
11
  export declare function loadOpenClawSyncBufHelper(options?: {
12
12
  syncBufModulePath?: string;
13
13
  }): Promise<{
14
- persistGetUpdatesBuf?: PublicWeixinPersistGetUpdatesBuf;
14
+ persistGetUpdatesBuf: PublicWeixinPersistGetUpdatesBuf;
15
15
  }>;
16
16
  export declare function loadLatestWeixinAccountState(options?: {
17
17
  stateDirModulePath?: string;
@@ -19,6 +19,9 @@ export function createOpenClawSyncBufHelper(input) {
19
19
  return {
20
20
  async persistGetUpdatesBuf({ accountId, getUpdatesBuf }) {
21
21
  const filePath = input.getSyncBufFilePath(accountId);
22
+ if (typeof filePath !== "string" || filePath.trim().length === 0) {
23
+ throw new Error("[wechat-compat] sync-buf helper returned invalid file path");
24
+ }
22
25
  input.saveGetUpdatesBuf(filePath, getUpdatesBuf);
23
26
  },
24
27
  };
@@ -28,7 +31,7 @@ export async function loadOpenClawSyncBufHelper(options = {}) {
28
31
  const syncBufModulePath = require.resolve(options.syncBufModulePath ?? OPENCLAW_SYNC_BUF_MODULE);
29
32
  const syncBufModule = getSyncBufJiti()(syncBufModulePath);
30
33
  if (typeof syncBufModule.getSyncBufFilePath !== "function" || typeof syncBufModule.saveGetUpdatesBuf !== "function") {
31
- return {};
34
+ throw new Error("[wechat-compat] sync-buf source helper unavailable");
32
35
  }
33
36
  return createOpenClawSyncBufHelper({
34
37
  getSyncBufFilePath: syncBufModule.getSyncBufFilePath,
@@ -13,20 +13,6 @@ function toBoolean(value) {
13
13
  if (typeof value === "boolean") {
14
14
  return value;
15
15
  }
16
- if (value === 1) {
17
- return true;
18
- }
19
- if (value === 0) {
20
- return false;
21
- }
22
- return undefined;
23
- }
24
- function firstNumber(...values) {
25
- for (const value of values) {
26
- if (typeof value === "number" && Number.isFinite(value)) {
27
- return value;
28
- }
29
- }
30
16
  return undefined;
31
17
  }
32
18
  async function tryDescribeAccount(accountHelpers, accountId) {
@@ -55,13 +41,17 @@ export async function buildOpenClawMenuAccount(input) {
55
41
  }
56
42
  const resolvedRaw = asObject(await input.accountHelpers.resolveAccount(accountId));
57
43
  const describedRaw = asObject(await tryDescribeAccount(input.accountHelpers, accountId));
58
- const enabled = toBoolean(resolvedRaw.enabled) ?? toBoolean(resolvedRaw.isEnabled) ?? toBoolean(describedRaw.enabled) ?? toBoolean(describedRaw.isEnabled) ?? false;
59
- const configured = toBoolean(describedRaw.configured) ?? toBoolean(describedRaw.isConfigured) ?? toBoolean(resolvedRaw.configured) ?? toBoolean(resolvedRaw.isConfigured) ?? false;
60
- const userId = firstNonEmptyString(input.latestAccountState?.userId, resolvedRaw.userId, resolvedRaw.user_id, describedRaw.userId, describedRaw.user_id);
61
- const boundAt = firstNumber(input.latestAccountState?.boundAt, input.latestAccountState?.savedAt, resolvedRaw.boundAt, resolvedRaw.savedAt, describedRaw.boundAt, describedRaw.savedAt);
44
+ const enabled = toBoolean(resolvedRaw.enabled) ?? toBoolean(describedRaw.enabled) ?? false;
45
+ const configured = toBoolean(describedRaw.configured) ?? toBoolean(resolvedRaw.configured) ?? false;
46
+ const userId = firstNonEmptyString(input.latestAccountState?.userId, resolvedRaw.userId, describedRaw.userId);
47
+ const boundAt = [
48
+ input.latestAccountState?.boundAt,
49
+ resolvedRaw.boundAt,
50
+ describedRaw.boundAt,
51
+ ].find((value) => typeof value === "number" && Number.isFinite(value));
62
52
  return {
63
53
  accountId,
64
- name: firstNonEmptyString(resolvedRaw.name, resolvedRaw.displayName, describedRaw.name, describedRaw.displayName),
54
+ name: firstNonEmptyString(resolvedRaw.name, describedRaw.name),
65
55
  enabled,
66
56
  configured,
67
57
  userId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-copilot-account-switcher",
3
- "version": "0.14.6",
3
+ "version": "0.14.8",
4
4
  "description": "GitHub Copilot account switcher plugin for OpenCode",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",