@tencent-weixin/openclaw-weixin 2.3.1 → 2.4.2

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 (102) hide show
  1. package/dist/index.js +16 -0
  2. package/dist/index.js.map +1 -0
  3. package/dist/src/api/api.js +374 -0
  4. package/dist/src/api/api.js.map +1 -0
  5. package/dist/src/api/config-cache.js +64 -0
  6. package/dist/src/api/config-cache.js.map +1 -0
  7. package/dist/src/api/session-guard.js +49 -0
  8. package/dist/src/api/session-guard.js.map +1 -0
  9. package/dist/src/api/types.js +35 -0
  10. package/dist/src/api/types.js.map +1 -0
  11. package/dist/src/auth/accounts.js +326 -0
  12. package/dist/src/auth/accounts.js.map +1 -0
  13. package/dist/src/auth/login-qr.js +332 -0
  14. package/dist/src/auth/login-qr.js.map +1 -0
  15. package/dist/src/auth/pairing.js +104 -0
  16. package/dist/src/auth/pairing.js.map +1 -0
  17. package/dist/src/cdn/aes-ecb.js +19 -0
  18. package/dist/src/cdn/aes-ecb.js.map +1 -0
  19. package/dist/src/cdn/cdn-upload.js +73 -0
  20. package/dist/src/cdn/cdn-upload.js.map +1 -0
  21. package/dist/src/cdn/cdn-url.js +14 -0
  22. package/dist/src/cdn/cdn-url.js.map +1 -0
  23. package/dist/src/cdn/pic-decrypt.js +89 -0
  24. package/dist/src/cdn/pic-decrypt.js.map +1 -0
  25. package/dist/src/cdn/upload.js +106 -0
  26. package/dist/src/cdn/upload.js.map +1 -0
  27. package/dist/src/channel.js +460 -0
  28. package/dist/src/channel.js.map +1 -0
  29. package/dist/src/compat.js +67 -0
  30. package/dist/src/compat.js.map +1 -0
  31. package/dist/src/config/config-schema.js +19 -0
  32. package/dist/src/config/config-schema.js.map +1 -0
  33. package/dist/src/media/media-download.js +95 -0
  34. package/dist/src/media/media-download.js.map +1 -0
  35. package/dist/src/media/mime.js +73 -0
  36. package/dist/src/media/mime.js.map +1 -0
  37. package/dist/src/media/silk-transcode.js +64 -0
  38. package/dist/src/media/silk-transcode.js.map +1 -0
  39. package/dist/src/media/voice-outbound.js +177 -0
  40. package/dist/src/media/voice-outbound.js.map +1 -0
  41. package/dist/src/messaging/abort-fence.js +70 -0
  42. package/dist/src/messaging/abort-fence.js.map +1 -0
  43. package/dist/src/messaging/buttons.js +117 -0
  44. package/dist/src/messaging/buttons.js.map +1 -0
  45. package/dist/src/messaging/debug-mode.js +63 -0
  46. package/dist/src/messaging/debug-mode.js.map +1 -0
  47. package/dist/src/messaging/error-notice.js +24 -0
  48. package/dist/src/messaging/error-notice.js.map +1 -0
  49. package/dist/src/messaging/inbound.js +201 -0
  50. package/dist/src/messaging/inbound.js.map +1 -0
  51. package/dist/src/messaging/lane-key.js +66 -0
  52. package/dist/src/messaging/lane-key.js.map +1 -0
  53. package/dist/src/messaging/markdown-filter.js +368 -0
  54. package/dist/src/messaging/markdown-filter.js.map +1 -0
  55. package/dist/src/messaging/merged-record.js +149 -0
  56. package/dist/src/messaging/merged-record.js.map +1 -0
  57. package/dist/src/messaging/model-buttons.js +182 -0
  58. package/dist/src/messaging/model-buttons.js.map +1 -0
  59. package/dist/src/messaging/model-callback-handler.js +133 -0
  60. package/dist/src/messaging/model-callback-handler.js.map +1 -0
  61. package/dist/src/messaging/outbound-hooks.js +56 -0
  62. package/dist/src/messaging/outbound-hooks.js.map +1 -0
  63. package/dist/src/messaging/process-message.js +381 -0
  64. package/dist/src/messaging/process-message.js.map +1 -0
  65. package/dist/src/messaging/send-media.js +54 -0
  66. package/dist/src/messaging/send-media.js.map +1 -0
  67. package/dist/src/messaging/send.js +182 -0
  68. package/dist/src/messaging/send.js.map +1 -0
  69. package/dist/src/messaging/slash-commands.js +70 -0
  70. package/dist/src/messaging/slash-commands.js.map +1 -0
  71. package/dist/src/monitor/lane-scheduler.js +46 -0
  72. package/dist/src/monitor/lane-scheduler.js.map +1 -0
  73. package/dist/src/monitor/monitor.js +143 -0
  74. package/dist/src/monitor/monitor.js.map +1 -0
  75. package/dist/src/runtime.js +54 -0
  76. package/dist/src/runtime.js.map +1 -0
  77. package/dist/src/storage/state-dir.js +9 -0
  78. package/dist/src/storage/state-dir.js.map +1 -0
  79. package/dist/src/storage/sync-buf.js +64 -0
  80. package/dist/src/storage/sync-buf.js.map +1 -0
  81. package/dist/src/streaming/stream-pipeline.js +431 -0
  82. package/dist/src/streaming/stream-pipeline.js.map +1 -0
  83. package/dist/src/streaming/stream-session.js +260 -0
  84. package/dist/src/streaming/stream-session.js.map +1 -0
  85. package/dist/src/streaming/stream.js +239 -0
  86. package/dist/src/streaming/stream.js.map +1 -0
  87. package/dist/src/util/logger.js +120 -0
  88. package/dist/src/util/logger.js.map +1 -0
  89. package/dist/src/util/markdown-fences.js +54 -0
  90. package/dist/src/util/markdown-fences.js.map +1 -0
  91. package/dist/src/util/random.js +16 -0
  92. package/dist/src/util/random.js.map +1 -0
  93. package/dist/src/util/redact.js +54 -0
  94. package/dist/src/util/redact.js.map +1 -0
  95. package/index.ts +0 -5
  96. package/openclaw.plugin.json +11 -1
  97. package/package.json +9 -2
  98. package/src/api/api.ts +2 -3
  99. package/src/auth/accounts.ts +0 -1
  100. package/src/channel.ts +13 -1
  101. package/src/monitor/monitor.ts +11 -10
  102. package/src/runtime.ts +0 -70
