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,18 @@
1
+ export type WechatSlashCommand = {
2
+ type: "status";
3
+ } | {
4
+ type: "todo";
5
+ } | {
6
+ type: "reply";
7
+ handle: string;
8
+ text: string;
9
+ } | {
10
+ type: "recover";
11
+ handle?: string;
12
+ } | {
13
+ type: "allow";
14
+ handle: string;
15
+ reply: "once" | "always" | "reject";
16
+ message?: string;
17
+ };
18
+ export declare function parseWechatSlashCommand(input: string): WechatSlashCommand | null;
@@ -0,0 +1,58 @@
1
+ export function parseWechatSlashCommand(input) {
2
+ if (typeof input !== "string") {
3
+ return null;
4
+ }
5
+ const normalized = input.trim();
6
+ if (normalized === "/status") {
7
+ return { type: "status" };
8
+ }
9
+ if (normalized === "/todo") {
10
+ return { type: "todo" };
11
+ }
12
+ const parts = normalized.split(/\s+/);
13
+ const command = parts[0];
14
+ if (command === "/reply") {
15
+ if (parts.length < 3) {
16
+ return null;
17
+ }
18
+ const handle = parts[1];
19
+ const textParts = parts.slice(2);
20
+ const text = textParts.join(" ").trim();
21
+ if (!handle || !text) {
22
+ return null;
23
+ }
24
+ return { type: "reply", handle, text };
25
+ }
26
+ if (command === "/recover") {
27
+ if (parts.length === 1) {
28
+ return { type: "recover" };
29
+ }
30
+ if (parts.length !== 2) {
31
+ return null;
32
+ }
33
+ const handle = parts[1];
34
+ if (!handle) {
35
+ return null;
36
+ }
37
+ return { type: "recover", handle };
38
+ }
39
+ if (command === "/allow") {
40
+ if (parts.length < 3) {
41
+ return null;
42
+ }
43
+ const handle = parts[1];
44
+ const rawReply = parts[2];
45
+ const messageParts = parts.slice(3);
46
+ if (!handle || !rawReply) {
47
+ return null;
48
+ }
49
+ if (rawReply !== "once" && rawReply !== "always" && rawReply !== "reject") {
50
+ return null;
51
+ }
52
+ const message = messageParts.join(" ").trim();
53
+ return message.length > 0
54
+ ? { type: "allow", handle, reply: rawReply, message }
55
+ : { type: "allow", handle, reply: rawReply };
56
+ }
57
+ return null;
58
+ }
@@ -0,0 +1,27 @@
1
+ export type JitiLoader = (path: string) => unknown;
2
+ type CreateJiti = (id: string | URL, options?: Record<string, unknown>) => JitiLoader;
3
+ type JitiImport = (specifier: string) => Promise<unknown> | unknown;
4
+ type JitiResolve = (specifier: string) => string;
5
+ type JitiRequire = (specifier: string) => unknown;
6
+ type ModuleImport = (specifier: string) => Promise<unknown>;
7
+ type JitiNamespace = {
8
+ createJiti?: unknown;
9
+ default?: unknown;
10
+ "module.exports"?: unknown;
11
+ };
12
+ export declare function resolveCreateJiti(namespace: JitiNamespace): CreateJiti;
13
+ export declare function resolveJitiEsmEntry(resolveImpl?: JitiResolve): string;
14
+ export declare function resolveJitiCjsEntry(resolveImpl?: JitiResolve): string;
15
+ export declare function hasBunRuntime(bunVersion?: string | undefined): boolean;
16
+ export declare function wrapCreateJiti(createJiti: CreateJiti): CreateJiti;
17
+ export declare function loadJiti(importImpl?: JitiImport, resolveImpl?: JitiResolve, requireImpl?: JitiRequire): Promise<{
18
+ createJiti: CreateJiti;
19
+ }>;
20
+ export declare function loadModuleWithTsFallback(modulePath: string, options?: {
21
+ bunVersion?: string | undefined;
22
+ importImpl?: ModuleImport;
23
+ loadJitiImpl?: typeof loadJiti;
24
+ parentURL?: string | URL;
25
+ jitiOptions?: Record<string, unknown>;
26
+ }): Promise<unknown>;
27
+ export {};
@@ -0,0 +1,118 @@
1
+ import { createRequire } from "node:module";
2
+ import path from "node:path";
3
+ import { pathToFileURL } from "node:url";
4
+ function isCreateJiti(value) {
5
+ return typeof value === "function";
6
+ }
7
+ export function resolveCreateJiti(namespace) {
8
+ if (isCreateJiti(namespace)) {
9
+ return namespace;
10
+ }
11
+ if (isCreateJiti(namespace.createJiti)) {
12
+ return namespace.createJiti;
13
+ }
14
+ if (isCreateJiti(namespace.default)) {
15
+ return namespace.default;
16
+ }
17
+ if (isCreateJiti(namespace["module.exports"])) {
18
+ return namespace["module.exports"];
19
+ }
20
+ if (namespace.default &&
21
+ typeof namespace.default === "object" &&
22
+ isCreateJiti(namespace.default.createJiti)) {
23
+ return namespace.default.createJiti;
24
+ }
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})`);
39
+ }
40
+ export function resolveJitiEsmEntry(resolveImpl = createRequire(import.meta.url).resolve) {
41
+ const packageJsonPath = resolveImpl("jiti/package.json");
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");
47
+ }
48
+ function onJitiError(error) {
49
+ throw error;
50
+ }
51
+ const nativeImport = (id) => import(id);
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
+ }
56
+ export function hasBunRuntime(bunVersion = process.versions?.bun) {
57
+ return typeof bunVersion === "string" && bunVersion.length > 0;
58
+ }
59
+ export function wrapCreateJiti(createJiti) {
60
+ const requireFromJiti = createRequire(resolveJitiEsmEntry());
61
+ let transformImpl;
62
+ const lazyTransform = (...args) => {
63
+ if (!transformImpl) {
64
+ transformImpl = requireFromJiti("../dist/babel.cjs");
65
+ }
66
+ return transformImpl(...args);
67
+ };
68
+ return (id, options = {}) => {
69
+ const nextOptions = typeof options.transform === "function"
70
+ ? options
71
+ : { ...options, transform: lazyTransform };
72
+ return createJiti(id, nextOptions, {
73
+ onError: onJitiError,
74
+ nativeImport,
75
+ createRequire,
76
+ });
77
+ };
78
+ }
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
+ }
101
+ }
102
+ export async function loadModuleWithTsFallback(modulePath, options = {}) {
103
+ const moduleUrl = pathToFileURL(modulePath).href;
104
+ const importImpl = options.importImpl ?? nativeImport;
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)) {
109
+ return await importImpl(moduleUrl);
110
+ }
111
+ const { createJiti } = await (options.loadJitiImpl ?? loadJiti)();
112
+ const loader = createJiti(options.parentURL ?? import.meta.url, {
113
+ interopDefault: true,
114
+ extensions: DEFAULT_JITI_EXTENSIONS,
115
+ ...(options.jitiOptions ?? {}),
116
+ });
117
+ return loader(modulePath);
118
+ }
@@ -0,0 +1,29 @@
1
+ export type WeixinAccountHelpers = {
2
+ listAccountIds: () => Promise<string[]>;
3
+ resolveAccount: (accountId: string) => Promise<{
4
+ accountId: string;
5
+ enabled: boolean;
6
+ configured: boolean;
7
+ name?: string;
8
+ userId?: string;
9
+ }>;
10
+ describeAccount: (accountIdOrInput: string | {
11
+ accountId: string;
12
+ }) => Promise<{
13
+ accountId: string;
14
+ enabled: boolean;
15
+ configured: boolean;
16
+ name?: string;
17
+ userId?: string;
18
+ }>;
19
+ };
20
+ type OpenClawAccountSourceHelpers = {
21
+ listAccountIds: () => string[] | Promise<string[]>;
22
+ loadAccount: (accountId: string) => unknown | Promise<unknown>;
23
+ resolveAccount: (accountId: string) => unknown | Promise<unknown>;
24
+ };
25
+ export declare function createOpenClawAccountHelpers(input: OpenClawAccountSourceHelpers): WeixinAccountHelpers;
26
+ export declare function loadOpenClawAccountHelpers(options?: {
27
+ accountsModulePath?: string;
28
+ }): Promise<WeixinAccountHelpers>;
29
+ export {};
@@ -0,0 +1,60 @@
1
+ import { createRequire } from "node:module";
2
+ import { loadModuleWithTsFallback } from "./jiti-loader.js";
3
+ const OPENCLAW_WEIXIN_ACCOUNTS_MODULE = "@tencent-weixin/openclaw-weixin/src/auth/accounts.ts";
4
+ function asObject(value) {
5
+ return value && typeof value === "object" ? value : {};
6
+ }
7
+ function deriveEnabled(resolved) {
8
+ return resolved.enabled === true;
9
+ }
10
+ function isConfigured(resolved) {
11
+ return resolved.configured === true;
12
+ }
13
+ function toAccountId(input) {
14
+ return typeof input === "string" ? input : input.accountId;
15
+ }
16
+ export function createOpenClawAccountHelpers(input) {
17
+ const resolveStableAccount = async (accountId) => {
18
+ const resolved = asObject(await input.resolveAccount(accountId));
19
+ const stored = asObject(await input.loadAccount(accountId));
20
+ return {
21
+ accountId,
22
+ enabled: deriveEnabled(resolved),
23
+ configured: isConfigured(resolved),
24
+ name: typeof resolved.name === "string" ? resolved.name : undefined,
25
+ userId: typeof stored.userId === "string" ? stored.userId : undefined,
26
+ };
27
+ };
28
+ return {
29
+ async listAccountIds() {
30
+ const ids = await input.listAccountIds();
31
+ return Array.isArray(ids) ? ids.filter((it) => typeof it === "string" && it.length > 0) : [];
32
+ },
33
+ async resolveAccount(accountId) {
34
+ return resolveStableAccount(accountId);
35
+ },
36
+ async describeAccount(accountIdOrInput) {
37
+ return resolveStableAccount(toAccountId(accountIdOrInput));
38
+ },
39
+ };
40
+ }
41
+ export async function loadOpenClawAccountHelpers(options = {}) {
42
+ const require = createRequire(import.meta.url);
43
+ const accountsModulePath = require.resolve(options.accountsModulePath ?? OPENCLAW_WEIXIN_ACCOUNTS_MODULE);
44
+ const accountsModule = await loadModuleWithTsFallback(accountsModulePath, { parentURL: import.meta.url });
45
+ if (typeof accountsModule.listIndexedWeixinAccountIds !== "function" || typeof accountsModule.loadWeixinAccount !== "function") {
46
+ throw new Error("[wechat-compat] account source helper unavailable");
47
+ }
48
+ return createOpenClawAccountHelpers({
49
+ listAccountIds: () => accountsModule.listIndexedWeixinAccountIds(),
50
+ loadAccount: (accountId) => accountsModule.loadWeixinAccount(accountId),
51
+ resolveAccount: async (accountId) => {
52
+ const stored = asObject(await accountsModule.loadWeixinAccount(accountId));
53
+ return {
54
+ accountId,
55
+ enabled: stored.enabled === false ? false : true,
56
+ configured: typeof stored.token === "string" && stored.token.trim().length > 0,
57
+ };
58
+ },
59
+ });
60
+ }
@@ -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,169 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { loadQrCodeTerminal } from "./qrcode-terminal-loader.js";
3
+ const DEFAULT_BASE_URL = "https://ilinkai.weixin.qq.com";
4
+ const DEFAULT_ILINK_BOT_TYPE = "3";
5
+ const ACTIVE_LOGIN_TTL_MS = 5 * 60_000;
6
+ const QR_LONG_POLL_TIMEOUT_MS = 35_000;
7
+ const activeLogins = new Map();
8
+ function asObject(value) {
9
+ return value && typeof value === "object" ? value : {};
10
+ }
11
+ function asNonEmptyString(value) {
12
+ return typeof value === "string" && value.trim().length > 0 ? value : undefined;
13
+ }
14
+ function asPositiveNumber(value) {
15
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : undefined;
16
+ }
17
+ function isLoginFresh(login) {
18
+ return Date.now() - login.startedAt < ACTIVE_LOGIN_TTL_MS;
19
+ }
20
+ function purgeExpiredLogins() {
21
+ for (const [sessionKey, login] of activeLogins) {
22
+ if (!isLoginFresh(login)) {
23
+ activeLogins.delete(sessionKey);
24
+ }
25
+ }
26
+ }
27
+ async function fetchQrCode(apiBaseUrl, botType) {
28
+ const base = apiBaseUrl.endsWith("/") ? apiBaseUrl : `${apiBaseUrl}/`;
29
+ const url = new URL(`ilink/bot/get_bot_qrcode?bot_type=${encodeURIComponent(botType)}`, base);
30
+ const response = await fetch(url.toString());
31
+ if (!response.ok) {
32
+ throw new Error(`Failed to fetch QR code: ${response.status} ${response.statusText}`);
33
+ }
34
+ return await response.json();
35
+ }
36
+ async function renderQrTerminal(value) {
37
+ const qrcodeTerminal = loadQrCodeTerminal();
38
+ return await new Promise((resolve, reject) => {
39
+ try {
40
+ qrcodeTerminal.generate(value, { small: true }, (output) => {
41
+ resolve(typeof output === "string" && output.trim().length > 0 ? output : undefined);
42
+ });
43
+ }
44
+ catch (error) {
45
+ reject(error);
46
+ }
47
+ });
48
+ }
49
+ async function pollQrStatus(apiBaseUrl, qrcode) {
50
+ const base = apiBaseUrl.endsWith("/") ? apiBaseUrl : `${apiBaseUrl}/`;
51
+ const url = new URL(`ilink/bot/get_qrcode_status?qrcode=${encodeURIComponent(qrcode)}`, base);
52
+ const controller = new AbortController();
53
+ const timer = setTimeout(() => controller.abort(), QR_LONG_POLL_TIMEOUT_MS);
54
+ try {
55
+ const response = await fetch(url.toString(), {
56
+ headers: { "iLink-App-ClientVersion": "1" },
57
+ signal: controller.signal,
58
+ });
59
+ clearTimeout(timer);
60
+ const rawText = await response.text();
61
+ if (!response.ok) {
62
+ throw new Error(`Failed to poll QR status: ${response.status} ${response.statusText}`);
63
+ }
64
+ const parsed = JSON.parse(rawText);
65
+ return parsed && typeof parsed === "object" ? parsed : {};
66
+ }
67
+ catch (error) {
68
+ clearTimeout(timer);
69
+ if (error instanceof Error && error.name === "AbortError") {
70
+ return { status: "wait" };
71
+ }
72
+ throw error;
73
+ }
74
+ }
75
+ function createAccountHelpers() {
76
+ async function describeAccount(accountId) {
77
+ return {
78
+ accountId,
79
+ enabled: true,
80
+ configured: false,
81
+ };
82
+ }
83
+ return {
84
+ async listAccountIds() {
85
+ return [];
86
+ },
87
+ resolveAccount: describeAccount,
88
+ async describeAccount(accountIdOrInput) {
89
+ const accountId = typeof accountIdOrInput === "string" ? accountIdOrInput : accountIdOrInput.accountId;
90
+ return await describeAccount(accountId);
91
+ },
92
+ };
93
+ }
94
+ export async function loadOpenClawWeixinBindHelpers() {
95
+ const accountHelpers = createAccountHelpers();
96
+ return {
97
+ qrGateway: {
98
+ async loginWithQrStart(input) {
99
+ const params = asObject(input);
100
+ const sessionKey = asNonEmptyString(params.accountId) ?? randomUUID();
101
+ const force = params.force === true;
102
+ purgeExpiredLogins();
103
+ const existing = activeLogins.get(sessionKey);
104
+ if (!force && existing && isLoginFresh(existing) && existing.qrcodeUrl) {
105
+ return {
106
+ qrcodeUrl: existing.qrcodeUrl,
107
+ message: "二维码已就绪,请使用微信扫描。",
108
+ sessionKey,
109
+ };
110
+ }
111
+ const qrResponse = await fetchQrCode(DEFAULT_BASE_URL, DEFAULT_ILINK_BOT_TYPE);
112
+ activeLogins.set(sessionKey, {
113
+ sessionKey,
114
+ qrcode: qrResponse.qrcode,
115
+ qrcodeUrl: qrResponse.qrcode_img_content,
116
+ startedAt: Date.now(),
117
+ });
118
+ return {
119
+ qrTerminal: await renderQrTerminal(qrResponse.qrcode_img_content).catch(() => undefined),
120
+ qrDataUrl: qrResponse.qrcode_img_content,
121
+ qrcodeUrl: qrResponse.qrcode_img_content,
122
+ message: "使用微信扫描以下二维码,以完成连接。",
123
+ sessionKey,
124
+ };
125
+ },
126
+ async loginWithQrWait(input) {
127
+ const params = asObject(input);
128
+ const sessionKey = asNonEmptyString(params.sessionKey);
129
+ if (!sessionKey) {
130
+ throw new Error("missing sessionKey from qr wait");
131
+ }
132
+ let activeLogin = activeLogins.get(sessionKey);
133
+ if (!activeLogin) {
134
+ return { connected: false, message: "当前没有进行中的登录,请先发起登录。" };
135
+ }
136
+ if (!isLoginFresh(activeLogin)) {
137
+ activeLogins.delete(sessionKey);
138
+ return { connected: false, message: "二维码已过期,请重新生成。" };
139
+ }
140
+ const timeoutMs = Math.max(asPositiveNumber(params.timeoutMs) ?? 480_000, 1000);
141
+ const deadline = Date.now() + timeoutMs;
142
+ while (Date.now() < deadline) {
143
+ const statusResponse = await pollQrStatus(DEFAULT_BASE_URL, activeLogin.qrcode);
144
+ const status = asNonEmptyString(statusResponse.status);
145
+ if (status === "confirmed") {
146
+ activeLogins.delete(sessionKey);
147
+ return {
148
+ connected: true,
149
+ botToken: asNonEmptyString(statusResponse.bot_token),
150
+ accountId: asNonEmptyString(statusResponse.ilink_bot_id),
151
+ baseUrl: asNonEmptyString(statusResponse.baseurl),
152
+ userId: asNonEmptyString(statusResponse.ilink_user_id),
153
+ message: "✅ 与微信连接成功!",
154
+ };
155
+ }
156
+ if (status === "expired") {
157
+ activeLogins.delete(sessionKey);
158
+ return { connected: false, message: "二维码已过期,请重新生成。" };
159
+ }
160
+ await new Promise((resolve) => setTimeout(resolve, 1000));
161
+ activeLogin = activeLogins.get(sessionKey) ?? activeLogin;
162
+ }
163
+ activeLogins.delete(sessionKey);
164
+ return { connected: false, message: "登录超时,请重试。" };
165
+ },
166
+ },
167
+ accountHelpers,
168
+ };
169
+ }