@kafca/agentdock 0.1.22 → 0.1.23

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 (72) hide show
  1. package/README.md +3 -0
  2. package/dist/renderer/assets/{Badge-B_rgzxYO.js → Badge-C2K5apfV.js} +1 -1
  3. package/dist/renderer/assets/{Chat-CYtp6m7T.js → Chat-DI2YeVKb.js} +1 -1
  4. package/dist/renderer/assets/{ChatMarkdown-DOBMwa9d.js → ChatMarkdown-DMAr15Oi.js} +1 -1
  5. package/dist/renderer/assets/{Config-DppMSPHF.js → Config-7rP3EuUf.js} +1 -1
  6. package/dist/renderer/assets/{CronList-BDtB1SuA.js → CronList-81QslYE8.js} +1 -1
  7. package/dist/renderer/assets/{Dashboard-iFlRnd7Y.js → Dashboard-Db2T7rSZ.js} +2 -2
  8. package/dist/renderer/assets/{EmptyState-bKDt_T88.js → EmptyState-BBJ3cTvB.js} +1 -1
  9. package/dist/renderer/assets/{KnowledgeDetail-CKPhKPIg.js → KnowledgeDetail-Dz_6flG3.js} +2 -2
  10. package/dist/renderer/assets/{KnowledgeHome-CFeJre6i.js → KnowledgeHome-DIh046mN.js} +3 -3
  11. package/dist/renderer/assets/{Logs-CyxSVOFK.js → Logs-BXBtHJLf.js} +1 -1
  12. package/dist/renderer/assets/{Modal-CxPnk1Ey.js → Modal-BB2Av_Xs.js} +1 -1
  13. package/dist/renderer/assets/{Page-kU9vubzj.js → Page-DPSdvMgU.js} +1 -1
  14. package/dist/renderer/assets/{ProjectDetail-D9FY9NWZ.js → ProjectDetail-C6EWGvGg.js} +1 -1
  15. package/dist/renderer/assets/{ProjectList-DIwr2TXg.js → ProjectList-BBo5ygnS.js} +1 -1
  16. package/dist/renderer/assets/{Select-BaWoz5Dn.js → Select-CNX-Tx_O.js} +1 -1
  17. package/dist/renderer/assets/{SessionChat-Cm9CW8Zx.js → SessionChat-BIn46jqZ.js} +1 -1
  18. package/dist/renderer/assets/{SessionList-UPnWlXXO.js → SessionList-CZt6f1Rd.js} +1 -1
  19. package/dist/renderer/assets/{ThreadChat-YvVsACW3.js → ThreadChat-BotD0gKQ.js} +1 -1
  20. package/dist/renderer/assets/Workspace-C_4tdpjX.js +14 -0
  21. package/dist/renderer/assets/{arrow-left-BmZAVGV7.js → arrow-left-cHr5wkpq.js} +1 -1
  22. package/dist/renderer/assets/{book-open-DZ3GXFgI.js → book-open-4nx5C8T-.js} +1 -1
  23. package/dist/renderer/assets/{check-CeVSvDCv.js → check-exTS-R-x.js} +1 -1
  24. package/dist/renderer/assets/{chevron-down-yryuMoQ-.js → chevron-down-DSvxXQa0.js} +1 -1
  25. package/dist/renderer/assets/{circle-eASsFISH.js → circle-DyvEovdw.js} +1 -1
  26. package/dist/renderer/assets/desktop-B-KVT10D.js +1 -0
  27. package/dist/renderer/assets/{desktop-Che1cbpE.js → desktop-CVibRyAw.js} +3 -3
  28. package/dist/renderer/assets/{funnel-pwAe2Ixp.js → funnel-DA9qVdTy.js} +1 -1
  29. package/dist/renderer/assets/{heart-B2Sp-bCm.js → heart-DuEV0ZRJ.js} +1 -1
  30. package/dist/renderer/assets/{index-Cse2Br64.js → index-BgflqkCN.js} +3 -3
  31. package/dist/renderer/assets/{index-BC3MWbS1.js → index-Biz-aexG.js} +39 -39
  32. package/dist/renderer/assets/{pencil-BfEunUF3.js → pencil-DBDxeNmO.js} +1 -1
  33. package/dist/renderer/assets/{play-Dcs9Lj_Z.js → play-IzlP83_7.js} +1 -1
  34. package/dist/renderer/assets/{plug-B_YschQe.js → plug-CuT-qFnW.js} +1 -1
  35. package/dist/renderer/assets/{plus-Pqo-cXHQ.js → plus-DEqgkn63.js} +1 -1
  36. package/dist/renderer/assets/projects-DDtDN1rc.js +1 -0
  37. package/dist/renderer/assets/{save-BKM5yi1A.js → save-D4sRo07y.js} +1 -1
  38. package/dist/renderer/assets/{search-BU42h10M.js → search-DbPZLuV8.js} +1 -1
  39. package/dist/renderer/assets/{server-DA5t3Rtr.js → server-D1jUQmwN.js} +1 -1
  40. package/dist/renderer/assets/{session-utils-Cr9VhNSq.js → session-utils-Aife8ep2.js} +1 -1
  41. package/dist/renderer/assets/{sessions-bvV8ZMfX.js → sessions-DaH3H7gC.js} +1 -1
  42. package/dist/renderer/assets/{shield-check-BeOJKur_.js → shield-check-BGwUFWNr.js} +1 -1
  43. package/dist/renderer/assets/{trash-2-CQ7HKO3q.js → trash-2-CIFBioBa.js} +1 -1
  44. package/dist/renderer/assets/{user-CtFjY9SV.js → user-CnKcvHdo.js} +1 -1
  45. package/dist/renderer/assets/{wifi-off-Ci5m0ysf.js → wifi-off-ClO53WGa.js} +1 -1
  46. package/dist/renderer/index.html +1 -1
  47. package/dist-electron/electron/local-core-refactor.test.js +505 -0
  48. package/dist-electron/electron/plugin-kernel.test.js +74 -14
  49. package/dist-electron/packages/core-sdk/src/index.js +40 -20
  50. package/dist-electron/services/local-ai-core/src/acp/local-core-acp-turn-coordinator.js +5 -1
  51. package/dist-electron/services/local-ai-core/src/acp/workspace-acp-permissions.js +55 -3
  52. package/dist-electron/services/local-ai-core/src/channel/lark/cards.js +210 -0
  53. package/dist-electron/services/local-ai-core/src/channel/lark/config.js +54 -0
  54. package/dist-electron/services/local-ai-core/src/channel/lark/local-core-lark-gateway.js +212 -471
  55. package/dist-electron/services/local-ai-core/src/channel/lark/registration.js +50 -0
  56. package/dist-electron/services/local-ai-core/src/channel/lark/runtime-state.js +191 -0
  57. package/dist-electron/services/local-ai-core/src/channel/lark/types.js +2 -0
  58. package/dist-electron/services/local-ai-core/src/channel/shared/plugin.js +31 -0
  59. package/dist-electron/services/local-ai-core/src/channel/weixin/config.js +125 -0
  60. package/dist-electron/services/local-ai-core/src/channel/weixin/local-core-weixin-gateway.js +141 -324
  61. package/dist-electron/services/local-ai-core/src/channel/weixin/transport.js +169 -0
  62. package/dist-electron/services/local-ai-core/src/channel/weixin/types.js +2 -0
  63. package/dist-electron/services/local-ai-core/src/kernel/bootstrap.js +5 -0
  64. package/dist-electron/services/local-ai-core/src/plugins/builtin/channel-lark-plugin.js +18 -39
  65. package/dist-electron/services/local-ai-core/src/plugins/builtin/channel-weixin-plugin.js +18 -39
  66. package/dist-electron/services/local-ai-core/src/runtime/local-core-controller.js +55 -44
  67. package/dist-electron/services/local-ai-core/src/runtime/server.js +10 -8
  68. package/dist-electron/shared/desktop.js +10 -0
  69. package/package.json +1 -1
  70. package/dist/renderer/assets/Workspace-ST7V8u6T.js +0 -14
  71. package/dist/renderer/assets/desktop-DkrP_s-b.js +0 -1
  72. package/dist/renderer/assets/projects-BCj0k_Gy.js +0 -1
