@kafca/agentdock 0.1.22 → 0.1.24

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 (89) hide show
  1. package/README.md +4 -0
  2. package/dist/renderer/assets/{Badge-B_rgzxYO.js → Badge-CXcUMqb-.js} +1 -1
  3. package/dist/renderer/assets/{Chat-CYtp6m7T.js → Chat-BRxdPzi-.js} +1 -1
  4. package/dist/renderer/assets/{ChatMarkdown-DOBMwa9d.js → ChatMarkdown-sWGBIfIb.js} +1 -1
  5. package/dist/renderer/assets/{Config-DppMSPHF.js → Config-DqxuxIGg.js} +1 -1
  6. package/dist/renderer/assets/{CronList-BDtB1SuA.js → CronList-OgHxbti5.js} +1 -1
  7. package/dist/renderer/assets/{Dashboard-iFlRnd7Y.js → Dashboard-DmklXiH1.js} +2 -2
  8. package/dist/renderer/assets/{EmptyState-bKDt_T88.js → EmptyState-BvDmi8dq.js} +1 -1
  9. package/dist/renderer/assets/{KnowledgeDetail-CKPhKPIg.js → KnowledgeDetail-Bj59Hdpo.js} +2 -2
  10. package/dist/renderer/assets/{KnowledgeHome-CFeJre6i.js → KnowledgeHome-CigQ-nSo.js} +3 -3
  11. package/dist/renderer/assets/{Logs-CyxSVOFK.js → Logs-Bfc7gBpY.js} +1 -1
  12. package/dist/renderer/assets/{Modal-CxPnk1Ey.js → Modal-CfmiFknz.js} +1 -1
  13. package/dist/renderer/assets/{Page-kU9vubzj.js → Page-BxtLEw_b.js} +1 -1
  14. package/dist/renderer/assets/{ProjectDetail-D9FY9NWZ.js → ProjectDetail-CcUphxGm.js} +1 -1
  15. package/dist/renderer/assets/{ProjectList-DIwr2TXg.js → ProjectList-CVkvDIfy.js} +1 -1
  16. package/dist/renderer/assets/{Select-BaWoz5Dn.js → Select-DgQMZ0MD.js} +1 -1
  17. package/dist/renderer/assets/{SessionChat-Cm9CW8Zx.js → SessionChat-CRmGKfBx.js} +1 -1
  18. package/dist/renderer/assets/{SessionList-UPnWlXXO.js → SessionList-D0E8XbYw.js} +1 -1
  19. package/dist/renderer/assets/ThreadChat-DUgqZSBB.js +23 -0
  20. package/dist/renderer/assets/Workspace-Q4g4rlRT.js +14 -0
  21. package/dist/renderer/assets/{arrow-left-BmZAVGV7.js → arrow-left-BySN_zhF.js} +1 -1
  22. package/dist/renderer/assets/{book-open-DZ3GXFgI.js → book-open-BGlrnrTc.js} +1 -1
  23. package/dist/renderer/assets/{check-CeVSvDCv.js → check-BiJogEa5.js} +1 -1
  24. package/dist/renderer/assets/{chevron-down-yryuMoQ-.js → chevron-down-D2fHRJv6.js} +1 -1
  25. package/dist/renderer/assets/{circle-eASsFISH.js → circle-CIcjixmI.js} +1 -1
  26. package/dist/renderer/assets/desktop-DAXY9Bxv.js +4 -0
  27. package/dist/renderer/assets/desktop-DyA_NYew.js +1 -0
  28. package/dist/renderer/assets/{funnel-pwAe2Ixp.js → funnel-BUzwb-id.js} +1 -1
  29. package/dist/renderer/assets/{heart-B2Sp-bCm.js → heart-DZEQ1z_V.js} +1 -1
  30. package/dist/renderer/assets/{index-BC3MWbS1.js → index-CB_KyLOr.js} +39 -39
  31. package/dist/renderer/assets/{index-Cse2Br64.js → index-DgsIo4vJ.js} +3 -3
  32. package/dist/renderer/assets/{pencil-BfEunUF3.js → pencil-DGL4agcl.js} +1 -1
  33. package/dist/renderer/assets/{play-Dcs9Lj_Z.js → play-DzasCLM4.js} +1 -1
  34. package/dist/renderer/assets/{plug-B_YschQe.js → plug-dubNMe_m.js} +1 -1
  35. package/dist/renderer/assets/{plus-Pqo-cXHQ.js → plus-DN4kML4k.js} +1 -1
  36. package/dist/renderer/assets/projects-migPtfRe.js +1 -0
  37. package/dist/renderer/assets/{save-BKM5yi1A.js → save-CNIc9tjl.js} +1 -1
  38. package/dist/renderer/assets/{search-BU42h10M.js → search-C0EGbF3I.js} +1 -1
  39. package/dist/renderer/assets/{server-DA5t3Rtr.js → server-BMxQ4zmc.js} +1 -1
  40. package/dist/renderer/assets/{session-utils-Cr9VhNSq.js → session-utils-Bf5O-iMi.js} +1 -1
  41. package/dist/renderer/assets/{sessions-bvV8ZMfX.js → sessions-BMSs6rEI.js} +1 -1
  42. package/dist/renderer/assets/{shield-check-BeOJKur_.js → shield-check-i3Tf1TUz.js} +1 -1
  43. package/dist/renderer/assets/{trash-2-CQ7HKO3q.js → trash-2-SKrcp18f.js} +1 -1
  44. package/dist/renderer/assets/{user-CtFjY9SV.js → user-cNUZddjZ.js} +1 -1
  45. package/dist/renderer/assets/{wifi-off-Ci5m0ysf.js → wifi-off-Dyb84ANR.js} +1 -1
  46. package/dist/renderer/index.html +1 -1
  47. package/dist-electron/electron/channel-content.test.js +29 -0
  48. package/dist-electron/electron/lac-cli.test.js +49 -4
  49. package/dist-electron/electron/local-core-acp-content.test.js +39 -0
  50. package/dist-electron/electron/local-core-contracts.test.js +67 -0
  51. package/dist-electron/electron/local-core-refactor.test.js +628 -0
  52. package/dist-electron/electron/plugin-kernel.test.js +74 -14
  53. package/dist-electron/electron/workspace-task-store.test.js +124 -0
  54. package/dist-electron/packages/contracts/src/local-core.js +110 -0
  55. package/dist-electron/packages/core-sdk/src/index.js +40 -20
  56. package/dist-electron/services/local-ai-core/src/acp/local-core-acp-content.js +25 -4
  57. package/dist-electron/services/local-ai-core/src/acp/local-core-acp-response-processor.js +3 -2
  58. package/dist-electron/services/local-ai-core/src/acp/local-core-acp-store.js +44 -28
  59. package/dist-electron/services/local-ai-core/src/acp/local-core-acp-turn-coordinator.js +5 -1
  60. package/dist-electron/services/local-ai-core/src/acp/workspace-acp-permissions.js +15 -6
  61. package/dist-electron/services/local-ai-core/src/channel/lark/cards.js +210 -0
  62. package/dist-electron/services/local-ai-core/src/channel/lark/config.js +54 -0
  63. package/dist-electron/services/local-ai-core/src/channel/lark/local-core-lark-gateway.js +280 -496
  64. package/dist-electron/services/local-ai-core/src/channel/lark/registration.js +50 -0
  65. package/dist-electron/services/local-ai-core/src/channel/lark/runtime-state.js +191 -0
  66. package/dist-electron/services/local-ai-core/src/channel/lark/types.js +2 -0
  67. package/dist-electron/services/local-ai-core/src/channel/shared/content.js +23 -0
  68. package/dist-electron/services/local-ai-core/src/channel/shared/plugin.js +31 -0
  69. package/dist-electron/services/local-ai-core/src/channel/weixin/config.js +125 -0
  70. package/dist-electron/services/local-ai-core/src/channel/weixin/local-core-weixin-gateway.js +164 -352
  71. package/dist-electron/services/local-ai-core/src/channel/weixin/transport.js +169 -0
  72. package/dist-electron/services/local-ai-core/src/channel/weixin/types.js +2 -0
  73. package/dist-electron/services/local-ai-core/src/cli/lac.js +19 -16
  74. package/dist-electron/services/local-ai-core/src/kernel/bootstrap.js +5 -0
  75. package/dist-electron/services/local-ai-core/src/plugins/builtin/channel-lark-plugin.js +18 -39
  76. package/dist-electron/services/local-ai-core/src/plugins/builtin/channel-weixin-plugin.js +18 -39
  77. package/dist-electron/services/local-ai-core/src/runtime/local-core-controller.js +55 -44
  78. package/dist-electron/services/local-ai-core/src/runtime/server.js +10 -8
  79. package/dist-electron/services/local-ai-core/src/scheduler/job-id.js +15 -0
  80. package/dist-electron/services/local-ai-core/src/scheduler/scheduler-service.js +30 -5
  81. package/dist-electron/shared/desktop.js +37 -5
  82. package/dist-electron/src/pages/Threads/thread-chat-model.js +57 -0
  83. package/dist-electron/src/pages/Threads/thread-chat-permission.test.js +70 -0
  84. package/package.json +1 -1
  85. package/dist/renderer/assets/ThreadChat-YvVsACW3.js +0 -23
  86. package/dist/renderer/assets/Workspace-ST7V8u6T.js +0 -14
  87. package/dist/renderer/assets/desktop-Che1cbpE.js +0 -4
  88. package/dist/renderer/assets/desktop-DkrP_s-b.js +0 -1
  89. package/dist/renderer/assets/projects-BCj0k_Gy.js +0 -1
