pikiloop 0.4.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 (154) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +353 -0
  3. package/README.v2.md +287 -0
  4. package/README.zh-CN.md +352 -0
  5. package/dashboard/dist/assets/AgentTab-UZPIhlkr.js +1 -0
  6. package/dashboard/dist/assets/DirBrowser-Ckcmi-Pi.js +1 -0
  7. package/dashboard/dist/assets/ExtensionsTab-KZhEDrdu.js +1 -0
  8. package/dashboard/dist/assets/IMAccessTab-Bd_IY1GQ.js +1 -0
  9. package/dashboard/dist/assets/Modal-CTeL0y7P.js +1 -0
  10. package/dashboard/dist/assets/Modals-axftHasy.js +1 -0
  11. package/dashboard/dist/assets/Select-C8tOdPhe.js +1 -0
  12. package/dashboard/dist/assets/SessionPanel-C1geSRxw.js +1 -0
  13. package/dashboard/dist/assets/SystemTab-DBDkaPiO.js +1 -0
  14. package/dashboard/dist/assets/anthropic-BAdojD7P.ico +0 -0
  15. package/dashboard/dist/assets/codex-DYadqqp0.png +0 -0
  16. package/dashboard/dist/assets/deepseek-BeYNZEk0.ico +0 -0
  17. package/dashboard/dist/assets/doubao-DloFDuFR.png +0 -0
  18. package/dashboard/dist/assets/feishu-C4OMrjCW.ico +0 -0
  19. package/dashboard/dist/assets/gemini-BYkEpiWr.svg +1 -0
  20. package/dashboard/dist/assets/hermes-BAarh-tH.png +0 -0
  21. package/dashboard/dist/assets/index-CpM4CqZJ.js +23 -0
  22. package/dashboard/dist/assets/index-DXSohzrE.js +3 -0
  23. package/dashboard/dist/assets/index-reSbuley.css +1 -0
  24. package/dashboard/dist/assets/markdown-DxQYQFeH.js +29 -0
  25. package/dashboard/dist/assets/minimax-PuEGTfrF.ico +0 -0
  26. package/dashboard/dist/assets/mlx-DhWwjtMw.png +0 -0
  27. package/dashboard/dist/assets/ollama-Bt9O-2K_.png +0 -0
  28. package/dashboard/dist/assets/openrouter-CsJ_bD5Q.ico +0 -0
  29. package/dashboard/dist/assets/playwright-BldPFZgC.ico +0 -0
  30. package/dashboard/dist/assets/qwen-xykkX0_y.png +0 -0
  31. package/dashboard/dist/assets/react-vendor-C7Sl8SE7.js +9 -0
  32. package/dashboard/dist/assets/router-DHISdpPk.js +3 -0
  33. package/dashboard/dist/assets/shared-BIP_4k4I.js +1 -0
  34. package/dashboard/dist/favicon.svg +28 -0
  35. package/dashboard/dist/index.html +17 -0
  36. package/dist/agent/acp-client.js +261 -0
  37. package/dist/agent/auto-update.js +432 -0
  38. package/dist/agent/await-resume.js +50 -0
  39. package/dist/agent/cli/auth.js +325 -0
  40. package/dist/agent/cli/catalog.js +40 -0
  41. package/dist/agent/cli/detector.js +136 -0
  42. package/dist/agent/cli/index.js +7 -0
  43. package/dist/agent/cli/registry.js +33 -0
  44. package/dist/agent/driver.js +39 -0
  45. package/dist/agent/drivers/claude-tui.js +2297 -0
  46. package/dist/agent/drivers/claude.js +2689 -0
  47. package/dist/agent/drivers/codex.js +2210 -0
  48. package/dist/agent/drivers/gemini.js +1059 -0
  49. package/dist/agent/drivers/hermes.js +795 -0
  50. package/dist/agent/goal.js +274 -0
  51. package/dist/agent/handover.js +130 -0
  52. package/dist/agent/images.js +355 -0
  53. package/dist/agent/index.js +50 -0
  54. package/dist/agent/mcp/bridge.js +791 -0
  55. package/dist/agent/mcp/extensions.js +637 -0
  56. package/dist/agent/mcp/oauth.js +353 -0
  57. package/dist/agent/mcp/registry.js +119 -0
  58. package/dist/agent/mcp/session-server.js +229 -0
  59. package/dist/agent/mcp/tools/ask-user.js +113 -0
  60. package/dist/agent/mcp/tools/await-resume.js +77 -0
  61. package/dist/agent/mcp/tools/goal.js +144 -0
  62. package/dist/agent/mcp/tools/types.js +12 -0
  63. package/dist/agent/mcp/tools/workspace.js +212 -0
  64. package/dist/agent/npm.js +31 -0
  65. package/dist/agent/session.js +1206 -0
  66. package/dist/agent/skill-installer.js +160 -0
  67. package/dist/agent/skills.js +257 -0
  68. package/dist/agent/stream.js +743 -0
  69. package/dist/agent/types.js +13 -0
  70. package/dist/agent/utils.js +687 -0
  71. package/dist/bot/bot.js +2499 -0
  72. package/dist/bot/command-ui.js +633 -0
  73. package/dist/bot/commands.js +513 -0
  74. package/dist/bot/headless-bot.js +36 -0
  75. package/dist/bot/host.js +192 -0
  76. package/dist/bot/human-loop.js +168 -0
  77. package/dist/bot/menu.js +48 -0
  78. package/dist/bot/orchestration.js +79 -0
  79. package/dist/bot/render-shared.js +309 -0
  80. package/dist/bot/session-hub.js +361 -0
  81. package/dist/bot/session-status.js +55 -0
  82. package/dist/bot/streaming.js +309 -0
  83. package/dist/browser-profile.js +579 -0
  84. package/dist/browser-supervisor.js +249 -0
  85. package/dist/catalog/cli-tools.js +421 -0
  86. package/dist/catalog/index.js +21 -0
  87. package/dist/catalog/local-models.js +94 -0
  88. package/dist/catalog/mcp-servers.js +315 -0
  89. package/dist/catalog/skill-repos.js +173 -0
  90. package/dist/channels/base.js +55 -0
  91. package/dist/channels/dingtalk/bot.js +549 -0
  92. package/dist/channels/dingtalk/channel.js +268 -0
  93. package/dist/channels/discord/bot.js +552 -0
  94. package/dist/channels/discord/channel.js +245 -0
  95. package/dist/channels/feishu/bot.js +1275 -0
  96. package/dist/channels/feishu/channel.js +911 -0
  97. package/dist/channels/feishu/markdown.js +91 -0
  98. package/dist/channels/feishu/render.js +619 -0
  99. package/dist/channels/health.js +109 -0
  100. package/dist/channels/slack/bot.js +554 -0
  101. package/dist/channels/slack/channel.js +283 -0
  102. package/dist/channels/states.js +6 -0
  103. package/dist/channels/telegram/bot.js +1310 -0
  104. package/dist/channels/telegram/channel.js +820 -0
  105. package/dist/channels/telegram/directory.js +111 -0
  106. package/dist/channels/telegram/live-preview.js +220 -0
  107. package/dist/channels/telegram/render.js +384 -0
  108. package/dist/channels/wecom/bot.js +558 -0
  109. package/dist/channels/wecom/channel.js +479 -0
  110. package/dist/channels/weixin/api.js +520 -0
  111. package/dist/channels/weixin/bot.js +1000 -0
  112. package/dist/channels/weixin/channel.js +222 -0
  113. package/dist/cli/autostart.js +262 -0
  114. package/dist/cli/channel-supervisor.js +313 -0
  115. package/dist/cli/channels.js +54 -0
  116. package/dist/cli/main.js +726 -0
  117. package/dist/cli/onboarding.js +227 -0
  118. package/dist/cli/run.js +308 -0
  119. package/dist/cli/setup-wizard.js +235 -0
  120. package/dist/core/config/runtime-config.js +201 -0
  121. package/dist/core/config/user-config.js +510 -0
  122. package/dist/core/config/validation.js +521 -0
  123. package/dist/core/constants.js +400 -0
  124. package/dist/core/git.js +145 -0
  125. package/dist/core/legacy-compat.js +60 -0
  126. package/dist/core/logging.js +101 -0
  127. package/dist/core/platform.js +59 -0
  128. package/dist/core/process-control.js +315 -0
  129. package/dist/core/secrets/index.js +42 -0
  130. package/dist/core/secrets/inline-seal.js +60 -0
  131. package/dist/core/secrets/ref.js +33 -0
  132. package/dist/core/secrets/resolver.js +65 -0
  133. package/dist/core/secrets/store.js +63 -0
  134. package/dist/core/utils.js +233 -0
  135. package/dist/core/version.js +15 -0
  136. package/dist/dashboard/platform.js +219 -0
  137. package/dist/dashboard/routes/agents.js +450 -0
  138. package/dist/dashboard/routes/cli.js +174 -0
  139. package/dist/dashboard/routes/config.js +523 -0
  140. package/dist/dashboard/routes/extensions.js +745 -0
  141. package/dist/dashboard/routes/local-models.js +290 -0
  142. package/dist/dashboard/routes/models.js +324 -0
  143. package/dist/dashboard/routes/sessions.js +838 -0
  144. package/dist/dashboard/runtime.js +410 -0
  145. package/dist/dashboard/server.js +237 -0
  146. package/dist/dashboard/session-control.js +347 -0
  147. package/dist/model/catalog.js +104 -0
  148. package/dist/model/index.js +20 -0
  149. package/dist/model/injector.js +272 -0
  150. package/dist/model/provider-models.js +112 -0
  151. package/dist/model/store.js +212 -0
  152. package/dist/model/types.js +13 -0
  153. package/dist/model/validation.js +203 -0
  154. package/package.json +82 -0