@@ -11,12 +11,10 @@ const node_events_1 = require("node:events");
11
11
  const node_crypto_2 = require("node:crypto");
12
12
  const desktop_js_1 = require("../../../../../shared/desktop.js");
13
13
  const file_utils_js_1 = require("../shared/file-utils.js");
14
+ const config_js_1 = require("./config.js");
15
+ const transport_js_1 = require("./transport.js");
14
16
  // ==================== Constants ====================
15
17
  const PAIRING_EXPIRY_MS = 10 * 60 * 1000;
16
- const DEFAULT_BASE_URL = 'https://ilinkai.weixin.qq.com';
17
- const DEFAULT_CDN_BASE_URL = 'https://novac2c.cdn.weixin.qq.com/c2c';
18
- const LONG_POLL_TIMEOUT_MS = 35_000;
19
- const API_TIMEOUT_MS = 15_000;
20
18
  const RETRY_DELAY_MS = 2_000;
21
19
  const BACKOFF_DELAY_MS = 30_000;
22
20
  const MAX_CONSECUTIVE_FAILURES = 3;
@@ -26,15 +24,7 @@ const WEIXIN_CONTEXT_REPLY_MAX_BYTES = 3500;
26
24
  const WEIXIN_CONTEXT_SEND_LIMIT = 10;
27
25
  const WEIXIN_RESERVED_TERMINAL_SENDS = 1;
28
26
  const WEIXIN_PROGRESS_SEND_BUDGET = WEIXIN_CONTEXT_SEND_LIMIT - WEIXIN_RESERVED_TERMINAL_SENDS;
29
- const WEIXIN_CHANNEL_VERSION = '2.1.7';
30
- const WEIXIN_ILINK_APP_ID = 'bot';
31
- const WEIXIN_ILINK_APP_CLIENT_VERSION = '131335';
32
27
  const RESPONSE_TIMEOUT_MS = 5 * 60 * 1000;
33
- const TEXT_ITEM_TYPE = 1;
34
- const IMAGE_ITEM_TYPE = 2;
35
- const VOICE_ITEM_TYPE = 3;
36
- const FILE_ITEM_TYPE = 4;
37
- const UPLOAD_MEDIA_TYPE_FILE = 3;
38
28
  const WEIXIN_MAX_UPLOAD_FILE_SIZE = 30 * 1024 * 1024;
39
29
  // ==================== Utilities ====================
40
30
  function stripHtml(html) {
@@ -60,18 +50,6 @@ function sleep(ms, signal) {
60
50
  signal?.addEventListener('abort', onAbort, { once: true });
61
51
  });
62
52
  }
63
- function safeFilePart(value) {
64
- return String(value || 'default').replace(/[^a-zA-Z0-9._-]/g, '_').slice(0, 80) || 'default';
65
- }
66
- function createWechatUin() {
67
- return Buffer.from(String(node_crypto_1.default.randomInt(0, 0xffffffff))).toString('base64');
68
- }
69
- function createIlinkHeaders() {
70
- return {
71
- 'iLink-App-Id': WEIXIN_ILINK_APP_ID,
72
- 'iLink-App-ClientVersion': WEIXIN_ILINK_APP_CLIENT_VERSION,
73
- };
74
- }
75
53
  function utf8ByteLength(value) {
76
54
  return Buffer.byteLength(value, 'utf-8');
77
55
  }