@@ -4,19 +4,19 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.LocalCoreWeixinGateway = void 0;
7
+ exports.createWeixinAttachmentContentPart = createWeixinAttachmentContentPart;
7
8
  const node_crypto_1 = __importDefault(require("node:crypto"));
8
9
  const node_fs_1 = __importDefault(require("node:fs"));
9
10
  const node_path_1 = __importDefault(require("node:path"));
10
11
  const node_events_1 = require("node:events");
11
12
  const node_crypto_2 = require("node:crypto");
12
13
  const desktop_js_1 = require("../../../../../shared/desktop.js");
14
+ const content_js_1 = require("../shared/content.js");
13
15
  const file_utils_js_1 = require("../shared/file-utils.js");
16
+ const config_js_1 = require("./config.js");
17
+ const transport_js_1 = require("./transport.js");
14
18
  // ==================== Constants ====================
15
19
  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
20
  const RETRY_DELAY_MS = 2_000;
21
21
  const BACKOFF_DELAY_MS = 30_000;
22
22
  const MAX_CONSECUTIVE_FAILURES = 3;
@@ -26,15 +26,7 @@ const WEIXIN_CONTEXT_REPLY_MAX_BYTES = 3500;
26
26
  const WEIXIN_CONTEXT_SEND_LIMIT = 10;
27
27
  const WEIXIN_RESERVED_TERMINAL_SENDS = 1;
28
28
  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
29
  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
30
  const WEIXIN_MAX_UPLOAD_FILE_SIZE = 30 * 1024 * 1024;
39
31
  // ==================== Utilities ====================
40
32
  function stripHtml(html) {
@@ -60,18 +52,6 @@ function sleep(ms, signal) {
60
52
  signal?.addEventListener('abort', onAbort, { once: true });
61
53
  });
62
54
  }
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
55
  function utf8ByteLength(value) {
76
56
  return Buffer.byteLength(value, 'utf-8');
77
57
  }
@@ -119,6 +99,23 @@ function splitTextByUtf8Bytes(text, maxBytes) {
119
99
  pushCurrent();
120
100
  return chunks;
121
101
  }