@@ -0,0 +1,520 @@
1
+ /**
2
+ * WeChat official account API integration.
3
+ */
4
+ import crypto from 'node:crypto';
5
+ import QRCode from 'qrcode';
6
+ import { VALIDATION_TIMEOUTS, WEIXIN_LIMITS } from '../../core/constants.js';
7
+ export const WEIXIN_DEFAULT_BASE_URL = 'https://ilinkai.weixin.qq.com';
8
+ const WEIXIN_QR_LOGIN_TTL_MS = 5 * 60_000;
9
+ const WEIXIN_DEFAULT_BOT_TYPE = '3';
10
+ const WEIXIN_QR_CONTENT_CHECK_TIMEOUT_MS = 5_000;
11
+ export const WeixinMessageType = {
12
+ NONE: 0,
13
+ USER: 1,
14
+ BOT: 2,
15
+ };
16
+ export const WeixinMessageItemType = {
17
+ NONE: 0,
18
+ TEXT: 1,
19
+ IMAGE: 2,
20
+ VOICE: 3,
21
+ FILE: 4,
22
+ VIDEO: 5,
23
+ };
24
+ export const WeixinMessageState = {
25
+ NEW: 0,
26
+ GENERATING: 1,
27
+ FINISH: 2,
28
+ };
29
+ export const WeixinTypingStatus = {
30
+ TYPING: 1,
31
+ CANCEL: 2,
32
+ };
33
+ const activeWeixinQrLogins = new Map();
34
+ function randomWechatUin() {
35
+ const uint32 = crypto.randomBytes(4).readUInt32BE(0);
36
+ return Buffer.from(String(uint32), 'utf-8').toString('base64');
37
+ }
38
+ function withTimeoutSignal(timeoutMs) {
39
+ const controller = new AbortController();
40
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
41
+ return {
42
+ signal: controller.signal,
43
+ dispose: () => clearTimeout(timer),
44
+ };
45
+ }
46
+ /**
47
+ * Combine a timeout signal with an optional external "stop" signal so that
48
+ * either source can abort the in-flight request. Used by long-poll callers
49
+ * to cut over immediately when the channel is being torn down (config change,
50
+ * channel removal) without waiting out the long-poll timeout.
51
+ */
52
+ function combineAbortSignals(timeoutMs, external) {
53
+ const timeout = withTimeoutSignal(timeoutMs);
54
+ if (!external)
55
+ return timeout;
56
+ if (external.aborted) {
57
+ timeout.dispose();
58
+ return { signal: external, dispose: () => { } };
59
+ }
60
+ const controller = new AbortController();
61
+ const onAbort = () => controller.abort();
62
+ timeout.signal.addEventListener('abort', onAbort, { once: true });
63
+ external.addEventListener('abort', onAbort, { once: true });
64
+ return {
65
+ signal: controller.signal,
66
+ dispose: () => {
67
+ timeout.signal.removeEventListener('abort', onAbort);
68
+ external.removeEventListener('abort', onAbort);
69
+ timeout.dispose();
70
+ },
71
+ };
72
+ }
73
+ function buildWeixinHeaders(body, token) {
74
+ const headers = {
75
+ 'Content-Type': 'application/json',
76
+ 'Content-Length': String(Buffer.byteLength(body, 'utf-8')),
77
+ AuthorizationType: 'ilink_bot_token',
78
+ 'X-WECHAT-UIN': randomWechatUin(),
79
+ };
80
+ if (token?.trim())
81
+ headers.Authorization = `Bearer ${token.trim()}`;
82
+ return headers;
83
+ }
84
+ function describeError(error) {
85
+ if (error instanceof Error)
86
+ return error.message;
87
+ return String(error ?? 'unknown error');
88
+ }
89
+ function isAbortError(error) {
90
+ return error instanceof Error && error.name === 'AbortError';
91
+ }
92
+ function normalizeFetchBaseUrl(baseUrl) {
93
+ const trimmed = String(baseUrl || '').trim();
94
+ return (trimmed || WEIXIN_DEFAULT_BASE_URL).replace(/\/+$/, '');
95
+ }
96
+ function isHttpUrl(value) {
97
+ try {
98
+ const url = new URL(value);
99
+ return url.protocol === 'http:' || url.protocol === 'https:';
100
+ }
101
+ catch {
102
+ return false;
103
+ }
104
+ }
105
+ function isImageDataUrl(value) {
106
+ return /^data:image\/[a-z0-9.+-]+;base64,/i.test(value);
107
+ }
108
+ function normalizeEncodedImageDataUrl(value) {
109
+ const trimmed = String(value || '').trim().replace(/\s+/g, '');
110
+ if (!trimmed)
111
+ return null;
112
+ if (isImageDataUrl(trimmed))
113
+ return trimmed;
114
+ if (trimmed.startsWith('iVBORw0KGgo'))
115
+ return `data:image/png;base64,${trimmed}`;
116
+ if (trimmed.startsWith('/9j/'))
117
+ return `data:image/jpeg;base64,${trimmed}`;
118
+ if (trimmed.startsWith('R0lGOD'))
119
+ return `data:image/gif;base64,${trimmed}`;
120
+ if (trimmed.startsWith('UklGR'))
121
+ return `data:image/webp;base64,${trimmed}`;
122
+ if (trimmed.startsWith('PHN2Zy') || trimmed.startsWith('PD94bWwg'))
123
+ return `data:image/svg+xml;base64,${trimmed}`;
124
+ return null;
125
+ }
126
+ async function probeRemoteContentType(url, timeoutMs) {
127
+ const headTimeout = withTimeoutSignal(timeoutMs);
128
+ try {
129
+ const response = await fetch(url, {
130
+ method: 'HEAD',
131
+ signal: headTimeout.signal,
132
+ });
133
+ if (response.ok) {
134
+ const contentType = String(response.headers.get('content-type') || '').trim();
135
+ if (contentType)
136
+ return contentType;
137
+ }
138
+ }
139
+ catch { }
140
+ finally {
141
+ headTimeout.dispose();
142
+ }
143
+ const getTimeout = withTimeoutSignal(timeoutMs);
144
+ try {
145
+ const response = await fetch(url, {
146
+ method: 'GET',
147
+ headers: { Range: 'bytes=0-0' },
148
+ signal: getTimeout.signal,
149
+ });
150
+ const contentType = String(response.headers.get('content-type') || '').trim();
151
+ void response.body?.cancel().catch(() => { });
152
+ return contentType;
153
+ }
154
+ catch {
155
+ return '';
156
+ }
157
+ finally {
158
+ getTimeout.dispose();
159
+ }
160
+ }
161
+ async function buildQrDataUrl(content) {
162
+ return QRCode.toDataURL(content, {
163
+ errorCorrectionLevel: 'M',
164
+ margin: 1,
165
+ width: 512,
166
+ });
167
+ }
168
+ export function normalizeWeixinBaseUrl(baseUrl) {
169
+ return normalizeFetchBaseUrl(baseUrl);
170
+ }
171
+ export async function resolveWeixinQrDisplayUrl(qrcodeImgContent) {
172
+ const trimmed = String(qrcodeImgContent || '').trim();
173
+ if (!trimmed)
174
+ throw new Error('Missing QR code image content.');
175
+ const embeddedImage = normalizeEncodedImageDataUrl(trimmed);
176
+ if (embeddedImage)
177
+ return embeddedImage;
178
+ if (isHttpUrl(trimmed)) {
179
+ const contentType = await probeRemoteContentType(trimmed, WEIXIN_QR_CONTENT_CHECK_TIMEOUT_MS);
180
+ if (/^image\//i.test(contentType))
181
+ return trimmed;
182
+ return buildQrDataUrl(trimmed);
183
+ }
184
+ return buildQrDataUrl(trimmed);
185
+ }
186
+ async function parseJsonResponse(response, label) {
187
+ const raw = await response.text();
188
+ if (!response.ok) {
189
+ throw new Error(`${label} failed: HTTP ${response.status} ${response.statusText || ''}`.trim());
190
+ }
191
+ try {
192
+ return JSON.parse(raw);
193
+ }
194
+ catch (error) {
195
+ throw new Error(`${label} returned invalid JSON: ${describeError(error)}`);
196
+ }
197
+ }
198
+ async function weixinPostJson(params) {
199
+ const url = new URL(params.endpoint, `${normalizeFetchBaseUrl(params.baseUrl)}/`);
200
+ const body = JSON.stringify(params.body);
201
+ const combined = combineAbortSignals(params.timeoutMs, params.signal);
202
+ try {
203
+ const response = await fetch(url.toString(), {
204
+ method: 'POST',
205
+ headers: buildWeixinHeaders(body, params.token),
206
+ body,
207
+ signal: combined.signal,
208
+ });
209
+ return await parseJsonResponse(response, params.label);
210
+ }
211
+ finally {
212
+ combined.dispose();
213
+ }
214
+ }
215
+ async function weixinGetJson(params) {
216
+ const timeout = withTimeoutSignal(params.timeoutMs);
217
+ try {
218
+ const response = await fetch(params.url, {
219
+ method: 'GET',
220
+ headers: params.headers,
221
+ signal: timeout.signal,
222
+ });
223
+ return await parseJsonResponse(response, params.label);
224
+ }
225
+ finally {
226
+ timeout.dispose();
227
+ }
228
+ }
229
+ function purgeExpiredWeixinQrLogins() {
230
+ const now = Date.now();
231
+ for (const [sessionKey, login] of activeWeixinQrLogins) {
232
+ if (now - login.startedAt >= WEIXIN_QR_LOGIN_TTL_MS)
233
+ activeWeixinQrLogins.delete(sessionKey);
234
+ }
235
+ }
236
+ function buildSendMessageBody(toUserId, text, contextToken) {
237
+ return {
238
+ msg: {
239
+ from_user_id: '',
240
+ to_user_id: toUserId,
241
+ client_id: crypto.randomUUID(),
242
+ message_type: WeixinMessageType.BOT,
243
+ message_state: WeixinMessageState.FINISH,
244
+ item_list: text
245
+ ? [{ type: WeixinMessageItemType.TEXT, text_item: { text } }]
246
+ : undefined,
247
+ context_token: contextToken || undefined,
248
+ },
249
+ base_info: {},
250
+ };
251
+ }
252
+ export async function weixinGetUpdates(params) {
253
+ const timeoutMs = params.timeoutMs ?? WEIXIN_LIMITS.longPollTimeout;
254
+ try {
255
+ return await weixinPostJson({
256
+ baseUrl: params.baseUrl,
257
+ endpoint: 'ilink/bot/getupdates',
258
+ body: {
259
+ get_updates_buf: params.getUpdatesBuf ?? '',
260
+ base_info: {},
261
+ },
262
+ token: params.token,
263
+ timeoutMs,
264
+ label: 'weixin getupdates',
265
+ signal: params.signal,
266
+ });
267
+ }
268
+ catch (error) {
269
+ if (isAbortError(error)) {
270
+ return {
271
+ ret: 0,
272
+ msgs: [],
273
+ get_updates_buf: params.getUpdatesBuf ?? '',
274
+ };
275
+ }
276
+ throw error;
277
+ }
278
+ }
279
+ export async function weixinSendTextMessage(params) {
280
+ await weixinPostJson({
281
+ baseUrl: params.baseUrl,
282
+ endpoint: 'ilink/bot/sendmessage',
283
+ body: buildSendMessageBody(params.toUserId, params.text, params.contextToken),
284
+ token: params.token,
285
+ timeoutMs: params.timeoutMs ?? 15_000,
286
+ label: 'weixin sendmessage',
287
+ });
288
+ }
289
+ export async function weixinGetConfig(params) {
290
+ return weixinPostJson({
291
+ baseUrl: params.baseUrl,
292
+ endpoint: 'ilink/bot/getconfig',
293
+ body: {
294
+ ilink_user_id: params.userId,
295
+ context_token: params.contextToken || undefined,
296
+ base_info: {},
297
+ },
298
+ token: params.token,
299
+ timeoutMs: params.timeoutMs ?? 10_000,
300
+ label: 'weixin getconfig',
301
+ });
302
+ }
303
+ export async function weixinSendTyping(params) {
304
+ await weixinPostJson({
305
+ baseUrl: params.baseUrl,
306
+ endpoint: 'ilink/bot/sendtyping',
307
+ body: {
308
+ ilink_user_id: params.userId,
309
+ typing_ticket: params.typingTicket,
310
+ status: params.status ?? WeixinTypingStatus.TYPING,
311
+ base_info: {},
312
+ },
313
+ token: params.token,
314
+ timeoutMs: params.timeoutMs ?? 10_000,
315
+ label: 'weixin sendtyping',
316
+ });
317
+ }
318
+ export function extractWeixinTextBody(message) {
319
+ const items = message.item_list || [];
320
+ for (const item of items) {
321
+ if (item.type === WeixinMessageItemType.TEXT && item.text_item?.text) {
322
+ return String(item.text_item.text);
323
+ }
324
+ if (item.type === WeixinMessageItemType.VOICE && item.voice_item?.text) {
325
+ return String(item.voice_item.text);
326
+ }
327
+ }
328
+ return '';
329
+ }
330
+ export function markdownToWeixinPlainText(text) {
331
+ let result = String(text || '');
332
+ result = result.replace(/```[^\n]*\n?([\s\S]*?)```/g, (_, code) => code.trim());
333
+ result = result.replace(/!\[[^\]]*\]\([^)]*\)/g, '');
334
+ result = result.replace(/\[([^\]]+)\]\([^)]*\)/g, '$1');
335
+ result = result.replace(/^\|[\s:|-]+\|$/gm, '');
336
+ result = result.replace(/^\|(.+)\|$/gm, (_, inner) => inner.split('|').map(cell => cell.trim()).join(' '));
337
+ result = result.replace(/[*_~`>#-]/g, match => (match === '-' ? '-' : ''));
338
+ return result.replace(/\r\n?/g, '\n').trim();
339
+ }
340
+ function buildQrUrl(baseUrl, botType) {
341
+ return new URL(`ilink/bot/get_bot_qrcode?bot_type=${encodeURIComponent(botType)}`, `${normalizeFetchBaseUrl(baseUrl)}/`).toString();
342
+ }
343
+ function buildQrStatusUrl(baseUrl, qrcode) {
344
+ return new URL(`ilink/bot/get_qrcode_status?qrcode=${encodeURIComponent(qrcode)}`, `${normalizeFetchBaseUrl(baseUrl)}/`).toString();
345
+ }
346
+ async function fetchWeixinQrCode(baseUrl, botType) {
347
+ return weixinGetJson({
348
+ url: buildQrUrl(baseUrl, botType),
349
+ timeoutMs: VALIDATION_TIMEOUTS.weixinDefault,
350
+ label: 'weixin get_bot_qrcode',
351
+ });
352
+ }
353
+ async function fetchWeixinQrStatus(baseUrl, qrcode, timeoutMs) {
354
+ try {
355
+ return await weixinGetJson({
356
+ url: buildQrStatusUrl(baseUrl, qrcode),
357
+ timeoutMs,
358
+ label: 'weixin get_qrcode_status',
359
+ headers: {
360
+ 'iLink-App-ClientVersion': '1',
361
+ },
362
+ });
363
+ }
364
+ catch (error) {
365
+ if (isAbortError(error))
366
+ return { status: 'wait' };
367
+ throw error;
368
+ }
369
+ }
370
+ export async function startWeixinQrLogin(params = {}) {
371
+ purgeExpiredWeixinQrLogins();
372
+ const sessionKey = String(params.sessionKey || crypto.randomUUID());
373
+ const baseUrl = normalizeFetchBaseUrl(params.baseUrl);
374
+ const existing = activeWeixinQrLogins.get(sessionKey);
375
+ if (existing) {
376
+ return {
377
+ ok: true,
378
+ sessionKey,
379
+ qrcodeUrl: existing.qrcodeUrl,
380
+ message: '二维码已生成,请使用微信扫描。',
381
+ };
382
+ }
383
+ try {
384
+ const qr = await fetchWeixinQrCode(baseUrl, params.botType || WEIXIN_DEFAULT_BOT_TYPE);
385
+ if (!qr.qrcode || !qr.qrcode_img_content) {
386
+ return {
387
+ ok: false,
388
+ sessionKey,
389
+ message: '微信未返回二维码数据。',
390
+ error: 'Missing QR code payload.',
391
+ };
392
+ }
393
+ const qrcodeUrl = await resolveWeixinQrDisplayUrl(qr.qrcode_img_content);
394
+ activeWeixinQrLogins.set(sessionKey, {
395
+ sessionKey,
396
+ qrcode: qr.qrcode,
397
+ qrcodeUrl,
398
+ startedAt: Date.now(),
399
+ });
400
+ return {
401
+ ok: true,
402
+ sessionKey,
403
+ qrcodeUrl,
404
+ message: '二维码已生成,请使用微信扫描。',
405
+ };
406
+ }
407
+ catch (error) {
408
+ return {
409
+ ok: false,
410
+ sessionKey,
411
+ message: '生成微信二维码失败。',
412
+ error: describeError(error),
413
+ };
414
+ }
415
+ }
416
+ export async function waitForWeixinQrLogin(params) {
417
+ purgeExpiredWeixinQrLogins();
418
+ const baseUrl = normalizeFetchBaseUrl(params.baseUrl);
419
+ const login = activeWeixinQrLogins.get(params.sessionKey);
420
+ if (!login) {
421
+ return {
422
+ ok: false,
423
+ connected: false,
424
+ status: 'error',
425
+ message: '当前没有进行中的微信扫码会话,请重新生成二维码。',
426
+ error: 'QR session not found.',
427
+ };
428
+ }
429
+ if (Date.now() - login.startedAt >= WEIXIN_QR_LOGIN_TTL_MS) {
430
+ activeWeixinQrLogins.delete(params.sessionKey);
431
+ return {
432
+ ok: false,
433
+ connected: false,
434
+ status: 'expired',
435
+ message: '二维码已过期,请重新生成。',
436
+ qrcodeUrl: login.qrcodeUrl,
437
+ error: 'QR session expired.',
438
+ };
439
+ }
440
+ try {
441
+ const status = await fetchWeixinQrStatus(baseUrl, login.qrcode, Math.max(1_000, params.timeoutMs ?? VALIDATION_TIMEOUTS.weixinQrPoll));
442
+ switch (status.status) {
443
+ case 'confirmed':
444
+ activeWeixinQrLogins.delete(params.sessionKey);
445
+ if (!status.bot_token || !status.ilink_bot_id) {
446
+ return {
447
+ ok: false,
448
+ connected: false,
449
+ status: 'error',
450
+ message: '微信登录已确认,但返回数据不完整。',
451
+ error: 'Missing bot token or account id.',
452
+ };
453
+ }
454
+ return {
455
+ ok: true,
456
+ connected: true,
457
+ status: 'confirmed',
458
+ message: '微信连接成功。',
459
+ botToken: status.bot_token,
460
+ accountId: status.ilink_bot_id,
461
+ userId: status.ilink_user_id,
462
+ baseUrl: normalizeFetchBaseUrl(status.baseurl || baseUrl),
463
+ };
464
+ case 'expired': {
465
+ const refreshed = await fetchWeixinQrCode(baseUrl, params.botType || WEIXIN_DEFAULT_BOT_TYPE);
466
+ if (refreshed.qrcode && refreshed.qrcode_img_content) {
467
+ const qrcodeUrl = await resolveWeixinQrDisplayUrl(refreshed.qrcode_img_content);
468
+ activeWeixinQrLogins.set(params.sessionKey, {
469
+ sessionKey: params.sessionKey,
470
+ qrcode: refreshed.qrcode,
471
+ qrcodeUrl,
472
+ startedAt: Date.now(),
473
+ });
474
+ return {
475
+ ok: true,
476
+ connected: false,
477
+ status: 'expired',
478
+ message: '二维码已刷新,请重新扫码。',
479
+ qrcodeUrl,
480
+ };
481
+ }
482
+ return {
483
+ ok: false,
484
+ connected: false,
485
+ status: 'expired',
486
+ message: '二维码已过期,请重新生成。',
487
+ qrcodeUrl: login.qrcodeUrl,
488
+ error: 'Failed to refresh QR code.',
489
+ };
490
+ }
491
+ case 'scaned':
492
+ return {
493
+ ok: true,
494
+ connected: false,
495
+ status: 'scaned',
496
+ message: '已扫码,请在微信里确认。',
497
+ qrcodeUrl: login.qrcodeUrl,
498
+ };
499
+ case 'wait':
500
+ default:
501
+ return {
502
+ ok: true,
503
+ connected: false,
504
+ status: 'wait',
505
+ message: '等待扫码中。',
506
+ qrcodeUrl: login.qrcodeUrl,
507
+ };
508
+ }
509
+ }
510
+ catch (error) {
511
+ activeWeixinQrLogins.delete(params.sessionKey);
512
+ return {
513
+ ok: false,
514
+ connected: false,
515
+ status: 'error',
516
+ message: '微信扫码登录失败。',
517
+ error: describeError(error),
518
+ };
519
+ }
520
+ }