@@ -163,21 +141,25 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
163
141
  async refreshBindings() {
164
142
  const config = await this.options.readConfig();
165
143
  const bindings = this.collectBindings(config);
166
- const nextWorkspaceIds = new Set(bindings.map((b) => b.workspaceId));
167
- for (const workspaceId of [...this.runtime.keys()]) {
168
- if (!nextWorkspaceIds.has(workspaceId)) {
169
- await this.stopWorkspace(workspaceId);
144
+ const nextKeys = new Set(bindings.map((b) => (0, config_js_1.runtimeKey)(b.workspaceId, b.instanceId)));
145
+ for (const key of [...this.runtime.keys()]) {
146
+ if (!nextKeys.has(key)) {
147
+ await this.stopWorkspaceKey(key);
170
148
  }
171
149
  }
172
150
  for (const binding of bindings) {
173
- const current = this.runtime.get(binding.workspaceId);
151
+ const key = (0, config_js_1.runtimeKey)(binding.workspaceId, binding.instanceId);
152
+ const current = this.runtime.get(key);
174
153
  if (!binding.enabled) {
175
154
  if (current) {
176
- await this.stopWorkspace(binding.workspaceId);
155
+ await this.stopWorkspaceKey(key);
177
156
  }
178
157
  else {
179
- this.runtime.set(binding.workspaceId, {
158
+ this.runtime.set(key, {
180
159
  workspaceId: binding.workspaceId,
160
+ instanceId: binding.instanceId,
161
+ displayName: binding.displayName,
162
+ platformKey: binding.platformKey,
181
163
  enabled: false,
182
164
  status: 'disabled',
183
165
  connected: false,
@@ -193,35 +175,39 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
193
175
  }
194
176
  this.notifyRuntimeStateChanged();
195
177
  }
196
- async testConnection(workspaceId) {
197
- const binding = await this.getBinding(workspaceId);
178
+ async testConnection(workspaceId, instanceId) {
179
+ const binding = await this.getBinding(workspaceId, instanceId);
198
180
  try {
199
- const bufPath = this.getBufPath(binding);
181
+ const bufPath = (0, config_js_1.getWeixinBufPath)(binding);
200
182
  node_fs_1.default.accessSync(bufPath);
201
- return { success: true, platform: 'weixin', workspaceId, appId: binding.accountId };
183
+ return { success: true, platform: 'weixin', workspaceId, instanceId: binding.instanceId, appId: binding.accountId };
202
184
  }
203
185
  catch {
204
- return { success: false, platform: 'weixin', workspaceId, error: 'No sync buf found for account. Ensure the plugin has been connected at least once.' };
186
+ return { success: false, platform: 'weixin', workspaceId, instanceId: binding.instanceId, error: 'No sync buf found for account. Ensure the plugin has been connected at least once.' };
205
187
  }
206
188
  }
207
- async enable(workspaceId) {
208
- const binding = await this.getBinding(workspaceId);
189
+ async enable(workspaceId, instanceId) {
190
+ const binding = await this.getBinding(workspaceId, instanceId);
209
191
  await this.startWorkspace(binding);
210
- return this.getStatus(workspaceId);
192
+ return this.getStatus(workspaceId, binding.instanceId);
211
193
  }
212
- async disable(workspaceId) {
213
- await this.stopWorkspace(workspaceId);
214
- return this.getStatus(workspaceId);
194
+ async disable(workspaceId, instanceId) {
195
+ await this.stopWorkspace(workspaceId, instanceId);
196
+ return this.getStatus(workspaceId, instanceId);
215
197
  }
216
- getStatus(workspaceId) {
198
+ getStatus(workspaceId, instanceId) {
217
199
  this.options.store.expirePendingPairings();
218
- const binding = this.runtime.get(workspaceId);
200
+ const resolved = this.resolveRuntimeState(workspaceId, instanceId);
201
+ const binding = resolved.state;
202
+ const platformKey = binding?.platformKey || (0, config_js_1.channelPlatformKey)('weixin', resolved.instanceId);
219
203
  const pairings = this.options.store.listPendingPairings(workspaceId)
220
- .filter((row) => row.platform === 'weixin' && row.expires_at >= new Date().toISOString());
221
- const users = this.options.store.listAuthorizedUsers(workspaceId, 'weixin');
204
+ .filter((row) => row.platform === platformKey && row.expires_at >= new Date().toISOString());
205
+ const users = this.options.store.listAuthorizedUsers(workspaceId, platformKey);
222
206
  return {
223
207
  workspaceId,
224
208
  platform: 'weixin',
209
+ instanceId: resolved.instanceId,
210
+ displayName: binding?.displayName,
225
211
  enabled: Boolean(binding?.enabled),
226
212
  connected: Boolean(binding?.connected),
227
213
  status: binding?.status || 'disabled',
@@ -233,16 +219,20 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
233
219
  };
234
220
  }
235
221
  listStatuses() {
236
- return [...this.runtime.keys()].sort().map((workspaceId) => this.getStatus(workspaceId));
222
+ return [...this.runtime.values()]
223
+ .sort((a, b) => `${a.workspaceId}:${a.instanceId}`.localeCompare(`${b.workspaceId}:${b.instanceId}`))
224
+ .map((state) => this.getStatus(state.workspaceId, state.instanceId));
237
225
  }
238
226
  listPendingPairings(workspaceId) {
239
227
  this.options.store.expirePendingPairings();
240
228
  return this.options.store
241
- .listPairingRequests(workspaceId, 'weixin')
229
+ .listPairingRequests(workspaceId)
230
+ .filter((item) => item.platform === 'weixin' || item.platform.startsWith('weixin:'))
242
231
  .filter((item) => item.status === 'pending' && item.expiresAt >= new Date().toISOString());
243
232
  }
244
233
  listAuthorizedUsers(workspaceId) {
245
- return this.options.store.listAuthorizedUsers(workspaceId, 'weixin');
234
+ return this.options.store.listAuthorizedUsers(workspaceId)
235
+ .filter((item) => item.platform === 'weixin' || item.platform.startsWith('weixin:'));
246
236
  }
247
237
  async start() {
248
238
  await this.refreshBindings();
@@ -251,12 +241,12 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
251
241
  this.close();
252
242
  }
253
243
  close() {
254
- return Promise.all([...this.runtime.keys()].map((id) => this.stopWorkspace(id))).then(() => undefined);
244
+ return Promise.all([...this.runtime.keys()].map((id) => this.stopWorkspaceKey(id))).then(() => undefined);
255
245
  }
256
246
  // ==================== QR Code Login ====================
257
- async getQrCode(workspaceId) {
258
- const binding = await this.getBinding(workspaceId);
259
- const data = await this.apiGet(binding, `ilink/bot/get_bot_qrcode?bot_type=3`);
247
+ async getQrCode(workspaceId, instanceId) {
248
+ const binding = await this.getBinding(workspaceId, instanceId);
249
+ const data = await (0, transport_js_1.weixinApiGet)(binding, `ilink/bot/get_bot_qrcode?bot_type=3`);
260
250
  if (data.errcode && data.errcode !== 0) {
261
251
  throw new Error(`Failed to get QR code: ${data.errmsg || data.errcode}`);
262
252
  }
@@ -264,11 +254,11 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
264
254
  throw new Error('No QR code returned from iLink');
265
255
  }
266
256
  const qrCodeUrl = data.qrcode_img_content || `${binding.baseUrl.replace(/\/$/, '')}/ilink/bot/qr_code/${data.qrcode}`;
267
- return { ticket: data.qrcode, expiresIn: data.expired || 180, qrCodeUrl };
257
+ return { ticket: data.qrcode, expiresIn: data.expired || 180, qrCodeUrl, instanceId: binding.instanceId, displayName: binding.displayName };
268
258
  }
269
- async checkQrCodeStatus(workspaceId, ticket) {
270
- const binding = await this.getBinding(workspaceId);
271
- const data = await this.apiGet(binding, `ilink/bot/get_qrcode_status?qrcode=${encodeURIComponent(ticket)}`);
259
+ async checkQrCodeStatus(workspaceId, ticket, instanceId) {
260
+ const binding = await this.getBinding(workspaceId, instanceId);
261
+ const data = await (0, transport_js_1.weixinApiGet)(binding, `ilink/bot/get_qrcode_status?qrcode=${encodeURIComponent(ticket)}`);
272
262
  if (data.errcode && data.errcode !== 0) {
273
263
  throw new Error(`Failed to check QR code status: ${data.errmsg || data.errcode}`);
274
264
  }
@@ -277,14 +267,14 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
277
267
  if (!data.bot_token) {
278
268
  throw new Error('WeChat QR code confirmed but no bot token was returned.');
279
269
  }
280
- this.saveCredentials(binding, {
270
+ (0, config_js_1.saveWeixinCredentials)(binding, {
281
271
  token: data.bot_token,
282
272
  baseUrl: data.baseurl || binding.baseUrl,
283
273
  botId: data.ilink_bot_id,
284
274
  userId: data.ilink_user_id || data.user_id,
285
275
  savedAt: new Date().toISOString(),
286
276
  });
287
- await this.startWorkspace(await this.getBinding(workspaceId));
277
+ await this.startWorkspace(await this.getBinding(workspaceId, binding.instanceId));
288
278
  }
289
279
  if (!['wait', 'signed', 'confirmed', 'expired'].includes(status)) {
290
280
  return { status: 'wait' };
@@ -301,7 +291,7 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
301
291
  const pairing = this.options.store.getPairingRequest(code);
302
292
  if (!pairing)
303
293
  throw new Error(`Pairing code not found: ${code}`);
304
- if (pairing.platform !== 'weixin')
294
+ if (pairing.platform !== 'weixin' && !pairing.platform.startsWith('weixin:'))
305
295
  throw new Error(`Pairing code ${code} is not a WeChat pairing`);
306
296
  if (pairing.status !== 'pending')
307
297
  throw new Error(`Pairing code ${code} is already ${pairing.status}`);
@@ -309,13 +299,13 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
309
299
  this.options.store.updatePairingStatus(code, 'expired');
310
300
  throw new Error(`Pairing code ${code} has expired`);
311
301
  }
312
- const existing = this.options.store.getAuthorizedUser(pairing.workspace_id, pairing.platform_user_id, 'weixin');
302
+ const existing = this.options.store.getAuthorizedUser(pairing.workspace_id, pairing.platform_user_id, pairing.platform);
313
303
  const userId = existing?.id || `wx-user-${(0, node_crypto_2.randomUUID)()}`;
314
304
  const authorizedAt = new Date().toISOString();
315
305
  this.options.store.createAuthorizedUser({
316
306
  id: userId,
317
307
  workspace_id: pairing.workspace_id,
318
- platform: 'weixin',
308
+ platform: pairing.platform,
319
309
  platform_user_id: pairing.platform_user_id,
320
310
  chat_id: pairing.chat_id,
321
311
  display_name: pairing.display_name,
@@ -324,7 +314,7 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
324
314
  });
325
315
  this.options.store.updatePairingStatus(code, 'approved');
326
316
  this.notifyRuntimeStateChanged();
327
- const user = this.options.store.listAuthorizedUsers(pairing.workspace_id, 'weixin').find((e) => e.id === userId);
317
+ const user = this.options.store.listAuthorizedUsers(pairing.workspace_id, pairing.platform).find((e) => e.id === userId);
328
318
  if (!user)
329
319
  throw new Error('Authorized user lookup failed after approval');
330
320
  return user;
@@ -333,7 +323,7 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
333
323
  const pairing = this.options.store.getPairingRequest(code);
334
324
  if (!pairing)
335
325
  throw new Error(`Pairing code not found: ${code}`);
336
- if (pairing.platform !== 'weixin')
326
+ if (pairing.platform !== 'weixin' && !pairing.platform.startsWith('weixin:'))
337
327
  throw new Error(`Pairing code ${code} is not a WeChat pairing`);
338
328
  this.options.store.updatePairingStatus(code, 'rejected');
339
329
  this.notifyRuntimeStateChanged();
@@ -363,12 +353,14 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
363
353
  this.options.log?.(`localcore-weixin bridge route miss for sessionKey=${sessionKey} type=${event.type}`);
364
354
  return;
365
355
  }
366
- const state = this.runtime.get(route.workspaceId);
356
+ const routeInstanceId = route.instanceId || 'default';
357
+ const routePlatformKey = route.platformKey || (0, config_js_1.channelPlatformKey)('weixin', routeInstanceId);
358
+ const state = this.runtime.get((0, config_js_1.runtimeKey)(route.workspaceId, routeInstanceId)) || this.runtime.get(route.workspaceId);
367
359
  if (!state?.connected) {
368
360
  this.options.log?.(`localcore-weixin bridge event ignored because workspace is not connected: ${route.workspaceId}`);
369
361
  return;
370
362
  }
371
- const initialBinding = this.options.store.getPlatformThreadBinding(route.workspaceId, route.chatId, route.platformUserId, 'weixin');
363
+ const initialBinding = this.options.store.getPlatformThreadBinding(route.workspaceId, route.chatId, route.platformUserId, routePlatformKey);
372
364
  if (!initialBinding) {
373
365
  this.options.log?.(`localcore-weixin bridge binding miss for workspace=${route.workspaceId}`);
374
366
  return;
@@ -386,7 +378,7 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
386
378
  const current = previous
387
379
  .catch(() => undefined)
388
380
  .then(async () => {
389
- const binding = this.options.store.getPlatformThreadBinding(route.workspaceId, route.chatId, route.platformUserId, 'weixin');
381
+ const binding = this.options.store.getPlatformThreadBinding(route.workspaceId, route.chatId, route.platformUserId, routePlatformKey);
390
382
  if (!binding)
391
383
  return;
392
384
  if (this.mutedThreadBridgeCounts.has(binding.thread_id))
@@ -441,7 +433,7 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
441
433
  return this.sendScheduledMessage(workspaceId, { type: 'channel.chat', channelId: chatId }, text);
442
434
  }
443
435
  async sendScheduledMessage(workspaceId, route, text) {
444
- const state = this.runtime.get(workspaceId);
436
+ const state = this.resolveRuntimeState(workspaceId, route.instanceId).state;
445
437
  if (!state?.connected) {
446
438
  this.options.log?.(`localcore-weixin scheduled message skipped: workspace not connected: ${workspaceId}`);
447
439
  return '';
@@ -457,7 +449,7 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
457
449
  }
458
450
  }
459
451
  async sendOutboundMessage(workspaceId, input) {
460
- const state = this.runtime.get(workspaceId);
452
+ const state = this.resolveRuntimeState(workspaceId, input.route?.instanceId).state;
461
453
  if (!state?.connected) {
462
454
  throw new Error(`WeChat workspace is not connected: ${workspaceId}`);
463
455
  }
@@ -465,8 +457,9 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
465
457
  if (!channelId) {
466
458
  throw new Error('Missing WeChat target channel id');
467
459
  }
468
- const binding = await this.getBinding(workspaceId);
469
- const contextToken = this.resolveContextTokenForFileSend(workspaceId, channelId, input.route.participantId);
460
+ const binding = await this.getBinding(workspaceId, input.route?.instanceId);
461
+ const platformKey = state.platformKey;
462
+ const contextToken = this.resolveContextTokenForFileSend(workspaceId, channelId, input.route.participantId, platformKey);
470
463
  const messageIds = [];
471
464
  const attachments = [];
472
465
  for (const part of input.parts || []) {
@@ -528,6 +521,8 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
528
521
  }
529
522
  // ==================== Inbound Message Handling ====================
530
523
  async handleInboundMessage(input) {
524
+ const instanceId = input.instanceId || 'default';
525
+ const platformKey = input.platformKey || (0, config_js_1.channelPlatformKey)('weixin', instanceId);
531
526
  if (this.isDuplicateInboundMessage(input)) {
532
527
  this.options.log?.(`localcore-weixin skipped duplicate inbound message workspace=${input.workspaceId} chat=${input.chatId} id=${input.contextToken || input.messageId}`);
533
528
  return { paired: true, duplicate: true };
@@ -545,28 +540,28 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
545
540
  },
546
541
  });
547
542
  this.options.store.expirePendingPairings();
548
- const binding = await this.getBinding(input.workspaceId);
549
- let authorized = this.options.store.getAuthorizedUser(input.workspaceId, input.platformUserId, 'weixin');
543
+ const binding = await this.getBinding(input.workspaceId, instanceId);
544
+ let authorized = this.options.store.getAuthorizedUser(input.workspaceId, input.platformUserId, platformKey);
550
545
  if (!authorized) {
551
546
  if (binding.allowFrom === '*') {
552
547
  const authorizedAt = new Date().toISOString();
553
548
  this.options.store.createAuthorizedUser({
554
549
  id: `wx-user-${(0, node_crypto_2.randomUUID)()}`,
555
550
  workspace_id: input.workspaceId,
556
- platform: 'weixin',
551
+ platform: platformKey,
557
552
  platform_user_id: input.platformUserId,
558
553
  chat_id: input.chatId,
559
554
  display_name: input.displayName,
560
555
  thread_id: null,
561
556
  authorized_at: authorizedAt,
562
557
  });
563
- authorized = this.options.store.getAuthorizedUser(input.workspaceId, input.platformUserId, 'weixin');
558
+ authorized = this.options.store.getAuthorizedUser(input.workspaceId, input.platformUserId, platformKey);
564
559
  this.options.log?.(`localcore-weixin auto-approved user for ${input.workspaceId}: ${input.platformUserId}`);
565
560
  this.notifyRuntimeStateChanged();
566
561
  }
567
562
  }
568
563
  if (!authorized) {
569
- const existingPending = this.options.store.listPendingPairings(input.workspaceId).find((item) => item.platform === 'weixin' && item.platform_user_id === input.platformUserId && item.chat_id === input.chatId && item.status === 'pending');
564
+ const existingPending = this.options.store.listPendingPairings(input.workspaceId).find((item) => item.platform === platformKey && item.platform_user_id === input.platformUserId && item.chat_id === input.chatId && item.status === 'pending');
570
565
  let pairingCode = existingPending?.code || '';
571
566
  if (!existingPending) {
572
567
  const now = new Date();
@@ -574,7 +569,7 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
574
569
  this.options.store.createPairingRequest({
575
570
  code: pairingCode,
576
571
  workspace_id: input.workspaceId,
577
- platform: 'weixin',
572
+ platform: platformKey,
578
573
  platform_user_id: input.platformUserId,
579
574
  chat_id: input.chatId,
580
575
  display_name: input.displayName,
@@ -584,23 +579,23 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
584
579
  });
585
580
  this.notifyRuntimeStateChanged();
586
581
  }
587
- const state = this.runtime.get(input.workspaceId);
582
+ const state = this.runtime.get((0, config_js_1.runtimeKey)(input.workspaceId, instanceId));
588
583
  if (state?.connected) {
589
584
  await this.sendTextMessage(state, input.chatId, `**已收到消息**\n\n当前账号还未授权接入这个工作区。\n请在桌面端完成审批后再次发送消息。\n\n配对码:\`${pairingCode}\``, input.contextToken);
590
585
  }
591
586
  return { paired: false };
592
587
  }
593
588
  const router = this.options.getWorkspaceRouter();
594
- const threadBinding = this.options.store.getPlatformThreadBinding(input.workspaceId, input.chatId, input.platformUserId, 'weixin');
589
+ const threadBinding = this.options.store.getPlatformThreadBinding(input.workspaceId, input.chatId, input.platformUserId, platformKey);
595
590
  let threadId = threadBinding?.thread_id || authorized.thread_id || '';
596
591
  if (!threadId) {
597
592
  const thread = await router.createThread(input.workspaceId, input.displayName || `WeChat ${input.chatId}`);
598
593
  threadId = thread.id;
599
- this.options.store.updateAuthorizedUserThread(input.workspaceId, input.platformUserId, threadId, 'weixin');
594
+ this.options.store.updateAuthorizedUserThread(input.workspaceId, input.platformUserId, threadId, platformKey);
600
595
  const now = new Date().toISOString();
601
596
  this.options.store.upsertPlatformThreadBinding({
602
597
  workspace_id: input.workspaceId,
603
- platform: 'weixin',
598
+ platform: platformKey,
604
599
  chat_id: input.chatId,
605
600
  platform_user_id: input.platformUserId,
606
601
  thread_id: threadId,
@@ -611,13 +606,15 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
611
606
  }
612
607
  const sessionKey = router.getThreadSessionKey(threadId);
613
608
  const normalizedText = String(input.text || '').trim().toLowerCase();
614
- const permissionThreadId = (normalizedText === 'allow' || normalizedText === 'allow all' || normalizedText === 'deny') ? this.findAwaitingPermissionThreadId(input.workspaceId, input.chatId, input.platformUserId) : '';
609
+ const permissionThreadId = (normalizedText === 'allow' || normalizedText === 'allow all' || normalizedText === 'deny') ? this.findAwaitingPermissionThreadId(input.workspaceId, input.chatId, input.platformUserId, input.platformKey) : '';
615
610
  if (permissionThreadId && permissionThreadId !== threadId) {
616
611
  threadId = permissionThreadId;
617
612
  }
618
613
  const effectiveSessionKey = router.getThreadSessionKey(threadId);
619
614
  this.threadRouting.set(effectiveSessionKey, {
620
615
  workspaceId: input.workspaceId,
616
+ instanceId,
617
+ platformKey,
621
618
  platformUserId: input.platformUserId,
622
619
  chatId: input.chatId,
623
620
  threadId,
@@ -628,10 +625,10 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
628
625
  const title = slashCommand.args.join(' ').trim() || `${input.displayName || 'WeChat'} ${new Date().toLocaleTimeString()}`;
629
626
  const nextThread = await router.createThread(input.workspaceId, title);
630
627
  const now = new Date().toISOString();
631
- this.options.store.updateAuthorizedUserThread(input.workspaceId, input.platformUserId, nextThread.id, 'weixin');
628
+ this.options.store.updateAuthorizedUserThread(input.workspaceId, input.platformUserId, nextThread.id, platformKey);
632
629
  this.options.store.upsertPlatformThreadBinding({
633
630
  workspace_id: input.workspaceId,
634
- platform: 'weixin',
631
+ platform: platformKey,
635
632
  chat_id: input.chatId,
636
633
  platform_user_id: input.platformUserId,
637
634
  thread_id: nextThread.id,
@@ -641,11 +638,13 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
641
638
  });
642
639
  this.threadRouting.set(router.getThreadSessionKey(nextThread.id), {
643
640
  workspaceId: input.workspaceId,
641
+ instanceId,
642
+ platformKey,
644
643
  platformUserId: input.platformUserId,
645
644
  chatId: input.chatId,
646
645
  threadId: nextThread.id,
647
646
  });
648
- const st = this.runtime.get(input.workspaceId);
647
+ const st = this.runtime.get((0, config_js_1.runtimeKey)(input.workspaceId, instanceId));
649
648
  if (st?.connected) {
650
649
  await this.sendTextMessage(st, input.chatId, '**已开始新会话**', input.contextToken);
651
650
  }
@@ -657,70 +656,49 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
657
656
  await router.sendThreadAction(threadId, input.text);
658
657
  return { paired: true, threadId };
659
658
  }
660
- this.options.store.updatePlatformThreadMessageId(input.workspaceId, input.chatId, input.platformUserId, input.contextToken || input.messageId, 'weixin');
659
+ this.options.store.updatePlatformThreadMessageId(input.workspaceId, input.chatId, input.platformUserId, input.contextToken || input.messageId, platformKey);
661
660
  const wrappedText = (0, desktop_js_1.wrapUserMessageWithSchedulerProtocol)(input.text);
662
661
  await router.sendThreadMessage(threadId, this.createThreadMessageInput(wrappedText, input.contentParts));
663
662
  return { paired: true, threadId };
664
663
  }
665
664
  // ==================== Private: Bindings ====================
666
- async getBinding(workspaceId) {
665
+ resolveRuntimeState(workspaceId, instanceId) {
666
+ if (instanceId) {
667
+ const state = this.runtime.get((0, config_js_1.runtimeKey)(workspaceId, instanceId)) || (instanceId === 'default' ? this.runtime.get(workspaceId) : undefined);
668
+ return { instanceId, state };
669
+ }
670
+ const states = [...this.runtime.values()].filter((entry) => entry.workspaceId === workspaceId);
671
+ const state = states.find((entry) => entry.instanceId === 'default') || states[0];
672
+ return { instanceId: state?.instanceId || 'default', state: state || this.runtime.get(workspaceId) };
673
+ }
674
+ async getBinding(workspaceId, instanceId) {
667
675
  const config = await this.options.readConfig();
668
- const binding = this.collectBindings(config).find((e) => e.workspaceId === workspaceId);
676
+ const bindings = this.collectBindings(config).filter((e) => e.workspaceId === workspaceId);
677
+ const binding = instanceId
678
+ ? bindings.find((entry) => entry.instanceId === instanceId)
679
+ : bindings.find((entry) => entry.instanceId === 'default') || bindings[0];
669
680
  if (!binding)
670
- throw new Error(`No WeChat binding configured for workspace "${workspaceId}"`);
681
+ throw new Error(`No WeChat binding configured for workspace "${workspaceId}"${instanceId ? ` instance "${instanceId}"` : ''}`);
671
682
  return binding;
672
683
  }
673
684
  collectBindings(config) {
674
- const projects = Array.isArray(config?.projects) ? config.projects : [];
675
- return projects.flatMap((project) => {
676
- const platforms = Array.isArray(project.platforms) ? project.platforms : [];
677
- return platforms
678
- .map((platform) => ({
679
- platformType: (0, desktop_js_1.normalizeDesktopPlatformType)(platform?.type),
680
- options: platform?.options && typeof platform.options === 'object'
681
- ? platform.options
682
- : {},
683
- }))
684
- .filter((p) => p.platformType === 'weixin')
685
- .map((p) => {
686
- const stateDir = String(p.options.state_dir || this.getDefaultStateDir()).trim();
687
- const credentials = this.loadCredentials(project.name, stateDir);
688
- const configuredToken = String(p.options.token || '').trim();
689
- const configuredBaseUrl = String(p.options.base_url || '').trim();
690
- const accountId = String(p.options.account_id || credentials?.botId || 'qr-login').trim();
691
- return {
692
- workspaceId: project.name,
693
- token: configuredToken || credentials?.token || '',
694
- accountId,
695
- baseUrl: configuredBaseUrl || credentials?.baseUrl || DEFAULT_BASE_URL,
696
- cdnBaseUrl: String(p.options.cdn_base_url || DEFAULT_CDN_BASE_URL).trim(),
697
- allowFrom: String(p.options.allow_from || '*').trim(),
698
- routeTag: String(p.options.route_tag || '').trim(),
699
- longPollTimeoutMs: Number(p.options.long_poll_timeout_ms || LONG_POLL_TIMEOUT_MS) || LONG_POLL_TIMEOUT_MS,
700
- stateDir,
701
- proxy: String(p.options.proxy || '').trim(),
702
- proxyUsername: String(p.options.proxy_username || '').trim(),
703
- proxyPassword: String(p.options.proxy_password || '').trim(),
704
- enabled: true,
705
- project,
706
- };
707
- });
708
- });
709
- }
710
- getDefaultStateDir() {
711
- return node_path_1.default.join(process.cwd(), 'weixin-monitor');
685
+ return (0, config_js_1.collectWeixinWorkspaceBindings)(config);
712
686
  }
713
687
  // ==================== Private: Workspace Lifecycle ====================
714
688
  async startWorkspace(binding) {
715
- await this.stopWorkspace(binding.workspaceId);
689
+ const key = (0, config_js_1.runtimeKey)(binding.workspaceId, binding.instanceId);
690
+ await this.stopWorkspaceKey(key);
716
691
  const status = {
717
692
  workspaceId: binding.workspaceId,
693
+ instanceId: binding.instanceId,
694
+ displayName: binding.displayName,
695
+ platformKey: binding.platformKey,
718
696
  enabled: true,
719
697
  status: 'starting',
720
698
  connected: false,
721
699
  accountId: binding.accountId,
722
700
  };
723
- this.runtime.set(binding.workspaceId, status);
701
+ this.runtime.set(key, status);
724
702
  this.notifyRuntimeStateChanged();
725
703
  try {
726
704
  const abortController = new AbortController();
@@ -755,16 +733,23 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
755
733
  }
756
734
  this.notifyRuntimeStateChanged();
757
735
  }
758
- async stopWorkspace(workspaceId) {
759
- const state = this.runtime.get(workspaceId);
736
+ async stopWorkspace(workspaceId, instanceId) {
737
+ const resolved = this.resolveRuntimeState(workspaceId, instanceId);
738
+ await this.stopWorkspaceKey((0, config_js_1.runtimeKey)(workspaceId, resolved.instanceId));
739
+ }
740
+ async stopWorkspaceKey(key) {
741
+ const state = this.runtime.get(key);
760
742
  if (!state)
761
743
  return;
762
744
  try {
763
745
  state.abortController?.abort();
764
746
  }
765
747
  catch { }
766
- this.runtime.set(workspaceId, {
767
- workspaceId,
748
+ this.runtime.set(key, {
749
+ workspaceId: state.workspaceId,
750
+ instanceId: state.instanceId,
751
+ displayName: state.displayName,
752
+ platformKey: state.platformKey,
768
753
  enabled: false,
769
754
  status: 'stopped',
770
755
  connected: false,
@@ -773,35 +758,6 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
773
758
  this.notifyRuntimeStateChanged();
774
759
  }
775
760
  // ==================== Private: Long-poll Monitor ====================
776
- getBufPath(binding) {
777
- return node_path_1.default.join(binding.stateDir, `${binding.accountId}.buf`);
778
- }
779
- getCredentialsPath(workspaceId, stateDir) {
780
- return node_path_1.default.join(stateDir, `${safeFilePart(workspaceId)}.credentials.json`);
781
- }
782
- loadCredentials(workspaceId, stateDir) {
783
- try {
784
- const raw = node_fs_1.default.readFileSync(this.getCredentialsPath(workspaceId, stateDir), 'utf-8');
785
- const parsed = JSON.parse(raw);
786
- const token = String(parsed.token || '').trim();
787
- if (!token)
788
- return null;
789
- return {
790
- token,
791
- baseUrl: parsed.baseUrl ? String(parsed.baseUrl) : undefined,
792
- botId: parsed.botId ? String(parsed.botId) : undefined,
793
- userId: parsed.userId ? String(parsed.userId) : undefined,
794
- savedAt: parsed.savedAt ? String(parsed.savedAt) : new Date().toISOString(),
795
- };
796
- }
797
- catch {
798
- return null;
799
- }
800
- }
801
- saveCredentials(binding, credentials) {
802
- node_fs_1.default.mkdirSync(binding.stateDir, { recursive: true });
803
- node_fs_1.default.writeFileSync(this.getCredentialsPath(binding.workspaceId, binding.stateDir), JSON.stringify(credentials, null, 2), 'utf-8');
804
- }
805
761
  normalizeQrStatus(status) {
806
762
  const normalized = String(status || 'wait').trim().toLowerCase();
807
763
  if (normalized === 'scaned' || normalized === 'scanned' || normalized === 'signed')
@@ -812,32 +768,19 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
812
768
  return 'expired';
813
769
  return 'wait';
814
770
  }
815
- loadBuf(binding) {
816
- try {
817
- return node_fs_1.default.readFileSync(this.getBufPath(binding), 'utf-8');
818
- }
819
- catch {
820
- return '';
821
- }
822
- }
823
- saveBuf(binding, buf) {
824
- const dir = binding.stateDir;
825
- node_fs_1.default.mkdirSync(dir, { recursive: true });
826
- node_fs_1.default.writeFileSync(this.getBufPath(binding), buf, 'utf-8');
827
- }
828
771
  async runMonitorLoop(binding, signal) {
829
- let buf = this.loadBuf(binding);
772
+ let buf = (0, config_js_1.loadWeixinBuf)(binding);
830
773
  let consecutiveFailures = 0;
831
774
  while (!signal.aborted) {
832
775
  try {
833
- const resp = await this.getUpdates(binding, buf, signal);
776
+ const resp = await (0, transport_js_1.getWeixinUpdates)(binding, buf, signal);
834
777
  const isApiError = (resp.ret !== undefined && resp.ret !== 0) || (resp.errcode !== undefined && resp.errcode !== 0);
835
778
  if (isApiError) {
836
779
  consecutiveFailures++;
837
780
  const errorText = resp.errmsg ? ` errmsg=${resp.errmsg}` : '';
838
781
  this.options.log?.(`localcore-weixin getUpdates failed for ${binding.workspaceId}: ret=${resp.ret} errcode=${resp.errcode}${errorText} (${consecutiveFailures}/${MAX_CONSECUTIVE_FAILURES})`);
839
782
  if (resp.errcode === -14 || resp.ret === -14) {
840
- const state = this.runtime.get(binding.workspaceId);
783
+ const state = this.runtime.get((0, config_js_1.runtimeKey)(binding.workspaceId, binding.instanceId));
841
784
  if (state) {
842
785
  state.status = 'error';
843
786
  state.connected = false;
@@ -857,13 +800,13 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
857
800
  consecutiveFailures = 0;
858
801
  if (resp.get_updates_buf) {
859
802
  buf = resp.get_updates_buf;
860
- this.saveBuf(binding, buf);
803
+ (0, config_js_1.saveWeixinBuf)(binding, buf);
861
804
  }
862
805
  for (const msg of resp.msgs ?? []) {
863
806
  const items = msg.item_list ?? [];
864
- const textItem = items.find((i) => i.type === TEXT_ITEM_TYPE);
865
- const voiceTextItems = items.filter((i) => i.type === VOICE_ITEM_TYPE && i.voice_item?.text);
866
- const mediaItems = items.filter((i) => i.type === IMAGE_ITEM_TYPE || i.type === FILE_ITEM_TYPE);
807
+ const textItem = items.find((i) => i.type === transport_js_1.TEXT_ITEM_TYPE);
808
+ const voiceTextItems = items.filter((i) => i.type === transport_js_1.VOICE_ITEM_TYPE && i.voice_item?.text);
809
+ const mediaItems = items.filter((i) => i.type === transport_js_1.IMAGE_ITEM_TYPE || i.type === transport_js_1.FILE_ITEM_TYPE);
867
810
  if (!textItem && voiceTextItems.length === 0 && mediaItems.length === 0)
868
811
  continue;
869
812
  const conversationId = msg.from_user_id ?? '';
@@ -902,6 +845,8 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
902
845
  continue;
903
846
  await this.handleInboundMessage({
904
847
  workspaceId: binding.workspaceId,
848
+ instanceId: binding.instanceId,
849
+ platformKey: binding.platformKey,
905
850
  platformUserId: conversationId,
906
851
  chatId: conversationId,
907
852
  displayName: conversationId.slice(-6),
@@ -931,65 +876,6 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
931
876
  }
932
877
  }
933
878
  // ==================== Private: HTTP API ====================
934
- async apiPost(binding, endpoint, bodyObj, timeoutMs, signal) {
935
- const url = `${binding.baseUrl.replace(/\/$/, '')}/${endpoint}`;
936
- const body = JSON.stringify(bodyObj);
937
- const controller = new AbortController();
938
- const timer = setTimeout(() => controller.abort(), timeoutMs);
939
- const onAbort = () => controller.abort();
940
- signal?.addEventListener('abort', onAbort, { once: true });
941
- try {
942
- const headers = {
943
- 'Content-Type': 'application/json',
944
- 'Content-Length': String(Buffer.byteLength(body, 'utf-8')),
945
- 'X-WECHAT-UIN': createWechatUin(),
946
- ...createIlinkHeaders(),
947
- };
948
- if (binding.token) {
949
- headers.AuthorizationType = 'ilink_bot_token';
950
- headers.Authorization = `Bearer ${binding.token}`;
951
- }
952
- const res = await fetch(url, {
953
- method: 'POST',
954
- headers,
955
- body,
956
- signal: controller.signal,
957
- });
958
- if (!res.ok)
959
- throw new Error(`HTTP ${res.status}`);
960
- return (await res.json());
961
- }
962
- finally {
963
- clearTimeout(timer);
964
- signal?.removeEventListener('abort', onAbort);
965
- }
966
- }
967
- async apiGet(binding, endpoint, timeoutMs = API_TIMEOUT_MS) {
968
- const url = `${binding.baseUrl.replace(/\/$/, '')}/${endpoint}`;
969
- const controller = new AbortController();
970
- const timer = setTimeout(() => controller.abort(), timeoutMs);
971
- try {
972
- const headers = createIlinkHeaders();
973
- if (binding.token) {
974
- headers.AuthorizationType = 'ilink_bot_token';
975
- headers.Authorization = `Bearer ${binding.token}`;
976
- }
977
- const res = await fetch(url, {
978
- method: 'GET',
979
- headers,
980
- signal: controller.signal,
981
- });
982
- if (!res.ok)
983
- throw new Error(`HTTP ${res.status}`);
984
- return (await res.json());
985
- }
986
- finally {
987
- clearTimeout(timer);
988
- }
989
- }
990
- async getUpdates(binding, buf, signal) {
991
- return this.apiPost(binding, 'ilink/bot/getupdates', { get_updates_buf: buf, base_info: { channel_version: WEIXIN_CHANNEL_VERSION } }, binding.longPollTimeoutMs, signal);
992
- }
993
879
  async sendTextMessage(state, toUserId, text, contextToken, options = {}) {
994
880
  const binding = await this.getBinding(state.workspaceId);
995
881
  const stripped = stripHtml(text);
@@ -998,56 +884,20 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
998
884
  : splitTextByUtf8Bytes(stripped, WEIXIN_TEXT_MESSAGE_MAX_BYTES);
999
885
  for (const [index, chunk] of chunks.entries()) {
1000
886
  const finalChunk = options.final && index === chunks.length - 1;
1001
- const resp = await this.sendTextMessageChunk(binding, toUserId, chunk, contextToken, {
887
+ const resp = await (0, transport_js_1.sendWeixinTextMessageChunk)(binding, toUserId, chunk, contextToken, {
1002
888
  clientId: options.clientId,
1003
889
  final: finalChunk,
1004
890
  });
1005
- if (this.isSendMessageError(resp)) {
891
+ if ((0, transport_js_1.isWeixinApiError)(resp)) {
1006
892
  throw new Error(`WeChat sendmessage failed: ret=${resp.ret} errcode=${resp.errcode}${resp.errmsg ? ` errmsg=${resp.errmsg}` : ''} chunk=${index + 1}/${chunks.length} bytes=${utf8ByteLength(chunk)} context=${contextToken ? 'yes' : 'no'} message_state=${finalChunk ? 2 : 1}`);
1007
893
  }
1008
894
  }
1009
895
  this.options.log?.(`localcore-weixin sent message to ${toUserId} for workspace ${state.workspaceId}${chunks.length > 1 ? ` chunks=${chunks.length}` : ''}`);
1010
896
  }
1011
- async sendTextMessageChunk(binding, toUserId, text, contextToken, options = {}) {
1012
- return this.apiPost(binding, 'ilink/bot/sendmessage', {
1013
- msg: {
1014
- from_user_id: '',
1015
- to_user_id: toUserId,
1016
- client_id: options.clientId || `openclaw-weixin-${node_crypto_1.default.randomUUID()}`,
1017
- message_type: 2,
1018
- message_state: options.final === false ? 1 : 2,
1019
- item_list: [{ type: TEXT_ITEM_TYPE, text_item: { text } }],
1020
- ...(contextToken ? { context_token: contextToken } : {}),
1021
- },
1022
- base_info: { channel_version: WEIXIN_CHANNEL_VERSION },
1023
- }, API_TIMEOUT_MS);
1024
- }
1025
897
  async sendFileMessage(binding, toUserId, fileName, uploaded, contextToken) {
1026
898
  const clientId = `openclaw-weixin-${node_crypto_1.default.randomUUID()}`;
1027
- const resp = await this.apiPost(binding, 'ilink/bot/sendmessage', {
1028
- msg: {
1029
- from_user_id: '',
1030
- to_user_id: toUserId,
1031
- client_id: clientId,
1032
- message_type: 2,
1033
- message_state: 2,
1034
- item_list: [{
1035
- type: FILE_ITEM_TYPE,
1036
- file_item: {
1037
- media: {
1038
- encrypt_query_param: uploaded.encryptedQueryParam,
1039
- aes_key: Buffer.from(uploaded.aesKeyHex).toString('base64'),
1040
- encrypt_type: 1,
1041
- },
1042
- file_name: fileName,
1043
- len: String(uploaded.fileSize),
1044
- },
1045
- }],
1046
- ...(contextToken ? { context_token: contextToken } : {}),
1047
- },
1048
- base_info: { channel_version: WEIXIN_CHANNEL_VERSION },
1049
- }, API_TIMEOUT_MS);
1050
- if (this.isSendMessageError(resp)) {
899
+ const resp = await (0, transport_js_1.sendWeixinFileMessage)(binding, toUserId, fileName, uploaded, contextToken, { clientId });
900
+ if ((0, transport_js_1.isWeixinApiError)(resp)) {
1051
901
  throw new Error(`WeChat send file failed: ret=${resp.ret} errcode=${resp.errcode}${resp.errmsg ? ` errmsg=${resp.errmsg}` : ''}`);
1052
902
  }
1053
903
  return clientId;
@@ -1068,9 +918,6 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
1068
918
  file,
1069
919
  };
1070
920
  }
1071
- isSendMessageError(resp) {
1072
- return (resp.ret !== undefined && resp.ret !== 0) || (resp.errcode !== undefined && resp.errcode !== 0);
1073
- }
1074
921
  async uploadFileToCdn(binding, toUserId, filePath) {
1075
922
  const plaintext = await node_fs_1.default.promises.readFile(filePath);
1076
923
  const fileKey = node_crypto_1.default.randomBytes(16).toString('hex');
@@ -1078,21 +925,11 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
1078
925
  const aesKeyHex = aesKey.toString('hex');
1079
926
  const rawMd5 = node_crypto_1.default.createHash('md5').update(plaintext).digest('hex');
1080
927
  const cipherSize = this.getAesEcbPaddedSize(plaintext.length);
1081
- const uploadUrlResp = await this.apiPost(binding, 'ilink/bot/getuploadurl', {
1082
- filekey: fileKey,
1083
- media_type: UPLOAD_MEDIA_TYPE_FILE,
1084
- to_user_id: toUserId,
1085
- rawsize: plaintext.length,
1086
- rawfilemd5: rawMd5,
1087
- filesize: cipherSize,
1088
- no_need_thumb: true,
1089
- aeskey: aesKeyHex,
1090
- base_info: { channel_version: WEIXIN_CHANNEL_VERSION },
1091
- }, API_TIMEOUT_MS);
1092
- if (this.isSendMessageError(uploadUrlResp) || !uploadUrlResp.upload_param) {
928
+ const uploadUrlResp = await (0, transport_js_1.getWeixinUploadUrl)(binding, fileKey, toUserId, plaintext.length, rawMd5, cipherSize, aesKeyHex);
929
+ if ((0, transport_js_1.isWeixinApiError)(uploadUrlResp) || !uploadUrlResp.upload_param) {
1093
930
  throw new Error(`WeChat getuploadurl failed: ret=${uploadUrlResp.ret} errcode=${uploadUrlResp.errcode}${uploadUrlResp.errmsg ? ` errmsg=${uploadUrlResp.errmsg}` : ''}`);
1094
931
  }
1095
- const encryptedQueryParam = await this.uploadEncryptedBufferToCdn(binding, plaintext, uploadUrlResp.upload_param, fileKey, aesKey);
932
+ const encryptedQueryParam = await (0, transport_js_1.uploadEncryptedBufferToWeixinCdn)(binding, plaintext, uploadUrlResp.upload_param, fileKey, aesKey);
1096
933
  return {
1097
934
  fileKey,
1098
935
  encryptedQueryParam,
@@ -1101,43 +938,23 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
1101
938
  cipherSize,
1102
939
  };
1103
940
  }
1104
- async uploadEncryptedBufferToCdn(binding, plaintext, uploadParam, fileKey, aesKey) {
1105
- const cipher = node_crypto_1.default.createCipheriv('aes-128-ecb', aesKey, null);
1106
- const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
1107
- const url = `${binding.cdnBaseUrl.replace(/\/$/, '')}/upload?encrypted_query_param=${encodeURIComponent(uploadParam)}&filekey=${encodeURIComponent(fileKey)}`;
1108
- const res = await fetch(url, {
1109
- method: 'POST',
1110
- headers: {
1111
- 'Content-Type': 'application/octet-stream',
1112
- },
1113
- body: new Uint8Array(ciphertext),
1114
- });
1115
- if (!res.ok) {
1116
- throw new Error(`WeChat CDN upload failed: HTTP ${res.status}`);
1117
- }
1118
- const encryptedQueryParam = res.headers.get('x-encrypted-param') || '';
1119
- if (!encryptedQueryParam) {
1120
- throw new Error('WeChat CDN upload response missing x-encrypted-param');
1121
- }
1122
- return encryptedQueryParam;
1123
- }
1124
941
  getAesEcbPaddedSize(size) {
1125
942
  return Math.ceil((size + 1) / 16) * 16;
1126
943
  }
1127
- resolveContextTokenForFileSend(workspaceId, channelId, participantId) {
944
+ resolveContextTokenForFileSend(workspaceId, channelId, participantId, platformKey = 'weixin') {
1128
945
  const preferredParticipantId = String(participantId || '').trim();
1129
946
  const direct = preferredParticipantId
1130
- ? this.options.store.getPlatformThreadBinding(workspaceId, channelId, preferredParticipantId, 'weixin')
947
+ ? this.options.store.getPlatformThreadBinding(workspaceId, channelId, preferredParticipantId, platformKey)
1131
948
  : undefined;
1132
949
  if (direct?.last_platform_message_id) {
1133
950
  return direct.last_platform_message_id;
1134
951
  }
1135
- const users = this.options.store.listAuthorizedUsers(workspaceId, 'weixin');
952
+ const users = this.options.store.listAuthorizedUsers(workspaceId, platformKey);
1136
953
  const user = users.find((entry) => entry.chatId === channelId || entry.platformUserId === channelId);
1137
954
  if (!user) {
1138
955
  return '';
1139
956
  }
1140
- return this.options.store.getPlatformThreadBinding(workspaceId, user.chatId, user.platformUserId, 'weixin')?.last_platform_message_id || '';
957
+ return this.options.store.getPlatformThreadBinding(workspaceId, user.chatId, user.platformUserId, platformKey)?.last_platform_message_id || '';
1141
958
  }
1142
959
  // ==================== Private: Attachment Download ====================
1143
960
  sniffExtAndKind(buf) {
@@ -1185,7 +1002,7 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
1185
1002
  resultBuf = rawBuf;
1186
1003
  }
1187
1004
  const { ext, kind } = this.sniffExtAndKind(resultBuf);
1188
- const declaredName = String(itemData?.file_name ?? (item.type === IMAGE_ITEM_TYPE ? 'image' : 'file'));
1005
+ const declaredName = String(itemData?.file_name ?? (item.type === transport_js_1.IMAGE_ITEM_TYPE ? 'image' : 'file'));
1189
1006
  const safeName = declaredName.replace(/[^a-zA-Z0-9._-]/g, '_').slice(0, 64);
1190
1007
  const safeMsgId = msgId.replace(/[^a-zA-Z0-9._-]/g, '_').slice(0, 48);
1191
1008
  const fileName = `${safeMsgId}-${idx}-${safeName}${ext}`;
@@ -1361,9 +1178,9 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
1361
1178
  this.processedInboundMessages.set(key, now + PROCESSED_MESSAGE_TTL_MS);
1362
1179
  return false;
1363
1180
  }
1364
- findAwaitingPermissionThreadId(workspaceId, chatId, platformUserId) {
1181
+ findAwaitingPermissionThreadId(workspaceId, chatId, platformUserId, platformKey = 'weixin') {
1365
1182
  for (const [sessionKey, route] of this.threadRouting.entries()) {
1366
- if (route.workspaceId !== workspaceId || route.chatId !== chatId || route.platformUserId !== platformUserId)
1183
+ if (route.workspaceId !== workspaceId || route.platformKey !== platformKey || route.chatId !== chatId || route.platformUserId !== platformUserId)
1367
1184
  continue;
1368
1185
  const turn = this.outboundTurns.get(sessionKey);
1369
1186
  if (turn?.awaitingPermission && route.threadId)