102
+ function createWeixinAttachmentContentPart(att) {
103
+ if (att.kind === 'image') {
104
+ if (!att.data)
105
+ return null;
106
+ return {
107
+ type: 'image',
108
+ data: att.data,
109
+ mimeType: att.mimeType,
110
+ fileName: att.name,
111
+ };
112
+ }
113
+ return {
114
+ type: 'file',
115
+ path: att.path,
116
+ fileName: att.name,
117
+ };
118
+ }
122
119
  function truncateTextByUtf8Bytes(text, maxBytes) {
123
120
  const normalized = String(text || '').replace(/\r\n/g, '\n').trim();
124
121
  if (utf8ByteLength(normalized) <= maxBytes)
@@ -163,21 +160,25 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
163
160
  async refreshBindings() {
164
161
  const config = await this.options.readConfig();
165
162
  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);
163
+ const nextKeys = new Set(bindings.map((b) => (0, config_js_1.runtimeKey)(b.workspaceId, b.instanceId)));
164
+ for (const key of [...this.runtime.keys()]) {
165
+ if (!nextKeys.has(key)) {
166
+ await this.stopWorkspaceKey(key);
170
167
  }
171
168
  }
172
169
  for (const binding of bindings) {
173
- const current = this.runtime.get(binding.workspaceId);
170
+ const key = (0, config_js_1.runtimeKey)(binding.workspaceId, binding.instanceId);
171
+ const current = this.runtime.get(key);
174
172
  if (!binding.enabled) {
175
173
  if (current) {
176
- await this.stopWorkspace(binding.workspaceId);
174
+ await this.stopWorkspaceKey(key);
177
175
  }
178
176
  else {
179
- this.runtime.set(binding.workspaceId, {
177
+ this.runtime.set(key, {
180
178
  workspaceId: binding.workspaceId,
179
+ instanceId: binding.instanceId,
180
+ displayName: binding.displayName,
181
+ platformKey: binding.platformKey,
181
182
  enabled: false,
182
183
  status: 'disabled',
183
184
  connected: false,
@@ -193,35 +194,39 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
193
194
  }
194
195
  this.notifyRuntimeStateChanged();
195
196
  }
196
- async testConnection(workspaceId) {
197
- const binding = await this.getBinding(workspaceId);
197
+ async testConnection(workspaceId, instanceId) {
198
+ const binding = await this.getBinding(workspaceId, instanceId);
198
199
  try {
199
- const bufPath = this.getBufPath(binding);
200
+ const bufPath = (0, config_js_1.getWeixinBufPath)(binding);
200
201
  node_fs_1.default.accessSync(bufPath);
201
- return { success: true, platform: 'weixin', workspaceId, appId: binding.accountId };
202
+ return { success: true, platform: 'weixin', workspaceId, instanceId: binding.instanceId, appId: binding.accountId };
202
203
  }
203
204
  catch {
204
- return { success: false, platform: 'weixin', workspaceId, error: 'No sync buf found for account. Ensure the plugin has been connected at least once.' };
205
+ 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
206
  }
206
207
  }
207
- async enable(workspaceId) {
208
- const binding = await this.getBinding(workspaceId);
208
+ async enable(workspaceId, instanceId) {
209
+ const binding = await this.getBinding(workspaceId, instanceId);
209
210
  await this.startWorkspace(binding);
210
- return this.getStatus(workspaceId);
211
+ return this.getStatus(workspaceId, binding.instanceId);
211
212
  }
212
- async disable(workspaceId) {
213
- await this.stopWorkspace(workspaceId);
214
- return this.getStatus(workspaceId);
213
+ async disable(workspaceId, instanceId) {
214
+ await this.stopWorkspace(workspaceId, instanceId);
215
+ return this.getStatus(workspaceId, instanceId);
215
216
  }
216
- getStatus(workspaceId) {
217
+ getStatus(workspaceId, instanceId) {
217
218
  this.options.store.expirePendingPairings();
218
- const binding = this.runtime.get(workspaceId);
219
+ const resolved = this.resolveRuntimeState(workspaceId, instanceId);
220
+ const binding = resolved.state;
221
+ const platformKey = binding?.platformKey || (0, config_js_1.channelPlatformKey)('weixin', resolved.instanceId);
219
222
  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');
223
+ .filter((row) => row.platform === platformKey && row.expires_at >= new Date().toISOString());
224
+ const users = this.options.store.listAuthorizedUsers(workspaceId, platformKey);
222
225
  return {
223
226
  workspaceId,
224
227
  platform: 'weixin',
228
+ instanceId: resolved.instanceId,
229
+ displayName: binding?.displayName,
225
230
  enabled: Boolean(binding?.enabled),
226
231
  connected: Boolean(binding?.connected),
227
232
  status: binding?.status || 'disabled',
@@ -233,16 +238,20 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
233
238
  };
234
239
  }
235
240
  listStatuses() {
236
- return [...this.runtime.keys()].sort().map((workspaceId) => this.getStatus(workspaceId));
241
+ return [...this.runtime.values()]
242
+ .sort((a, b) => `${a.workspaceId}:${a.instanceId}`.localeCompare(`${b.workspaceId}:${b.instanceId}`))
243
+ .map((state) => this.getStatus(state.workspaceId, state.instanceId));
237
244
  }
238
245
  listPendingPairings(workspaceId) {
239
246
  this.options.store.expirePendingPairings();
240
247
  return this.options.store
241
- .listPairingRequests(workspaceId, 'weixin')
248
+ .listPairingRequests(workspaceId)
249
+ .filter((item) => item.platform === 'weixin' || item.platform.startsWith('weixin:'))
242
250
  .filter((item) => item.status === 'pending' && item.expiresAt >= new Date().toISOString());
243
251
  }
244
252
  listAuthorizedUsers(workspaceId) {
245
- return this.options.store.listAuthorizedUsers(workspaceId, 'weixin');
253
+ return this.options.store.listAuthorizedUsers(workspaceId)
254
+ .filter((item) => item.platform === 'weixin' || item.platform.startsWith('weixin:'));
246
255
  }
247
256
  async start() {
248
257
  await this.refreshBindings();
@@ -251,12 +260,12 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
251
260
  this.close();
252
261
  }
253
262
  close() {
254
- return Promise.all([...this.runtime.keys()].map((id) => this.stopWorkspace(id))).then(() => undefined);
263
+ return Promise.all([...this.runtime.keys()].map((id) => this.stopWorkspaceKey(id))).then(() => undefined);
255
264
  }
256
265
  // ==================== 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`);
266
+ async getQrCode(workspaceId, instanceId) {
267
+ const binding = await this.getBinding(workspaceId, instanceId);
268
+ const data = await (0, transport_js_1.weixinApiGet)(binding, `ilink/bot/get_bot_qrcode?bot_type=3`);
260
269
  if (data.errcode && data.errcode !== 0) {
261
270
  throw new Error(`Failed to get QR code: ${data.errmsg || data.errcode}`);
262
271
  }
@@ -264,11 +273,11 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
264
273
  throw new Error('No QR code returned from iLink');
265
274
  }
266
275
  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 };
276
+ return { ticket: data.qrcode, expiresIn: data.expired || 180, qrCodeUrl, instanceId: binding.instanceId, displayName: binding.displayName };
268
277
  }
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)}`);
278
+ async checkQrCodeStatus(workspaceId, ticket, instanceId) {
279
+ const binding = await this.getBinding(workspaceId, instanceId);
280
+ const data = await (0, transport_js_1.weixinApiGet)(binding, `ilink/bot/get_qrcode_status?qrcode=${encodeURIComponent(ticket)}`);
272
281
  if (data.errcode && data.errcode !== 0) {
273
282
  throw new Error(`Failed to check QR code status: ${data.errmsg || data.errcode}`);
274
283
  }
@@ -277,14 +286,14 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
277
286
  if (!data.bot_token) {
278
287
  throw new Error('WeChat QR code confirmed but no bot token was returned.');
279
288
  }
280
- this.saveCredentials(binding, {
289
+ (0, config_js_1.saveWeixinCredentials)(binding, {
281
290
  token: data.bot_token,
282
291
  baseUrl: data.baseurl || binding.baseUrl,
283
292
  botId: data.ilink_bot_id,
284
293
  userId: data.ilink_user_id || data.user_id,
285
294
  savedAt: new Date().toISOString(),
286
295
  });
287
- await this.startWorkspace(await this.getBinding(workspaceId));
296
+ await this.startWorkspace(await this.getBinding(workspaceId, binding.instanceId));
288
297
  }
289
298
  if (!['wait', 'signed', 'confirmed', 'expired'].includes(status)) {
290
299
  return { status: 'wait' };
@@ -301,7 +310,7 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
301
310
  const pairing = this.options.store.getPairingRequest(code);
302
311
  if (!pairing)
303
312
  throw new Error(`Pairing code not found: ${code}`);
304
- if (pairing.platform !== 'weixin')
313
+ if (pairing.platform !== 'weixin' && !pairing.platform.startsWith('weixin:'))
305
314
  throw new Error(`Pairing code ${code} is not a WeChat pairing`);
306
315
  if (pairing.status !== 'pending')
307
316
  throw new Error(`Pairing code ${code} is already ${pairing.status}`);
@@ -309,13 +318,13 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
309
318
  this.options.store.updatePairingStatus(code, 'expired');
310
319
  throw new Error(`Pairing code ${code} has expired`);
311
320
  }
312
- const existing = this.options.store.getAuthorizedUser(pairing.workspace_id, pairing.platform_user_id, 'weixin');
321
+ const existing = this.options.store.getAuthorizedUser(pairing.workspace_id, pairing.platform_user_id, pairing.platform);
313
322
  const userId = existing?.id || `wx-user-${(0, node_crypto_2.randomUUID)()}`;
314
323
  const authorizedAt = new Date().toISOString();
315
324
  this.options.store.createAuthorizedUser({
316
325
  id: userId,
317
326
  workspace_id: pairing.workspace_id,
318
- platform: 'weixin',
327
+ platform: pairing.platform,
319
328
  platform_user_id: pairing.platform_user_id,
320
329
  chat_id: pairing.chat_id,
321
330
  display_name: pairing.display_name,
@@ -324,7 +333,7 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
324
333
  });
325
334
  this.options.store.updatePairingStatus(code, 'approved');
326
335
  this.notifyRuntimeStateChanged();
327
- const user = this.options.store.listAuthorizedUsers(pairing.workspace_id, 'weixin').find((e) => e.id === userId);
336
+ const user = this.options.store.listAuthorizedUsers(pairing.workspace_id, pairing.platform).find((e) => e.id === userId);
328
337
  if (!user)
329
338
  throw new Error('Authorized user lookup failed after approval');
330
339
  return user;
@@ -333,7 +342,7 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
333
342
  const pairing = this.options.store.getPairingRequest(code);
334
343
  if (!pairing)
335
344
  throw new Error(`Pairing code not found: ${code}`);
336
- if (pairing.platform !== 'weixin')
345
+ if (pairing.platform !== 'weixin' && !pairing.platform.startsWith('weixin:'))
337
346
  throw new Error(`Pairing code ${code} is not a WeChat pairing`);
338
347
  this.options.store.updatePairingStatus(code, 'rejected');
339
348
  this.notifyRuntimeStateChanged();
@@ -363,12 +372,14 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
363
372
  this.options.log?.(`localcore-weixin bridge route miss for sessionKey=${sessionKey} type=${event.type}`);
364
373
  return;
365
374
  }
366
- const state = this.runtime.get(route.workspaceId);
375
+ const routeInstanceId = route.instanceId || 'default';
376
+ const routePlatformKey = route.platformKey || (0, config_js_1.channelPlatformKey)('weixin', routeInstanceId);
377
+ const state = this.runtime.get((0, config_js_1.runtimeKey)(route.workspaceId, routeInstanceId)) || this.runtime.get(route.workspaceId);
367
378
  if (!state?.connected) {
368
379
  this.options.log?.(`localcore-weixin bridge event ignored because workspace is not connected: ${route.workspaceId}`);
369
380
  return;
370
381
  }
371
- const initialBinding = this.options.store.getPlatformThreadBinding(route.workspaceId, route.chatId, route.platformUserId, 'weixin');
382
+ const initialBinding = this.options.store.getPlatformThreadBinding(route.workspaceId, route.chatId, route.platformUserId, routePlatformKey);
372
383
  if (!initialBinding) {
373
384
  this.options.log?.(`localcore-weixin bridge binding miss for workspace=${route.workspaceId}`);
374
385
  return;
@@ -386,7 +397,7 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
386
397
  const current = previous
387
398
  .catch(() => undefined)
388
399
  .then(async () => {
389
- const binding = this.options.store.getPlatformThreadBinding(route.workspaceId, route.chatId, route.platformUserId, 'weixin');
400
+ const binding = this.options.store.getPlatformThreadBinding(route.workspaceId, route.chatId, route.platformUserId, routePlatformKey);
390
401
  if (!binding)
391
402
  return;
392
403
  if (this.mutedThreadBridgeCounts.has(binding.thread_id))
@@ -441,7 +452,7 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
441
452
  return this.sendScheduledMessage(workspaceId, { type: 'channel.chat', channelId: chatId }, text);
442
453
  }
443
454
  async sendScheduledMessage(workspaceId, route, text) {
444
- const state = this.runtime.get(workspaceId);
455
+ const state = this.resolveRuntimeState(workspaceId, route.instanceId).state;
445
456
  if (!state?.connected) {
446
457
  this.options.log?.(`localcore-weixin scheduled message skipped: workspace not connected: ${workspaceId}`);
447
458
  return '';
@@ -457,7 +468,7 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
457
468
  }
458
469
  }
459
470
  async sendOutboundMessage(workspaceId, input) {
460
- const state = this.runtime.get(workspaceId);
471
+ const state = this.resolveRuntimeState(workspaceId, input.route?.instanceId).state;
461
472
  if (!state?.connected) {
462
473
  throw new Error(`WeChat workspace is not connected: ${workspaceId}`);
463
474
  }
@@ -465,8 +476,9 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
465
476
  if (!channelId) {
466
477
  throw new Error('Missing WeChat target channel id');
467
478
  }
468
- const binding = await this.getBinding(workspaceId);
469
- const contextToken = this.resolveContextTokenForFileSend(workspaceId, channelId, input.route.participantId);
479
+ const binding = await this.getBinding(workspaceId, input.route?.instanceId);
480
+ const platformKey = state.platformKey;
481
+ const contextToken = this.resolveContextTokenForFileSend(workspaceId, channelId, input.route.participantId, platformKey);
470
482
  const messageIds = [];
471
483
  const attachments = [];
472
484
  for (const part of input.parts || []) {
@@ -528,6 +540,8 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
528
540
  }
529
541
  // ==================== Inbound Message Handling ====================
530
542
  async handleInboundMessage(input) {
543
+ const instanceId = input.instanceId || 'default';
544
+ const platformKey = input.platformKey || (0, config_js_1.channelPlatformKey)('weixin', instanceId);
531
545
  if (this.isDuplicateInboundMessage(input)) {
532
546
  this.options.log?.(`localcore-weixin skipped duplicate inbound message workspace=${input.workspaceId} chat=${input.chatId} id=${input.contextToken || input.messageId}`);
533
547
  return { paired: true, duplicate: true };
@@ -545,28 +559,28 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
545
559
  },
546
560
  });
547
561
  this.options.store.expirePendingPairings();
548
- const binding = await this.getBinding(input.workspaceId);
549
- let authorized = this.options.store.getAuthorizedUser(input.workspaceId, input.platformUserId, 'weixin');
562
+ const binding = await this.getBinding(input.workspaceId, instanceId);
563
+ let authorized = this.options.store.getAuthorizedUser(input.workspaceId, input.platformUserId, platformKey);
550
564
  if (!authorized) {
551
565
  if (binding.allowFrom === '*') {
552
566
  const authorizedAt = new Date().toISOString();
553
567
  this.options.store.createAuthorizedUser({
554
568
  id: `wx-user-${(0, node_crypto_2.randomUUID)()}`,
555
569
  workspace_id: input.workspaceId,
556
- platform: 'weixin',
570
+ platform: platformKey,
557
571
  platform_user_id: input.platformUserId,
558
572
  chat_id: input.chatId,
559
573
  display_name: input.displayName,
560
574
  thread_id: null,
561
575
  authorized_at: authorizedAt,
562
576
  });
563
- authorized = this.options.store.getAuthorizedUser(input.workspaceId, input.platformUserId, 'weixin');
577
+ authorized = this.options.store.getAuthorizedUser(input.workspaceId, input.platformUserId, platformKey);
564
578
  this.options.log?.(`localcore-weixin auto-approved user for ${input.workspaceId}: ${input.platformUserId}`);
565
579
  this.notifyRuntimeStateChanged();
566
580
  }
567
581
  }
568
582
  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');
583
+ 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
584
  let pairingCode = existingPending?.code || '';
571
585
  if (!existingPending) {
572
586
  const now = new Date();
@@ -574,7 +588,7 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
574
588
  this.options.store.createPairingRequest({
575
589
  code: pairingCode,
576
590
  workspace_id: input.workspaceId,
577
- platform: 'weixin',
591
+ platform: platformKey,
578
592
  platform_user_id: input.platformUserId,
579
593
  chat_id: input.chatId,
580
594
  display_name: input.displayName,
@@ -584,23 +598,23 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
584
598
  });
585
599
  this.notifyRuntimeStateChanged();
586
600
  }
587
- const state = this.runtime.get(input.workspaceId);
601
+ const state = this.runtime.get((0, config_js_1.runtimeKey)(input.workspaceId, instanceId));
588
602
  if (state?.connected) {
589
603
  await this.sendTextMessage(state, input.chatId, `**已收到消息**\n\n当前账号还未授权接入这个工作区。\n请在桌面端完成审批后再次发送消息。\n\n配对码:\`${pairingCode}\``, input.contextToken);
590
604
  }
591
605
  return { paired: false };
592
606
  }
593
607
  const router = this.options.getWorkspaceRouter();
594
- const threadBinding = this.options.store.getPlatformThreadBinding(input.workspaceId, input.chatId, input.platformUserId, 'weixin');
608
+ const threadBinding = this.options.store.getPlatformThreadBinding(input.workspaceId, input.chatId, input.platformUserId, platformKey);
595
609
  let threadId = threadBinding?.thread_id || authorized.thread_id || '';
596
610
  if (!threadId) {
597
611
  const thread = await router.createThread(input.workspaceId, input.displayName || `WeChat ${input.chatId}`);
598
612
  threadId = thread.id;
599
- this.options.store.updateAuthorizedUserThread(input.workspaceId, input.platformUserId, threadId, 'weixin');
613
+ this.options.store.updateAuthorizedUserThread(input.workspaceId, input.platformUserId, threadId, platformKey);
600
614
  const now = new Date().toISOString();
601
615
  this.options.store.upsertPlatformThreadBinding({
602
616
  workspace_id: input.workspaceId,
603
- platform: 'weixin',
617
+ platform: platformKey,
604
618
  chat_id: input.chatId,
605
619
  platform_user_id: input.platformUserId,
606
620
  thread_id: threadId,
@@ -611,13 +625,15 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
611
625
  }
612
626
  const sessionKey = router.getThreadSessionKey(threadId);
613
627
  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) : '';
628
+ const permissionThreadId = (normalizedText === 'allow' || normalizedText === 'allow all' || normalizedText === 'deny') ? this.findAwaitingPermissionThreadId(input.workspaceId, input.chatId, input.platformUserId, input.platformKey) : '';
615
629
  if (permissionThreadId && permissionThreadId !== threadId) {
616
630
  threadId = permissionThreadId;
617
631
  }
618
632
  const effectiveSessionKey = router.getThreadSessionKey(threadId);
619
633
  this.threadRouting.set(effectiveSessionKey, {
620
634
  workspaceId: input.workspaceId,
635
+ instanceId,
636
+ platformKey,
621
637
  platformUserId: input.platformUserId,
622
638
  chatId: input.chatId,
623
639
  threadId,
@@ -628,10 +644,10 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
628
644
  const title = slashCommand.args.join(' ').trim() || `${input.displayName || 'WeChat'} ${new Date().toLocaleTimeString()}`;
629
645
  const nextThread = await router.createThread(input.workspaceId, title);
630
646
  const now = new Date().toISOString();
631
- this.options.store.updateAuthorizedUserThread(input.workspaceId, input.platformUserId, nextThread.id, 'weixin');
647
+ this.options.store.updateAuthorizedUserThread(input.workspaceId, input.platformUserId, nextThread.id, platformKey);
632
648
  this.options.store.upsertPlatformThreadBinding({
633
649
  workspace_id: input.workspaceId,
634
- platform: 'weixin',
650
+ platform: platformKey,
635
651
  chat_id: input.chatId,
636
652
  platform_user_id: input.platformUserId,
637
653
  thread_id: nextThread.id,
@@ -641,11 +657,13 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
641
657
  });
642
658
  this.threadRouting.set(router.getThreadSessionKey(nextThread.id), {
643
659
  workspaceId: input.workspaceId,
660
+ instanceId,
661
+ platformKey,
644
662
  platformUserId: input.platformUserId,
645
663
  chatId: input.chatId,
646
664
  threadId: nextThread.id,
647
665
  });
648
- const st = this.runtime.get(input.workspaceId);
666
+ const st = this.runtime.get((0, config_js_1.runtimeKey)(input.workspaceId, instanceId));
649
667
  if (st?.connected) {
650
668
  await this.sendTextMessage(st, input.chatId, '**已开始新会话**', input.contextToken);
651
669
  }
@@ -657,70 +675,49 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
657
675
  await router.sendThreadAction(threadId, input.text);
658
676
  return { paired: true, threadId };
659
677
  }
660
- this.options.store.updatePlatformThreadMessageId(input.workspaceId, input.chatId, input.platformUserId, input.contextToken || input.messageId, 'weixin');
678
+ this.options.store.updatePlatformThreadMessageId(input.workspaceId, input.chatId, input.platformUserId, input.contextToken || input.messageId, platformKey);
661
679
  const wrappedText = (0, desktop_js_1.wrapUserMessageWithSchedulerProtocol)(input.text);
662
- await router.sendThreadMessage(threadId, this.createThreadMessageInput(wrappedText, input.contentParts));
680
+ await router.sendThreadMessage(threadId, (0, content_js_1.createChannelThreadMessageInput)(wrappedText, input.contentParts));
663
681
  return { paired: true, threadId };
664
682
  }
665
683
  // ==================== Private: Bindings ====================
666
- async getBinding(workspaceId) {
684
+ resolveRuntimeState(workspaceId, instanceId) {
685
+ if (instanceId) {
686
+ const state = this.runtime.get((0, config_js_1.runtimeKey)(workspaceId, instanceId)) || (instanceId === 'default' ? this.runtime.get(workspaceId) : undefined);
687
+ return { instanceId, state };
688
+ }
689
+ const states = [...this.runtime.values()].filter((entry) => entry.workspaceId === workspaceId);
690
+ const state = states.find((entry) => entry.instanceId === 'default') || states[0];
691
+ return { instanceId: state?.instanceId || 'default', state: state || this.runtime.get(workspaceId) };
692
+ }
693
+ async getBinding(workspaceId, instanceId) {
667
694
  const config = await this.options.readConfig();
668
- const binding = this.collectBindings(config).find((e) => e.workspaceId === workspaceId);
695
+ const bindings = this.collectBindings(config).filter((e) => e.workspaceId === workspaceId);
696
+ const binding = instanceId
697
+ ? bindings.find((entry) => entry.instanceId === instanceId)
698
+ : bindings.find((entry) => entry.instanceId === 'default') || bindings[0];
669
699
  if (!binding)
670
- throw new Error(`No WeChat binding configured for workspace "${workspaceId}"`);
700
+ throw new Error(`No WeChat binding configured for workspace "${workspaceId}"${instanceId ? ` instance "${instanceId}"` : ''}`);
671
701
  return binding;
672
702
  }
673
703
  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');
704
+ return (0, config_js_1.collectWeixinWorkspaceBindings)(config);
712
705
  }
713
706
  // ==================== Private: Workspace Lifecycle ====================
714
707
  async startWorkspace(binding) {
715
- await this.stopWorkspace(binding.workspaceId);
708
+ const key = (0, config_js_1.runtimeKey)(binding.workspaceId, binding.instanceId);
709
+ await this.stopWorkspaceKey(key);
716
710
  const status = {
717
711
  workspaceId: binding.workspaceId,
712
+ instanceId: binding.instanceId,
713
+ displayName: binding.displayName,
714
+ platformKey: binding.platformKey,
718
715
  enabled: true,
719
716
  status: 'starting',
720
717
  connected: false,
721
718
  accountId: binding.accountId,
722
719
  };
723
- this.runtime.set(binding.workspaceId, status);
720
+ this.runtime.set(key, status);
724
721
  this.notifyRuntimeStateChanged();
725
722
  try {
726
723
  const abortController = new AbortController();
@@ -755,16 +752,23 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
755
752
  }
756
753
  this.notifyRuntimeStateChanged();
757
754
  }
758
- async stopWorkspace(workspaceId) {
759
- const state = this.runtime.get(workspaceId);
755
+ async stopWorkspace(workspaceId, instanceId) {
756
+ const resolved = this.resolveRuntimeState(workspaceId, instanceId);
757
+ await this.stopWorkspaceKey((0, config_js_1.runtimeKey)(workspaceId, resolved.instanceId));
758
+ }
759
+ async stopWorkspaceKey(key) {
760
+ const state = this.runtime.get(key);
760
761
  if (!state)
761
762
  return;
762
763
  try {
763
764
  state.abortController?.abort();
764
765
  }
765
766
  catch { }
766
- this.runtime.set(workspaceId, {
767
- workspaceId,
767
+ this.runtime.set(key, {
768
+ workspaceId: state.workspaceId,
769
+ instanceId: state.instanceId,
770
+ displayName: state.displayName,
771
+ platformKey: state.platformKey,
768
772
  enabled: false,
769
773
  status: 'stopped',
770
774
  connected: false,
@@ -773,35 +777,6 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
773
777
  this.notifyRuntimeStateChanged();
774
778
  }
775
779
  // ==================== 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
780
  normalizeQrStatus(status) {
806
781
  const normalized = String(status || 'wait').trim().toLowerCase();
807
782
  if (normalized === 'scaned' || normalized === 'scanned' || normalized === 'signed')
@@ -812,32 +787,19 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
812
787
  return 'expired';
813
788
  return 'wait';
814
789
  }
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
790
  async runMonitorLoop(binding, signal) {
829
- let buf = this.loadBuf(binding);
791
+ let buf = (0, config_js_1.loadWeixinBuf)(binding);
830
792
  let consecutiveFailures = 0;
831
793
  while (!signal.aborted) {
832
794
  try {
833
- const resp = await this.getUpdates(binding, buf, signal);
795
+ const resp = await (0, transport_js_1.getWeixinUpdates)(binding, buf, signal);
834
796
  const isApiError = (resp.ret !== undefined && resp.ret !== 0) || (resp.errcode !== undefined && resp.errcode !== 0);
835
797
  if (isApiError) {
836
798
  consecutiveFailures++;
837
799
  const errorText = resp.errmsg ? ` errmsg=${resp.errmsg}` : '';
838
800
  this.options.log?.(`localcore-weixin getUpdates failed for ${binding.workspaceId}: ret=${resp.ret} errcode=${resp.errcode}${errorText} (${consecutiveFailures}/${MAX_CONSECUTIVE_FAILURES})`);
839
801
  if (resp.errcode === -14 || resp.ret === -14) {
840
- const state = this.runtime.get(binding.workspaceId);
802
+ const state = this.runtime.get((0, config_js_1.runtimeKey)(binding.workspaceId, binding.instanceId));
841
803
  if (state) {
842
804
  state.status = 'error';
843
805
  state.connected = false;
@@ -857,13 +819,13 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
857
819
  consecutiveFailures = 0;
858
820
  if (resp.get_updates_buf) {
859
821
  buf = resp.get_updates_buf;
860
- this.saveBuf(binding, buf);
822
+ (0, config_js_1.saveWeixinBuf)(binding, buf);
861
823
  }
862
824
  for (const msg of resp.msgs ?? []) {
863
825
  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);
826
+ const textItem = items.find((i) => i.type === transport_js_1.TEXT_ITEM_TYPE);
827
+ const voiceTextItems = items.filter((i) => i.type === transport_js_1.VOICE_ITEM_TYPE && i.voice_item?.text);
828
+ const mediaItems = items.filter((i) => i.type === transport_js_1.IMAGE_ITEM_TYPE || i.type === transport_js_1.FILE_ITEM_TYPE);
867
829
  if (!textItem && voiceTextItems.length === 0 && mediaItems.length === 0)
868
830
  continue;
869
831
  const conversationId = msg.from_user_id ?? '';
@@ -882,14 +844,9 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
882
844
  if (att) {
883
845
  attachmentText += attachmentText ? '\n' : '';
884
846
  attachmentText += att.kind === 'image' ? `[Image: ${att.path}]` : `[File "${att.name}": ${att.path}]`;
885
- if (att.kind === 'image' && att.data) {
886
- attachmentParts.push({
887
- type: 'image',
888
- data: att.data,
889
- mimeType: att.mimeType,
890
- fileName: att.name,
891
- });
892
- }
847
+ const part = createWeixinAttachmentContentPart(att);
848
+ if (part)
849
+ attachmentParts.push(part);
893
850
  }
894
851
  }
895
852
  catch (dlErr) {
@@ -902,6 +859,8 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
902
859
  continue;
903
860
  await this.handleInboundMessage({
904
861
  workspaceId: binding.workspaceId,
862
+ instanceId: binding.instanceId,
863
+ platformKey: binding.platformKey,
905
864
  platformUserId: conversationId,
906
865
  chatId: conversationId,
907
866
  displayName: conversationId.slice(-6),
@@ -931,65 +890,6 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
931
890
  }
932
891
  }
933
892
  // ==================== 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
893
  async sendTextMessage(state, toUserId, text, contextToken, options = {}) {
994
894
  const binding = await this.getBinding(state.workspaceId);
995
895
  const stripped = stripHtml(text);
@@ -998,56 +898,20 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
998
898
  : splitTextByUtf8Bytes(stripped, WEIXIN_TEXT_MESSAGE_MAX_BYTES);
999
899
  for (const [index, chunk] of chunks.entries()) {
1000
900
  const finalChunk = options.final && index === chunks.length - 1;
1001
- const resp = await this.sendTextMessageChunk(binding, toUserId, chunk, contextToken, {
901
+ const resp = await (0, transport_js_1.sendWeixinTextMessageChunk)(binding, toUserId, chunk, contextToken, {
1002
902
  clientId: options.clientId,
1003
903
  final: finalChunk,
1004
904
  });
1005
- if (this.isSendMessageError(resp)) {
905
+ if ((0, transport_js_1.isWeixinApiError)(resp)) {
1006
906
  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
907
  }
1008
908
  }
1009
909
  this.options.log?.(`localcore-weixin sent message to ${toUserId} for workspace ${state.workspaceId}${chunks.length > 1 ? ` chunks=${chunks.length}` : ''}`);
1010
910
  }
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
911
  async sendFileMessage(binding, toUserId, fileName, uploaded, contextToken) {
1026
912
  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)) {
913
+ const resp = await (0, transport_js_1.sendWeixinFileMessage)(binding, toUserId, fileName, uploaded, contextToken, { clientId });
914
+ if ((0, transport_js_1.isWeixinApiError)(resp)) {
1051
915
  throw new Error(`WeChat send file failed: ret=${resp.ret} errcode=${resp.errcode}${resp.errmsg ? ` errmsg=${resp.errmsg}` : ''}`);
1052
916
  }
1053
917
  return clientId;
@@ -1068,9 +932,6 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
1068
932
  file,
1069
933
  };
1070
934
  }
1071
- isSendMessageError(resp) {
1072
- return (resp.ret !== undefined && resp.ret !== 0) || (resp.errcode !== undefined && resp.errcode !== 0);
1073
- }
1074
935
  async uploadFileToCdn(binding, toUserId, filePath) {
1075
936
  const plaintext = await node_fs_1.default.promises.readFile(filePath);
1076
937
  const fileKey = node_crypto_1.default.randomBytes(16).toString('hex');
@@ -1078,21 +939,11 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
1078
939
  const aesKeyHex = aesKey.toString('hex');
1079
940
  const rawMd5 = node_crypto_1.default.createHash('md5').update(plaintext).digest('hex');
1080
941
  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) {
942
+ const uploadUrlResp = await (0, transport_js_1.getWeixinUploadUrl)(binding, fileKey, toUserId, plaintext.length, rawMd5, cipherSize, aesKeyHex);
943
+ if ((0, transport_js_1.isWeixinApiError)(uploadUrlResp) || !uploadUrlResp.upload_param) {
1093
944
  throw new Error(`WeChat getuploadurl failed: ret=${uploadUrlResp.ret} errcode=${uploadUrlResp.errcode}${uploadUrlResp.errmsg ? ` errmsg=${uploadUrlResp.errmsg}` : ''}`);
1094
945
  }
1095
- const encryptedQueryParam = await this.uploadEncryptedBufferToCdn(binding, plaintext, uploadUrlResp.upload_param, fileKey, aesKey);
946
+ const encryptedQueryParam = await (0, transport_js_1.uploadEncryptedBufferToWeixinCdn)(binding, plaintext, uploadUrlResp.upload_param, fileKey, aesKey);
1096
947
  return {
1097
948
  fileKey,
1098
949
  encryptedQueryParam,
@@ -1101,43 +952,23 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
1101
952
  cipherSize,
1102
953
  };
1103
954
  }
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
955
  getAesEcbPaddedSize(size) {
1125
956
  return Math.ceil((size + 1) / 16) * 16;
1126
957
  }
1127
- resolveContextTokenForFileSend(workspaceId, channelId, participantId) {
958
+ resolveContextTokenForFileSend(workspaceId, channelId, participantId, platformKey = 'weixin') {
1128
959
  const preferredParticipantId = String(participantId || '').trim();
1129
960
  const direct = preferredParticipantId
1130
- ? this.options.store.getPlatformThreadBinding(workspaceId, channelId, preferredParticipantId, 'weixin')
961
+ ? this.options.store.getPlatformThreadBinding(workspaceId, channelId, preferredParticipantId, platformKey)
1131
962
  : undefined;
1132
963
  if (direct?.last_platform_message_id) {
1133
964
  return direct.last_platform_message_id;
1134
965
  }
1135
- const users = this.options.store.listAuthorizedUsers(workspaceId, 'weixin');
966
+ const users = this.options.store.listAuthorizedUsers(workspaceId, platformKey);
1136
967
  const user = users.find((entry) => entry.chatId === channelId || entry.platformUserId === channelId);
1137
968
  if (!user) {
1138
969
  return '';
1139
970
  }
1140
- return this.options.store.getPlatformThreadBinding(workspaceId, user.chatId, user.platformUserId, 'weixin')?.last_platform_message_id || '';
971
+ return this.options.store.getPlatformThreadBinding(workspaceId, user.chatId, user.platformUserId, platformKey)?.last_platform_message_id || '';
1141
972
  }
1142
973
  // ==================== Private: Attachment Download ====================
1143
974
  sniffExtAndKind(buf) {
@@ -1185,7 +1016,7 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
1185
1016
  resultBuf = rawBuf;
1186
1017
  }
1187
1018
  const { ext, kind } = this.sniffExtAndKind(resultBuf);
1188
- const declaredName = String(itemData?.file_name ?? (item.type === IMAGE_ITEM_TYPE ? 'image' : 'file'));
1019
+ const declaredName = String(itemData?.file_name ?? (item.type === transport_js_1.IMAGE_ITEM_TYPE ? 'image' : 'file'));
1189
1020
  const safeName = declaredName.replace(/[^a-zA-Z0-9._-]/g, '_').slice(0, 64);
1190
1021
  const safeMsgId = msgId.replace(/[^a-zA-Z0-9._-]/g, '_').slice(0, 48);
1191
1022
  const fileName = `${safeMsgId}-${idx}-${safeName}${ext}`;
@@ -1200,25 +1031,6 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
1200
1031
  mimeType: kind === 'image' ? this.mimeTypeForImageExt(ext) : undefined,
1201
1032
  };
1202
1033
  }
1203
- createWrappedContentParts(wrappedText, parts) {
1204
- const nonTextParts = Array.isArray(parts)
1205
- ? parts.filter((part) => part.type !== 'text')
1206
- : [];
1207
- return [
1208
- { type: 'text', text: wrappedText },
1209
- ...nonTextParts,
1210
- ];
1211
- }
1212
- createThreadMessageInput(wrappedText, parts) {
1213
- const hasNonTextPart = Array.isArray(parts) && parts.some((part) => part.type !== 'text');
1214
- if (!hasNonTextPart) {
1215
- return wrappedText;
1216
- }
1217
- return {
1218
- displayText: wrappedText,
1219
- contentParts: this.createWrappedContentParts(wrappedText, parts),
1220
- };
1221
- }
1222
1034
  mimeTypeForImageExt(ext) {
1223
1035
  if (ext === '.jpg')
1224
1036
  return 'image/jpeg';
@@ -1361,9 +1173,9 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
1361
1173
  this.processedInboundMessages.set(key, now + PROCESSED_MESSAGE_TTL_MS);
1362
1174
  return false;
1363
1175
  }
1364
- findAwaitingPermissionThreadId(workspaceId, chatId, platformUserId) {
1176
+ findAwaitingPermissionThreadId(workspaceId, chatId, platformUserId, platformKey = 'weixin') {
1365
1177
  for (const [sessionKey, route] of this.threadRouting.entries()) {
1366
- if (route.workspaceId !== workspaceId || route.chatId !== chatId || route.platformUserId !== platformUserId)
1178
+ if (route.workspaceId !== workspaceId || route.platformKey !== platformKey || route.chatId !== chatId || route.platformUserId !== platformUserId)
1367
1179
  continue;
1368
1180
  const turn = this.outboundTurns.get(sessionKey);
1369
1181
  if (turn?.awaitingPermission && route.threadId)