opencode-copilot-account-switcher 0.14.35 → 0.14.36

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,5 +1,5 @@
1
1
  import { bindOperator, readOperatorBinding, rebindOperator, resetOperatorBinding } from "./operator-store.js";
2
- import { loadOpenClawWeixinPublicHelpers } from "./compat/openclaw-public-helpers.js";
2
+ import { loadOpenClawWeixinBindHelpers } from "./compat/openclaw-bind-helpers.js";
3
3
  import type { CommonSettingsStore } from "../common-settings-store.js";
4
4
  type BindAction = "wechat-bind" | "wechat-rebind";
5
5
  type WechatBindFlowResult = {
@@ -12,7 +12,7 @@ type WechatBindFlowResult = {
12
12
  };
13
13
  type WechatBindFlowInput = {
14
14
  action: BindAction;
15
- loadPublicHelpers?: typeof loadOpenClawWeixinPublicHelpers;
15
+ loadPublicHelpers?: typeof loadOpenClawWeixinBindHelpers;
16
16
  bindOperator?: typeof bindOperator;
17
17
  rebindOperator?: typeof rebindOperator;
18
18
  readOperatorBinding?: typeof readOperatorBinding;
@@ -1,5 +1,5 @@
1
1
  import { bindOperator, readOperatorBinding, rebindOperator, resetOperatorBinding } from "./operator-store.js";
2
- import { loadOpenClawWeixinPublicHelpers } from "./compat/openclaw-public-helpers.js";
2
+ import { loadOpenClawWeixinBindHelpers } from "./compat/openclaw-bind-helpers.js";
3
3
  import { buildOpenClawMenuAccount } from "./openclaw-account-adapter.js";
4
4
  import { loadQrCodeTerminal } from "./compat/qrcode-terminal-loader.js";
5
5
  import { normalizeAccountId } from "openclaw/plugin-sdk/account-id";
@@ -50,7 +50,7 @@ async function renderQrTerminalDefault(input) {
50
50
  }
51
51
  export async function runWechatBindFlow(input) {
52
52
  const now = input.now ?? Date.now;
53
- const loadPublicHelpers = input.loadPublicHelpers ?? loadOpenClawWeixinPublicHelpers;
53
+ const loadPublicHelpers = input.loadPublicHelpers ?? loadOpenClawWeixinBindHelpers;
54
54
  const persistOperatorBinding = input.bindOperator ?? bindOperator;
55
55
  const persistOperatorRebinding = input.rebindOperator ?? rebindOperator;
56
56
  const loadOperatorBinding = input.readOperatorBinding ?? readOperatorBinding;
@@ -2,6 +2,7 @@ export type JitiLoader = (path: string) => unknown;
2
2
  type CreateJiti = (id: string | URL, options?: Record<string, unknown>) => JitiLoader;
3
3
  type JitiImport = (specifier: string) => Promise<unknown> | unknown;
4
4
  type JitiResolve = (specifier: string) => string;
5
+ type JitiRequire = (specifier: string) => unknown;
5
6
  type ModuleImport = (specifier: string) => Promise<unknown>;
6
7
  type JitiNamespace = {
7
8
  createJiti?: unknown;
@@ -10,9 +11,10 @@ type JitiNamespace = {
10
11
  };
11
12
  export declare function resolveCreateJiti(namespace: JitiNamespace): CreateJiti;
12
13
  export declare function resolveJitiEsmEntry(resolveImpl?: JitiResolve): string;
14
+ export declare function resolveJitiCjsEntry(resolveImpl?: JitiResolve): string;
13
15
  export declare function hasBunRuntime(bunVersion?: string | undefined): boolean;
14
16
  export declare function wrapCreateJiti(createJiti: CreateJiti): CreateJiti;
15
- export declare function loadJiti(importImpl?: JitiImport, resolveImpl?: JitiResolve): Promise<{
17
+ export declare function loadJiti(importImpl?: JitiImport, resolveImpl?: JitiResolve, requireImpl?: JitiRequire): Promise<{
16
18
  createJiti: CreateJiti;
17
19
  }>;
18
20
  export declare function loadModuleWithTsFallback(modulePath: string, options?: {
@@ -22,17 +22,37 @@ export function resolveCreateJiti(namespace) {
22
22
  isCreateJiti(namespace.default.createJiti)) {
23
23
  return namespace.default.createJiti;
24
24
  }
25
- throw new Error("[wechat-compat] createJiti export unavailable");
25
+ if (namespace.default &&
26
+ typeof namespace.default === "object" &&
27
+ isCreateJiti(namespace.default.default)) {
28
+ return namespace.default.default;
29
+ }
30
+ if (namespace.default &&
31
+ typeof namespace.default === "object" &&
32
+ isCreateJiti(namespace.default["module.exports"])) {
33
+ return namespace.default["module.exports"];
34
+ }
35
+ const topLevelKeys = namespace && typeof namespace === "object" ? Object.keys(namespace).join(",") : typeof namespace;
36
+ const defaultValue = namespace?.default;
37
+ const defaultKeys = defaultValue && typeof defaultValue === "object" ? Object.keys(defaultValue).join(",") : typeof defaultValue;
38
+ throw new Error(`[wechat-compat] createJiti export unavailable (keys=${topLevelKeys}; default=${defaultKeys})`);
26
39
  }
27
40
  export function resolveJitiEsmEntry(resolveImpl = createRequire(import.meta.url).resolve) {
28
41
  const packageJsonPath = resolveImpl("jiti/package.json");
29
- return pathToFileURL(path.join(path.dirname(packageJsonPath), "dist", "jiti.cjs")).href;
42
+ return pathToFileURL(path.join(path.dirname(packageJsonPath), "lib", "jiti.cjs")).href;
43
+ }
44
+ export function resolveJitiCjsEntry(resolveImpl = createRequire(import.meta.url).resolve) {
45
+ const packageJsonPath = resolveImpl("jiti/package.json");
46
+ return path.join(path.dirname(packageJsonPath), "lib", "jiti.cjs");
30
47
  }
31
48
  function onJitiError(error) {
32
49
  throw error;
33
50
  }
34
51
  const nativeImport = (id) => import(id);
35
52
  const DEFAULT_JITI_EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".js", ".mjs", ".cjs", ".json"];
53
+ function isTypeScriptModulePath(modulePath) {
54
+ return /\.(ts|tsx|mts|cts)$/i.test(modulePath);
55
+ }
36
56
  export function hasBunRuntime(bunVersion = process.versions?.bun) {
37
57
  return typeof bunVersion === "string" && bunVersion.length > 0;
38
58
  }
@@ -56,16 +76,36 @@ export function wrapCreateJiti(createJiti) {
56
76
  });
57
77
  };
58
78
  }
59
- export async function loadJiti(importImpl = (specifier) => import(specifier), resolveImpl = createRequire(import.meta.url).resolve) {
60
- const namespace = await Promise.resolve(importImpl(resolveJitiEsmEntry(resolveImpl)));
61
- return {
62
- createJiti: wrapCreateJiti(resolveCreateJiti(namespace)),
63
- };
79
+ export async function loadJiti(importImpl = (specifier) => import(specifier), resolveImpl = createRequire(import.meta.url).resolve, requireImpl = createRequire(import.meta.url)) {
80
+ try {
81
+ const required = requireImpl("jiti");
82
+ return {
83
+ createJiti: wrapCreateJiti(resolveCreateJiti(required)),
84
+ };
85
+ }
86
+ catch {
87
+ // Fall back to import() when require-based loading is unavailable.
88
+ }
89
+ try {
90
+ const namespace = await Promise.resolve(importImpl("jiti"));
91
+ return {
92
+ createJiti: wrapCreateJiti(resolveCreateJiti(namespace)),
93
+ };
94
+ }
95
+ catch {
96
+ const namespace = await Promise.resolve(importImpl(resolveJitiEsmEntry(resolveImpl)));
97
+ return {
98
+ createJiti: wrapCreateJiti(resolveCreateJiti(namespace)),
99
+ };
100
+ }
64
101
  }
65
102
  export async function loadModuleWithTsFallback(modulePath, options = {}) {
66
103
  const moduleUrl = pathToFileURL(modulePath).href;
67
104
  const importImpl = options.importImpl ?? nativeImport;
68
- if (hasBunRuntime(options.bunVersion)) {
105
+ // Even under Bun, TS entrypoints inside node_modules can transitively hit ESM/CJS
106
+ // interop edges (for example openclaw -> json5 default import). Jiti keeps that
107
+ // path stable for the WeChat compat loader.
108
+ if (hasBunRuntime(options.bunVersion) && !isTypeScriptModulePath(modulePath)) {
69
109
  return await importImpl(moduleUrl);
70
110
  }
71
111
  const { createJiti } = await (options.loadJitiImpl ?? loadJiti)();
@@ -0,0 +1,29 @@
1
+ type WeixinBindQrGateway = {
2
+ loginWithQrStart: (input?: unknown) => Promise<unknown>;
3
+ loginWithQrWait: (input?: unknown) => Promise<unknown>;
4
+ };
5
+ type WeixinBindAccountHelpers = {
6
+ listAccountIds: () => Promise<string[]>;
7
+ resolveAccount: (accountId: string) => Promise<{
8
+ accountId: string;
9
+ enabled: boolean;
10
+ configured: boolean;
11
+ name?: string;
12
+ userId?: string;
13
+ }>;
14
+ describeAccount: (accountIdOrInput: string | {
15
+ accountId: string;
16
+ }) => Promise<{
17
+ accountId: string;
18
+ enabled: boolean;
19
+ configured: boolean;
20
+ name?: string;
21
+ userId?: string;
22
+ }>;
23
+ };
24
+ export type OpenClawWeixinBindHelpers = {
25
+ qrGateway: WeixinBindQrGateway;
26
+ accountHelpers: WeixinBindAccountHelpers;
27
+ };
28
+ export declare function loadOpenClawWeixinBindHelpers(): Promise<OpenClawWeixinBindHelpers>;
29
+ export {};
@@ -0,0 +1,153 @@
1
+ import { randomUUID } from "node:crypto";
2
+ const DEFAULT_BASE_URL = "https://ilinkai.weixin.qq.com";
3
+ const DEFAULT_ILINK_BOT_TYPE = "3";
4
+ const ACTIVE_LOGIN_TTL_MS = 5 * 60_000;
5
+ const QR_LONG_POLL_TIMEOUT_MS = 35_000;
6
+ const activeLogins = new Map();
7
+ function asObject(value) {
8
+ return value && typeof value === "object" ? value : {};
9
+ }
10
+ function asNonEmptyString(value) {
11
+ return typeof value === "string" && value.trim().length > 0 ? value : undefined;
12
+ }
13
+ function asPositiveNumber(value) {
14
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : undefined;
15
+ }
16
+ function isLoginFresh(login) {
17
+ return Date.now() - login.startedAt < ACTIVE_LOGIN_TTL_MS;
18
+ }
19
+ function purgeExpiredLogins() {
20
+ for (const [sessionKey, login] of activeLogins) {
21
+ if (!isLoginFresh(login)) {
22
+ activeLogins.delete(sessionKey);
23
+ }
24
+ }
25
+ }
26
+ async function fetchQrCode(apiBaseUrl, botType) {
27
+ const base = apiBaseUrl.endsWith("/") ? apiBaseUrl : `${apiBaseUrl}/`;
28
+ const url = new URL(`ilink/bot/get_bot_qrcode?bot_type=${encodeURIComponent(botType)}`, base);
29
+ const response = await fetch(url.toString());
30
+ if (!response.ok) {
31
+ throw new Error(`Failed to fetch QR code: ${response.status} ${response.statusText}`);
32
+ }
33
+ return await response.json();
34
+ }
35
+ async function pollQrStatus(apiBaseUrl, qrcode) {
36
+ const base = apiBaseUrl.endsWith("/") ? apiBaseUrl : `${apiBaseUrl}/`;
37
+ const url = new URL(`ilink/bot/get_qrcode_status?qrcode=${encodeURIComponent(qrcode)}`, base);
38
+ const controller = new AbortController();
39
+ const timer = setTimeout(() => controller.abort(), QR_LONG_POLL_TIMEOUT_MS);
40
+ try {
41
+ const response = await fetch(url.toString(), {
42
+ headers: { "iLink-App-ClientVersion": "1" },
43
+ signal: controller.signal,
44
+ });
45
+ clearTimeout(timer);
46
+ const rawText = await response.text();
47
+ if (!response.ok) {
48
+ throw new Error(`Failed to poll QR status: ${response.status} ${response.statusText}`);
49
+ }
50
+ const parsed = JSON.parse(rawText);
51
+ return parsed && typeof parsed === "object" ? parsed : {};
52
+ }
53
+ catch (error) {
54
+ clearTimeout(timer);
55
+ if (error instanceof Error && error.name === "AbortError") {
56
+ return { status: "wait" };
57
+ }
58
+ throw error;
59
+ }
60
+ }
61
+ function createAccountHelpers() {
62
+ async function describeAccount(accountId) {
63
+ return {
64
+ accountId,
65
+ enabled: true,
66
+ configured: false,
67
+ };
68
+ }
69
+ return {
70
+ async listAccountIds() {
71
+ return [];
72
+ },
73
+ resolveAccount: describeAccount,
74
+ async describeAccount(accountIdOrInput) {
75
+ const accountId = typeof accountIdOrInput === "string" ? accountIdOrInput : accountIdOrInput.accountId;
76
+ return await describeAccount(accountId);
77
+ },
78
+ };
79
+ }
80
+ export async function loadOpenClawWeixinBindHelpers() {
81
+ const accountHelpers = createAccountHelpers();
82
+ return {
83
+ qrGateway: {
84
+ async loginWithQrStart(input) {
85
+ const params = asObject(input);
86
+ const sessionKey = asNonEmptyString(params.accountId) ?? randomUUID();
87
+ const force = params.force === true;
88
+ purgeExpiredLogins();
89
+ const existing = activeLogins.get(sessionKey);
90
+ if (!force && existing && isLoginFresh(existing) && existing.qrcodeUrl) {
91
+ return {
92
+ qrcodeUrl: existing.qrcodeUrl,
93
+ message: "二维码已就绪,请使用微信扫描。",
94
+ sessionKey,
95
+ };
96
+ }
97
+ const qrResponse = await fetchQrCode(DEFAULT_BASE_URL, DEFAULT_ILINK_BOT_TYPE);
98
+ activeLogins.set(sessionKey, {
99
+ sessionKey,
100
+ qrcode: qrResponse.qrcode,
101
+ qrcodeUrl: qrResponse.qrcode_img_content,
102
+ startedAt: Date.now(),
103
+ });
104
+ return {
105
+ qrDataUrl: qrResponse.qrcode_img_content,
106
+ qrcodeUrl: qrResponse.qrcode_img_content,
107
+ message: "使用微信扫描以下二维码,以完成连接。",
108
+ sessionKey,
109
+ };
110
+ },
111
+ async loginWithQrWait(input) {
112
+ const params = asObject(input);
113
+ const sessionKey = asNonEmptyString(params.sessionKey);
114
+ if (!sessionKey) {
115
+ throw new Error("missing sessionKey from qr wait");
116
+ }
117
+ let activeLogin = activeLogins.get(sessionKey);
118
+ if (!activeLogin) {
119
+ return { connected: false, message: "当前没有进行中的登录,请先发起登录。" };
120
+ }
121
+ if (!isLoginFresh(activeLogin)) {
122
+ activeLogins.delete(sessionKey);
123
+ return { connected: false, message: "二维码已过期,请重新生成。" };
124
+ }
125
+ const timeoutMs = Math.max(asPositiveNumber(params.timeoutMs) ?? 480_000, 1000);
126
+ const deadline = Date.now() + timeoutMs;
127
+ while (Date.now() < deadline) {
128
+ const statusResponse = await pollQrStatus(DEFAULT_BASE_URL, activeLogin.qrcode);
129
+ const status = asNonEmptyString(statusResponse.status);
130
+ if (status === "confirmed") {
131
+ activeLogins.delete(sessionKey);
132
+ return {
133
+ connected: true,
134
+ accountId: asNonEmptyString(statusResponse.ilink_bot_id),
135
+ baseUrl: asNonEmptyString(statusResponse.baseurl),
136
+ userId: asNonEmptyString(statusResponse.ilink_user_id),
137
+ message: "✅ 与微信连接成功!",
138
+ };
139
+ }
140
+ if (status === "expired") {
141
+ activeLogins.delete(sessionKey);
142
+ return { connected: false, message: "二维码已过期,请重新生成。" };
143
+ }
144
+ await new Promise((resolve) => setTimeout(resolve, 1000));
145
+ activeLogin = activeLogins.get(sessionKey) ?? activeLogin;
146
+ }
147
+ activeLogins.delete(sessionKey);
148
+ return { connected: false, message: "登录超时,请重试。" };
149
+ },
150
+ },
151
+ accountHelpers,
152
+ };
153
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-copilot-account-switcher",
3
- "version": "0.14.35",
3
+ "version": "0.14.36",
4
4
  "description": "GitHub Copilot account switcher plugin for OpenCode",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -49,6 +49,7 @@
49
49
  "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)); })\"",
50
50
  "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)); })\" --",
51
51
  "wechat:smoke:guided": "npm run build && node dist/wechat/compat/openclaw-guided-smoke.js",
52
+ "test:wechat-real-host-gate": "npm run build && node --test test/wechat-opencode-real-host-gate.test.js",
52
53
  "test": "npm run build && node --test",
53
54
  "typecheck": "tsc --noEmit",
54
55
  "prepublishOnly": "npm run build"