@@ -0,0 +1,326 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { normalizeAccountId } from "openclaw/plugin-sdk/account-id";
4
+ import { resolveStateDir } from "../storage/state-dir.js";
5
+ import { resolveFrameworkAllowFromPath } from "./pairing.js";
6
+ import { logger } from "../util/logger.js";
7
+ export const DEFAULT_BASE_URL = "https://ilinkai.weixin.qq.com";
8
+ export const CDN_BASE_URL = "https://novac2c.cdn.weixin.qq.com/c2c";
9
+ // ---------------------------------------------------------------------------
10
+ // Account ID compatibility (legacy raw ID → normalized ID)
11
+ // ---------------------------------------------------------------------------
12
+ /**
13
+ * Pattern-based reverse of normalizeWeixinAccountId for known weixin ID suffixes.
14
+ * Used only as a compatibility fallback when loading accounts / sync bufs stored
15
+ * under the old raw ID.
16
+ * e.g. "b0f5860fdecb-im-bot" → "b0f5860fdecb@im.bot"
17
+ */
18
+ export function deriveRawAccountId(normalizedId) {
19
+ if (normalizedId.endsWith("-im-bot")) {
20
+ return `${normalizedId.slice(0, -7)}@im.bot`;
21
+ }
22
+ if (normalizedId.endsWith("-im-wechat")) {
23
+ return `${normalizedId.slice(0, -10)}@im.wechat`;
24
+ }
25
+ return undefined;
26
+ }
27
+ // ---------------------------------------------------------------------------
28
+ // Account index (persistent list of registered account IDs)
29
+ // ---------------------------------------------------------------------------
30
+ function resolveWeixinStateDir() {
31
+ return path.join(resolveStateDir(), "openclaw-weixin");
32
+ }
33
+ function resolveAccountIndexPath() {
34
+ return path.join(resolveWeixinStateDir(), "accounts.json");
35
+ }
36
+ /** Returns all accountIds registered via QR login. */
37
+ export function listIndexedWeixinAccountIds() {
38
+ const filePath = resolveAccountIndexPath();
39
+ try {
40
+ if (!fs.existsSync(filePath))
41
+ return [];
42
+ const raw = fs.readFileSync(filePath, "utf-8");
43
+ const parsed = JSON.parse(raw);
44
+ if (!Array.isArray(parsed))
45
+ return [];
46
+ return parsed.filter((id) => typeof id === "string" && id.trim() !== "");
47
+ }
48
+ catch {
49
+ return [];
50
+ }
51
+ }
52
+ /** Add accountId to the persistent index (no-op if already present). */
53
+ export function registerWeixinAccountId(accountId) {
54
+ const dir = resolveWeixinStateDir();
55
+ fs.mkdirSync(dir, { recursive: true });
56
+ const existing = listIndexedWeixinAccountIds();
57
+ if (existing.includes(accountId))
58
+ return;
59
+ const updated = [...existing, accountId];
60
+ fs.writeFileSync(resolveAccountIndexPath(), JSON.stringify(updated, null, 2), "utf-8");
61
+ }
62
+ /** Remove accountId from the persistent index. */
63
+ export function unregisterWeixinAccountId(accountId) {
64
+ const existing = listIndexedWeixinAccountIds();
65
+ const updated = existing.filter((id) => id !== accountId);
66
+ if (updated.length !== existing.length) {
67
+ fs.writeFileSync(resolveAccountIndexPath(), JSON.stringify(updated, null, 2), "utf-8");
68
+ }
69
+ }
70
+ /**
71
+ * Remove stale accounts that share the same userId as the newly-bound account.
72
+ * Called after a successful QR login to ensure only the latest account remains
73
+ * for a given WeChat user, preventing ambiguous contextToken matches.
74
+ *
75
+ * @param onClearContextTokens callback to clear context tokens for the removed account
76
+ */
77
+ export function clearStaleAccountsForUserId(currentAccountId, userId, onClearContextTokens) {
78
+ if (!userId)
79
+ return;
80
+ const allIds = listIndexedWeixinAccountIds();
81
+ for (const id of allIds) {
82
+ if (id === currentAccountId)
83
+ continue;
84
+ const data = loadWeixinAccount(id);
85
+ if (data?.userId?.trim() === userId) {
86
+ logger.info(`clearStaleAccountsForUserId: removing stale account=${id} (same userId=${userId})`);
87
+ onClearContextTokens?.(id);
88
+ clearWeixinAccount(id);
89
+ unregisterWeixinAccountId(id);
90
+ }
91
+ }
92
+ }
93
+ function resolveAccountsDir() {
94
+ return path.join(resolveWeixinStateDir(), "accounts");
95
+ }
96
+ function resolveAccountPath(accountId) {
97
+ return path.join(resolveAccountsDir(), `${accountId}.json`);
98
+ }
99
+ /**
100
+ * Legacy single-file token: `credentials/openclaw-weixin/credentials.json` (pre per-account files).
101
+ */
102
+ function loadLegacyToken() {
103
+ const legacyPath = path.join(resolveStateDir(), "credentials", "openclaw-weixin", "credentials.json");
104
+ try {
105
+ if (!fs.existsSync(legacyPath))
106
+ return undefined;
107
+ const raw = fs.readFileSync(legacyPath, "utf-8");
108
+ const parsed = JSON.parse(raw);
109
+ return typeof parsed.token === "string" ? parsed.token : undefined;
110
+ }
111
+ catch {
112
+ return undefined;
113
+ }
114
+ }
115
+ function readAccountFile(filePath) {
116
+ try {
117
+ if (fs.existsSync(filePath)) {
118
+ return JSON.parse(fs.readFileSync(filePath, "utf-8"));
119
+ }
120
+ }
121
+ catch {
122
+ // ignore
123
+ }
124
+ return null;
125
+ }
126
+ /** Load account data by ID, with compatibility fallbacks. */
127
+ export function loadWeixinAccount(accountId) {
128
+ // Primary: try given accountId (normalized IDs written after this change).
129
+ const primary = readAccountFile(resolveAccountPath(accountId));
130
+ if (primary)
131
+ return primary;
132
+ // Compatibility: if the given ID is normalized, derive the old raw filename
133
+ // (e.g. "b0f5860fdecb-im-bot" → "b0f5860fdecb@im.bot") for existing installs.
134
+ const rawId = deriveRawAccountId(accountId);
135
+ if (rawId) {
136
+ const compat = readAccountFile(resolveAccountPath(rawId));
137
+ if (compat)
138
+ return compat;
139
+ }
140
+ // Legacy fallback: read token from old single-account credentials file.
141
+ const token = loadLegacyToken();
142
+ if (token)
143
+ return { token };
144
+ return null;
145
+ }
146
+ /**
147
+ * Persist account data after QR login (merges into existing file).
148
+ * - token: overwritten when provided.
149
+ * - baseUrl: stored when non-empty; resolveWeixinAccount falls back to DEFAULT_BASE_URL.
150
+ * - userId: set when `update.userId` is provided; omitted from file when cleared to empty.
151
+ */
152
+ export function saveWeixinAccount(accountId, update) {
153
+ const dir = resolveAccountsDir();
154
+ fs.mkdirSync(dir, { recursive: true });
155
+ const existing = loadWeixinAccount(accountId) ?? {};
156
+ const token = update.token?.trim() || existing.token;
157
+ const baseUrl = update.baseUrl?.trim() || existing.baseUrl;
158
+ const userId = update.userId !== undefined
159
+ ? update.userId.trim() || undefined
160
+ : existing.userId?.trim() || undefined;
161
+ const data = {
162
+ ...(token ? { token, savedAt: new Date().toISOString() } : {}),
163
+ ...(baseUrl ? { baseUrl } : {}),
164
+ ...(userId ? { userId } : {}),
165
+ };
166
+ const filePath = resolveAccountPath(accountId);
167
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf-8");
168
+ try {
169
+ fs.chmodSync(filePath, 0o600);
170
+ }
171
+ catch {
172
+ // best-effort
173
+ }
174
+ }
175
+ /**
176
+ * Remove all files associated with an account:
177
+ * - accounts/{accountId}.json (credentials)
178
+ * - accounts/{accountId}.sync.json (getUpdates sync buf)
179
+ * - accounts/{accountId}.context-tokens.json (context tokens on disk)
180
+ * - credentials/openclaw-weixin-{accountId}-allowFrom.json (authorized users)
181
+ */
182
+ export function clearWeixinAccount(accountId) {
183
+ const dir = resolveAccountsDir();
184
+ const accountFiles = [
185
+ `${accountId}.json`,
186
+ `${accountId}.sync.json`,
187
+ `${accountId}.context-tokens.json`,
188
+ ];
189
+ for (const file of accountFiles) {
190
+ try {
191
+ fs.unlinkSync(path.join(dir, file));
192
+ }
193
+ catch {
194
+ // ignore if not found
195
+ }
196
+ }
197
+ try {
198
+ fs.unlinkSync(resolveFrameworkAllowFromPath(accountId));
199
+ }
200
+ catch {
201
+ // ignore if not found
202
+ }
203
+ }
204
+ /**
205
+ * Resolve the openclaw.json config file path.
206
+ * Checks OPENCLAW_CONFIG env var, then state dir.
207
+ */
208
+ function resolveConfigPath() {
209
+ const envPath = process.env.OPENCLAW_CONFIG?.trim();
210
+ if (envPath)
211
+ return envPath;
212
+ return path.join(resolveStateDir(), "openclaw.json");
213
+ }
214
+ /**
215
+ * Read `routeTag` from openclaw.json (for callers without an `OpenClawConfig` object).
216
+ * Checks per-account `channels.<id>.accounts[accountId].routeTag` first, then section-level
217
+ * `channels.<id>.routeTag`. Matches `feat_weixin_extension` behavior; channel key is `"openclaw-weixin"`.
218
+ *
219
+ * The config is cached after the first read since routeTag does not change at runtime.
220
+ */
221
+ let cachedRouteTagSection;
222
+ function loadRouteTagSection() {
223
+ if (cachedRouteTagSection !== undefined)
224
+ return cachedRouteTagSection;
225
+ try {
226
+ const configPath = resolveConfigPath();
227
+ if (!fs.existsSync(configPath)) {
228
+ cachedRouteTagSection = null;
229
+ return null;
230
+ }
231
+ const raw = fs.readFileSync(configPath, "utf-8");
232
+ const cfg = JSON.parse(raw);
233
+ const channels = cfg.channels;
234
+ const section = channels?.["openclaw-weixin"] ?? null;
235
+ cachedRouteTagSection = section;
236
+ return section;
237
+ }
238
+ catch {
239
+ cachedRouteTagSection = null;
240
+ return null;
241
+ }
242
+ }
243
+ export function loadConfigRouteTag(accountId) {
244
+ const section = loadRouteTagSection();
245
+ if (!section)
246
+ return undefined;
247
+ if (accountId) {
248
+ const accounts = section.accounts;
249
+ const tag = accounts?.[accountId]?.routeTag;
250
+ if (typeof tag === "number")
251
+ return String(tag);
252
+ if (typeof tag === "string" && tag.trim())
253
+ return tag.trim();
254
+ }
255
+ if (typeof section.routeTag === "number")
256
+ return String(section.routeTag);
257
+ return typeof section.routeTag === "string" && section.routeTag.trim()
258
+ ? section.routeTag.trim()
259
+ : undefined;
260
+ }
261
+ /**
262
+ * Read `botAgent` from `channels.openclaw-weixin.botAgent` in openclaw.json.
263
+ * Returns the raw configured string (caller is responsible for sanitization)
264
+ * or undefined when not set. Reuses the cached channel section.
265
+ */
266
+ export function loadConfigBotAgent() {
267
+ const section = loadRouteTagSection();
268
+ if (!section)
269
+ return undefined;
270
+ const value = section.botAgent;
271
+ return typeof value === "string" && value.trim() ? value : undefined;
272
+ }
273
+ /**
274
+ * Bump `channels.openclaw-weixin.channelConfigUpdatedAt` in openclaw.json on each successful login
275
+ * so the gateway reloads config from disk (no empty `accounts: {}` placeholder).
276
+ */
277
+ export async function triggerWeixinChannelReload() {
278
+ try {
279
+ const { loadConfig, writeConfigFile } = await import("openclaw/plugin-sdk/config-runtime");
280
+ const cfg = loadConfig();
281
+ const channels = (cfg.channels ?? {});
282
+ const existing = channels["openclaw-weixin"] ?? {};
283
+ const updated = {
284
+ ...cfg,
285
+ channels: {
286
+ ...channels,
287
+ "openclaw-weixin": {
288
+ ...existing,
289
+ channelConfigUpdatedAt: new Date().toISOString(),
290
+ },
291
+ },
292
+ };
293
+ await writeConfigFile(updated);
294
+ logger.info("triggerWeixinChannelReload: wrote channel config to openclaw.json");
295
+ }
296
+ catch (err) {
297
+ logger.warn(`triggerWeixinChannelReload: failed to update config: ${String(err)}`);
298
+ }
299
+ }
300
+ /** List accountIds from the index file (written at QR login). */
301
+ export function listWeixinAccountIds(_cfg) {
302
+ return listIndexedWeixinAccountIds();
303
+ }
304
+ /** Resolve a weixin account by ID, merging config and stored credentials. */
305
+ export function resolveWeixinAccount(cfg, accountId) {
306
+ const raw = accountId?.trim();
307
+ if (!raw) {
308
+ throw new Error("weixin: accountId is required (no default account)");
309
+ }
310
+ const id = normalizeAccountId(raw);
311
+ const section = cfg.channels?.["openclaw-weixin"];
312
+ const accountCfg = section?.accounts?.[id] ?? section ?? {};
313
+ const accountData = loadWeixinAccount(id);
314
+ const token = accountData?.token?.trim() || undefined;
315
+ const stateBaseUrl = accountData?.baseUrl?.trim() || "";
316
+ return {
317
+ accountId: id,
318
+ baseUrl: stateBaseUrl || DEFAULT_BASE_URL,
319
+ cdnBaseUrl: accountCfg.cdnBaseUrl?.trim() || CDN_BASE_URL,
320
+ token,
321
+ enabled: accountCfg.enabled !== false,
322
+ configured: Boolean(token),
323
+ name: accountCfg.name?.trim() || undefined,
324
+ };
325
+ }
326
+ //# sourceMappingURL=accounts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"accounts.js","sourceRoot":"","sources":["../../../src/auth/accounts.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,332 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { apiGetFetch, apiPostFetch } from "../api/api.js";
3
+ import { listIndexedWeixinAccountIds, loadWeixinAccount } from "./accounts.js";
4
+ import { logger } from "../util/logger.js";
5
+ import { redactToken } from "../util/redact.js";
6
+ const ACTIVE_LOGIN_TTL_MS = 5 * 60_000;
7
+ /** Client-side timeout for the long-poll get_qrcode_status request. */
8
+ const QR_LONG_POLL_TIMEOUT_MS = 35_000;
9
+ /** Default `bot_type` for ilink get_bot_qrcode / get_qrcode_status (this channel build). */
10
+ export const DEFAULT_ILINK_BOT_TYPE = "3";
11
+ /** Fixed API base URL for all QR code requests. */
12
+ const FIXED_BASE_URL = "https://ilinkai.weixin.qq.com";
13
+ const activeLogins = new Map();
14
+ function isLoginFresh(login) {
15
+ return Date.now() - login.startedAt < ACTIVE_LOGIN_TTL_MS;
16
+ }
17
+ /** Remove all expired entries from the activeLogins map to prevent memory leaks. */
18
+ function purgeExpiredLogins() {
19
+ for (const [id, login] of activeLogins) {
20
+ if (!isLoginFresh(login)) {
21
+ activeLogins.delete(id);
22
+ }
23
+ }
24
+ }
25
+ /** 获取本地已登录账号的 bot token 列表,最多返回最新的 10 个。 */
26
+ function getLocalBotTokenList() {
27
+ const accountIds = listIndexedWeixinAccountIds();
28
+ const tokens = [];
29
+ // 从最新注册的账号开始取(列表末尾为最新)
30
+ for (let i = accountIds.length - 1; i >= 0 && tokens.length < 10; i--) {
31
+ const data = loadWeixinAccount(accountIds[i]);
32
+ const token = data?.token?.trim();
33
+ if (token) {
34
+ tokens.push(token);
35
+ }
36
+ }
37
+ return tokens;
38
+ }
39
+ async function fetchQRCode(apiBaseUrl, botType) {
40
+ logger.info(`NewFetching QR code from: ${apiBaseUrl} bot_type=${botType}`);
41
+ const localTokenList = getLocalBotTokenList();
42
+ logger.info(`newfetchQRCode: local_token_list count=${localTokenList.length}`);
43
+ const rawText = await apiPostFetch({
44
+ baseUrl: apiBaseUrl,
45
+ endpoint: `ilink/bot/get_bot_qrcode?bot_type=${encodeURIComponent(botType)}`,
46
+ body: JSON.stringify({ local_token_list: localTokenList }),
47
+ label: "fetchQRCode",
48
+ });
49
+ return JSON.parse(rawText);
50
+ }
51
+ /** 从 stdin 读取一行用户输入,输出提示语后等待回车确认,返回 trim 后的字符串。 */
52
+ async function readVerifyCodeFromStdin(prompt) {
53
+ process.stdout.write(prompt);
54
+ return new Promise((resolve) => {
55
+ let input = "";
56
+ const onData = (chunk) => {
57
+ const str = chunk.toString();
58
+ input += str;
59
+ if (input.includes("\n")) {
60
+ process.stdin.removeListener("data", onData);
61
+ process.stdin.pause();
62
+ resolve(input.trim());
63
+ }
64
+ };
65
+ process.stdin.resume();
66
+ process.stdin.setEncoding("utf-8");
67
+ process.stdin.on("data", onData);
68
+ });
69
+ }
70
+ async function pollQRStatus(apiBaseUrl, qrcode, verifyCode) {
71
+ logger.debug(`Long-poll QR status from: ${apiBaseUrl} qrcode=***`);
72
+ try {
73
+ let endpoint = `ilink/bot/get_qrcode_status?qrcode=${encodeURIComponent(qrcode)}`;
74
+ if (verifyCode) {
75
+ endpoint += `&verify_code=${encodeURIComponent(verifyCode)}`;
76
+ }
77
+ const rawText = await apiGetFetch({
78
+ baseUrl: apiBaseUrl,
79
+ endpoint,
80
+ timeoutMs: QR_LONG_POLL_TIMEOUT_MS,
81
+ label: "pollQRStatus",
82
+ });
83
+ logger.debug(`pollQRStatus: body=${rawText.substring(0, 200)}`);
84
+ return JSON.parse(rawText);
85
+ }
86
+ catch (err) {
87
+ if (err instanceof Error && err.name === "AbortError") {
88
+ logger.debug(`pollQRStatus: client-side timeout after ${QR_LONG_POLL_TIMEOUT_MS}ms, returning wait`);
89
+ return { status: "wait" };
90
+ }
91
+ // 网关超时(如 Cloudflare 524)或其他网络错误,视为等待状态继续轮询
92
+ logger.warn(`pollQRStatus: network/gateway error, will retry: ${String(err)}`);
93
+ return { status: "wait" };
94
+ }
95
+ }
96
+ /**
97
+ * 在终端展示二维码及备用链接。
98
+ * 供 CLI 登录流程和 MCP Tool 登录流程共同复用。
99
+ */
100
+ export async function displayQRCode(qrcodeUrl) {
101
+ try {
102
+ const qrterm = await import("qrcode-terminal");
103
+ qrterm.default.generate(qrcodeUrl, { small: true });
104
+ process.stdout.write(`若二维码未能显示或无法使用,你可以访问以下链接以继续:\n`);
105
+ process.stdout.write(`${qrcodeUrl}\n`);
106
+ }
107
+ catch {
108
+ process.stdout.write(`若二维码未能显示或无法使用,你可以访问以下链接以继续:\n`);
109
+ process.stdout.write(`${qrcodeUrl}\n`);
110
+ }
111
+ }
112
+ export async function startWeixinLoginWithQr(opts) {
113
+ const sessionKey = opts.accountId || randomUUID();
114
+ purgeExpiredLogins();
115
+ const existing = activeLogins.get(sessionKey);
116
+ if (!opts.force && existing && isLoginFresh(existing) && existing.qrcodeUrl) {
117
+ return {
118
+ qrcodeUrl: existing.qrcodeUrl,
119
+ message: "二维码已显示,请用手机微信扫描。",
120
+ sessionKey,
121
+ };
122
+ }
123
+ try {
124
+ const botType = opts.botType || DEFAULT_ILINK_BOT_TYPE;
125
+ logger.info(`Starting Weixin login with bot_type=${botType}`);
126
+ const qrResponse = await fetchQRCode(FIXED_BASE_URL, botType);
127
+ logger.info(`QR code received, qrcode=${redactToken(qrResponse.qrcode)} imgContentLen=${qrResponse.qrcode_img_content?.length ?? 0}`);
128
+ logger.info(`二维码链接: ${qrResponse.qrcode_img_content}`);
129
+ const login = {
130
+ sessionKey,
131
+ id: randomUUID(),
132
+ qrcode: qrResponse.qrcode,
133
+ qrcodeUrl: qrResponse.qrcode_img_content,
134
+ startedAt: Date.now(),
135
+ };
136
+ activeLogins.set(sessionKey, login);
137
+ return {
138
+ qrcodeUrl: qrResponse.qrcode_img_content,
139
+ message: "用手机微信扫描以下二维码,以继续连接:",
140
+ sessionKey,
141
+ };
142
+ }
143
+ catch (err) {
144
+ logger.error(`Failed to start Weixin login: ${String(err)}`);
145
+ return {
146
+ message: `Failed to start login: ${String(err)}`,
147
+ sessionKey,
148
+ };
149
+ }
150
+ }
151
+ const MAX_QR_REFRESH_COUNT = 3;
152
+ /**
153
+ * 刷新二维码并展示给用户,返回是否成功。
154
+ * 成功时更新 activeLogin 的 qrcode/qrcodeUrl/startedAt,并重置 scannedPrinted。
155
+ */
156
+ async function refreshQRCode(activeLogin, botType, qrRefreshCount, onScannedReset) {
157
+ process.stdout.write(`\n⏳ 正在刷新二维码...(${qrRefreshCount}/${MAX_QR_REFRESH_COUNT})\n`);
158
+ logger.info(`waitForWeixinLogin: refreshing QR code (${qrRefreshCount}/${MAX_QR_REFRESH_COUNT})`);
159
+ try {
160
+ const qrResponse = await fetchQRCode(FIXED_BASE_URL, botType);
161
+ activeLogin.qrcode = qrResponse.qrcode;
162
+ activeLogin.qrcodeUrl = qrResponse.qrcode_img_content;
163
+ activeLogin.startedAt = Date.now();
164
+ onScannedReset();
165
+ logger.info(`waitForWeixinLogin: new QR code obtained qrcode=${redactToken(qrResponse.qrcode)}`);
166
+ process.stdout.write(`🔄 二维码已更新,请重新扫描。\n\n`);
167
+ await displayQRCode(qrResponse.qrcode_img_content);
168
+ return { success: true };
169
+ }
170
+ catch (refreshErr) {
171
+ logger.error(`waitForWeixinLogin: failed to refresh QR code: ${String(refreshErr)}`);
172
+ return { success: false, message: `刷新二维码失败: ${String(refreshErr)}` };
173
+ }
174
+ }
175
+ export async function waitForWeixinLogin(opts) {
176
+ let activeLogin = activeLogins.get(opts.sessionKey);
177
+ if (!activeLogin) {
178
+ logger.warn(`waitForWeixinLogin: no active login sessionKey=${opts.sessionKey}`);
179
+ return {
180
+ connected: false,
181
+ message: "当前没有进行中的登录,请先发起登录。",
182
+ };
183
+ }
184
+ if (!isLoginFresh(activeLogin)) {
185
+ logger.warn(`waitForWeixinLogin: login QR expired sessionKey=${opts.sessionKey}`);
186
+ activeLogins.delete(opts.sessionKey);
187
+ return {
188
+ connected: false,
189
+ message: "二维码已过期,请重新生成。",
190
+ };
191
+ }
192
+ const timeoutMs = Math.max(opts.timeoutMs ?? 480_000, 1000);
193
+ const deadline = Date.now() + timeoutMs;
194
+ let scannedPrinted = false;
195
+ let qrRefreshCount = 1;
196
+ // Initialize the effective polling base URL; may be updated on IDC redirect.
197
+ activeLogin.currentApiBaseUrl = FIXED_BASE_URL;
198
+ logger.info("Starting to poll QR code status...");
199
+ while (Date.now() < deadline) {
200
+ try {
201
+ const currentBaseUrl = activeLogin.currentApiBaseUrl ?? FIXED_BASE_URL;
202
+ const statusResponse = await pollQRStatus(currentBaseUrl, activeLogin.qrcode, activeLogin.pendingVerifyCode);
203
+ logger.debug(`pollQRStatus: status=${statusResponse.status} hasBotToken=${Boolean(statusResponse.bot_token)} hasBotId=${Boolean(statusResponse.ilink_bot_id)}`);
204
+ activeLogin.status = statusResponse.status;
205
+ switch (statusResponse.status) {
206
+ case "wait":
207
+ if (opts.verbose) {
208
+ process.stdout.write(".");
209
+ }
210
+ break;
211
+ case "scaned":
212
+ // 若携带了配对码且服务端返回 scaned,说明验证码正确,清除暂存
213
+ if (activeLogin.pendingVerifyCode) {
214
+ logger.info("verify code accepted, resuming polling");
215
+ activeLogin.pendingVerifyCode = undefined;
216
+ }
217
+ if (!scannedPrinted) {
218
+ process.stdout.write("\n正在验证\n");
219
+ scannedPrinted = true;
220
+ }
221
+ break;
222
+ case "need_verifycode": {
223
+ // 首次进入提示输入,再次进入(已有 pendingVerifyCode)说明上次输入错误
224
+ const verifyPrompt = activeLogin.pendingVerifyCode
225
+ ? "❌ 你输入的数字不匹配,请重新输入:"
226
+ : "输入手机微信显示的数字,以继续连接:";
227
+ const code = await readVerifyCodeFromStdin(verifyPrompt);
228
+ activeLogin.pendingVerifyCode = code;
229
+ // 立即进入下一次轮询,不等待 1s
230
+ continue;
231
+ }
232
+ case "expired": {
233
+ qrRefreshCount++;
234
+ if (qrRefreshCount > MAX_QR_REFRESH_COUNT) {
235
+ logger.warn(`waitForWeixinLogin: QR expired ${MAX_QR_REFRESH_COUNT} times, giving up sessionKey=${opts.sessionKey}`);
236
+ activeLogins.delete(opts.sessionKey);
237
+ return {
238
+ connected: false,
239
+ message: "二维码多次失效,连接流程已停止。请稍后再试。",
240
+ };
241
+ }
242
+ process.stdout.write(`\n⏳ 二维码已过期,正在刷新...\n`);
243
+ const expiredRefreshResult = await refreshQRCode(activeLogin, opts.botType || DEFAULT_ILINK_BOT_TYPE, qrRefreshCount, () => { scannedPrinted = false; });
244
+ if (!expiredRefreshResult.success) {
245
+ activeLogins.delete(opts.sessionKey);
246
+ return { connected: false, message: expiredRefreshResult.message };
247
+ }
248
+ break;
249
+ }
250
+ case "verify_code_blocked": {
251
+ logger.warn(`waitForWeixinLogin: verify code blocked, qrRefreshCount=${qrRefreshCount} sessionKey=${opts.sessionKey}`);
252
+ process.stdout.write("\n⛔ 多次输入错误,请稍后再试。\n");
253
+ // 清除配对码暂存
254
+ activeLogin.pendingVerifyCode = undefined;
255
+ qrRefreshCount++;
256
+ if (qrRefreshCount > MAX_QR_REFRESH_COUNT) {
257
+ logger.warn(`waitForWeixinLogin: verify_code_blocked and QR refresh limit reached, giving up sessionKey=${opts.sessionKey}`);
258
+ activeLogins.delete(opts.sessionKey);
259
+ return {
260
+ connected: false,
261
+ message: "多次输入错误,连接流程已停止。请稍后再试。",
262
+ };
263
+ }
264
+ const blockedRefreshResult = await refreshQRCode(activeLogin, opts.botType || DEFAULT_ILINK_BOT_TYPE, qrRefreshCount, () => { scannedPrinted = false; });
265
+ if (!blockedRefreshResult.success) {
266
+ activeLogins.delete(opts.sessionKey);
267
+ return { connected: false, message: blockedRefreshResult.message };
268
+ }
269
+ break;
270
+ }
271
+ case "binded_redirect": {
272
+ logger.info(`waitForWeixinLogin: binded_redirect received, bot already bound sessionKey=${opts.sessionKey}`);
273
+ process.stdout.write("\n✅ 已连接过此 OpenClaw,无需重复连接。\n");
274
+ activeLogins.delete(opts.sessionKey);
275
+ return {
276
+ connected: false,
277
+ message: "已连接过此 OpenClaw,无需重复连接。",
278
+ };
279
+ }
280
+ case "scaned_but_redirect": {
281
+ const redirectHost = statusResponse.redirect_host;
282
+ if (redirectHost) {
283
+ const newBaseUrl = `https://${redirectHost}`;
284
+ activeLogin.currentApiBaseUrl = newBaseUrl;
285
+ logger.info(`waitForWeixinLogin: IDC redirect, switching polling host to ${redirectHost}`);
286
+ }
287
+ else {
288
+ logger.warn(`waitForWeixinLogin: received scaned_but_redirect but redirect_host is missing, continuing with current host`);
289
+ }
290
+ break;
291
+ }
292
+ case "confirmed": {
293
+ if (!statusResponse.ilink_bot_id) {
294
+ activeLogins.delete(opts.sessionKey);
295
+ logger.error("Login confirmed but ilink_bot_id missing from response");
296
+ return {
297
+ connected: false,
298
+ message: "登录失败:服务器未返回 ilink_bot_id。",
299
+ };
300
+ }
301
+ activeLogin.botToken = statusResponse.bot_token;
302
+ activeLogins.delete(opts.sessionKey);
303
+ logger.info(`✅ Login confirmed! ilink_bot_id=${statusResponse.ilink_bot_id} ilink_user_id=${redactToken(statusResponse.ilink_user_id)}`);
304
+ return {
305
+ connected: true,
306
+ botToken: statusResponse.bot_token,
307
+ accountId: statusResponse.ilink_bot_id,
308
+ baseUrl: statusResponse.baseurl,
309
+ userId: statusResponse.ilink_user_id,
310
+ message: "已将此 OpenClaw 连接到微信。",
311
+ };
312
+ }
313
+ }
314
+ }
315
+ catch (err) {
316
+ logger.error(`Error polling QR status: ${String(err)}`);
317
+ activeLogins.delete(opts.sessionKey);
318
+ return {
319
+ connected: false,
320
+ message: `Login failed: ${String(err)}`,
321
+ };
322
+ }
323
+ await new Promise((r) => setTimeout(r, 1000));
324
+ }
325
+ logger.warn(`waitForWeixinLogin: timed out waiting for QR scan sessionKey=${opts.sessionKey} timeoutMs=${timeoutMs}`);
326
+ activeLogins.delete(opts.sessionKey);
327
+ return {
328
+ connected: false,
329
+ message: "登录超时,请重试。",
330
+ };
331
+ }
332
+ //# sourceMappingURL=login-qr.js.map