@kafca/agentdock 0.1.21 → 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 (81) hide show
  1. package/README.md +9 -0
  2. package/dist/renderer/assets/{Badge-BRmQIqbd.js → Badge-C2K5apfV.js} +1 -1
  3. package/dist/renderer/assets/{Chat-D94ODABB.js → Chat-DI2YeVKb.js} +1 -1
  4. package/dist/renderer/assets/{ChatMarkdown-DqW7OUvV.js → ChatMarkdown-DMAr15Oi.js} +1 -1
  5. package/dist/renderer/assets/{Config-9lgzpTCp.js → Config-7rP3EuUf.js} +2 -2
  6. package/dist/renderer/assets/CronList-81QslYE8.js +1 -0
  7. package/dist/renderer/assets/{Dashboard-COISXbQy.js → Dashboard-Db2T7rSZ.js} +2 -2
  8. package/dist/renderer/assets/{EmptyState-w5-I9zg7.js → EmptyState-BBJ3cTvB.js} +1 -1
  9. package/dist/renderer/assets/{KnowledgeDetail-DVxwrTLw.js → KnowledgeDetail-Dz_6flG3.js} +2 -2
  10. package/dist/renderer/assets/{KnowledgeHome-BKkubcwi.js → KnowledgeHome-DIh046mN.js} +3 -3
  11. package/dist/renderer/assets/Logs-BXBtHJLf.js +1 -0
  12. package/dist/renderer/assets/{Modal-Ceu_MP2e.js → Modal-BB2Av_Xs.js} +1 -1
  13. package/dist/renderer/assets/{Page-CpSczAz8.js → Page-DPSdvMgU.js} +1 -1
  14. package/dist/renderer/assets/{ProjectDetail-FJ_bNXLF.js → ProjectDetail-C6EWGvGg.js} +1 -1
  15. package/dist/renderer/assets/{ProjectList-DZKL4K9C.js → ProjectList-BBo5ygnS.js} +1 -1
  16. package/dist/renderer/assets/{Select-BHQk6qCU.js → Select-CNX-Tx_O.js} +2 -7
  17. package/dist/renderer/assets/SessionChat-BIn46jqZ.js +1 -0
  18. package/dist/renderer/assets/SessionList-CZt6f1Rd.js +1 -0
  19. package/dist/renderer/assets/{ThreadChat-D3s4GXGh.js → ThreadChat-BotD0gKQ.js} +7 -7
  20. package/dist/renderer/assets/Workspace-C_4tdpjX.js +14 -0
  21. package/dist/renderer/assets/{arrow-left-Dtal-eOi.js → arrow-left-cHr5wkpq.js} +1 -1
  22. package/dist/renderer/assets/{book-open-q814R_Ov.js → book-open-4nx5C8T-.js} +1 -1
  23. package/dist/renderer/assets/{check-DjfCdLvw.js → check-exTS-R-x.js} +1 -1
  24. package/dist/renderer/assets/chevron-down-DSvxXQa0.js +6 -0
  25. package/dist/renderer/assets/{circle-vfhmyE8v.js → circle-DyvEovdw.js} +1 -1
  26. package/dist/renderer/assets/desktop-B-KVT10D.js +1 -0
  27. package/dist/renderer/assets/desktop-CVibRyAw.js +4 -0
  28. package/dist/renderer/assets/{funnel-CUQibo1I.js → funnel-DA9qVdTy.js} +1 -1
  29. package/dist/renderer/assets/{heart-DG9_b8P1.js → heart-DuEV0ZRJ.js} +1 -1
  30. package/dist/renderer/assets/{index-Ds0IN5rP.js → index-BgflqkCN.js} +3 -3
  31. package/dist/renderer/assets/index-Biz-aexG.js +177 -0
  32. package/dist/renderer/assets/{index-3CzgyQYB.css → index-DFiJR0vv.css} +1 -1
  33. package/dist/renderer/assets/{pencil-Dij0LlRY.js → pencil-DBDxeNmO.js} +1 -1
  34. package/dist/renderer/assets/{play-CPFGjIyJ.js → play-IzlP83_7.js} +1 -1
  35. package/dist/renderer/assets/{plug-DH3RKQyn.js → plug-CuT-qFnW.js} +1 -1
  36. package/dist/renderer/assets/{plus-afkK9_Y8.js → plus-DEqgkn63.js} +1 -1
  37. package/dist/renderer/assets/projects-DDtDN1rc.js +1 -0
  38. package/dist/renderer/assets/{save-B-cKgWcy.js → save-D4sRo07y.js} +1 -1
  39. package/dist/renderer/assets/{search-DR6PeYkt.js → search-DbPZLuV8.js} +1 -1
  40. package/dist/renderer/assets/{server-BM6KOB5a.js → server-D1jUQmwN.js} +1 -1
  41. package/dist/renderer/assets/{session-utils-BDuThM0a.js → session-utils-Aife8ep2.js} +1 -1
  42. package/dist/renderer/assets/{sessions-C1yKps4T.js → sessions-DaH3H7gC.js} +1 -1
  43. package/dist/renderer/assets/{shield-check-BFjMG-r2.js → shield-check-BGwUFWNr.js} +1 -1
  44. package/dist/renderer/assets/{trash-2-CW1heMCf.js → trash-2-CIFBioBa.js} +1 -1
  45. package/dist/renderer/assets/{user-r_2sHG7m.js → user-CnKcvHdo.js} +1 -1
  46. package/dist/renderer/assets/{wifi-off-VMUokMpE.js → wifi-off-ClO53WGa.js} +1 -1
  47. package/dist/renderer/index.html +2 -2
  48. package/dist-electron/electron/local-core-refactor.test.js +812 -3
  49. package/dist-electron/electron/plugin-kernel.test.js +74 -14
  50. package/dist-electron/packages/core-sdk/src/index.js +40 -20
  51. package/dist-electron/services/local-ai-core/src/acp/local-core-acp-turn-coordinator.js +5 -1
  52. package/dist-electron/services/local-ai-core/src/acp/workspace-acp-permissions.js +55 -3
  53. package/dist-electron/services/local-ai-core/src/channel/lark/cards.js +210 -0
  54. package/dist-electron/services/local-ai-core/src/channel/lark/config.js +54 -0
  55. package/dist-electron/services/local-ai-core/src/{gateway → channel/lark}/local-core-lark-gateway.js +365 -383
  56. package/dist-electron/services/local-ai-core/src/channel/lark/registration.js +50 -0
  57. package/dist-electron/services/local-ai-core/src/channel/lark/runtime-state.js +191 -0
  58. package/dist-electron/services/local-ai-core/src/channel/lark/types.js +2 -0
  59. package/dist-electron/services/local-ai-core/src/channel/shared/file-utils.js +33 -0
  60. package/dist-electron/services/local-ai-core/src/channel/shared/plugin.js +31 -0
  61. package/dist-electron/services/local-ai-core/src/channel/weixin/config.js +125 -0
  62. package/dist-electron/services/local-ai-core/src/{gateway → channel/weixin}/local-core-weixin-gateway.js +262 -258
  63. package/dist-electron/services/local-ai-core/src/channel/weixin/transport.js +169 -0
  64. package/dist-electron/services/local-ai-core/src/channel/weixin/types.js +2 -0
  65. package/dist-electron/services/local-ai-core/src/cli/lac.js +41 -1
  66. package/dist-electron/services/local-ai-core/src/kernel/bootstrap.js +5 -0
  67. package/dist-electron/services/local-ai-core/src/plugins/builtin/channel-lark-plugin.js +19 -40
  68. package/dist-electron/services/local-ai-core/src/plugins/builtin/channel-weixin-plugin.js +19 -40
  69. package/dist-electron/services/local-ai-core/src/runtime/local-core-controller.js +96 -44
  70. package/dist-electron/services/local-ai-core/src/runtime/server.js +20 -8
  71. package/dist-electron/shared/desktop.js +24 -1
  72. package/package.json +1 -1
  73. package/dist/renderer/assets/CronList-BYZ-x_bp.js +0 -1
  74. package/dist/renderer/assets/Logs-D7HxZ8Nb.js +0 -1
  75. package/dist/renderer/assets/SessionChat-DpgSXF3m.js +0 -1
  76. package/dist/renderer/assets/SessionList-CU6drQ4N.js +0 -1
  77. package/dist/renderer/assets/Workspace-B7looTVo.js +0 -14
  78. package/dist/renderer/assets/desktop-BPIxWbkM.js +0 -1
  79. package/dist/renderer/assets/desktop-DZHC4PDB.js +0 -3
  80. package/dist/renderer/assets/index-Bn8ane6_.js +0 -177
  81. package/dist/renderer/assets/projects-DTDS9SnT.js +0 -1
@@ -9,13 +9,12 @@ const node_fs_1 = __importDefault(require("node:fs"));
9
9
  const node_path_1 = __importDefault(require("node:path"));
10
10
  const node_events_1 = require("node:events");
11
11
  const node_crypto_2 = require("node:crypto");
12
- const desktop_js_1 = require("../../../../shared/desktop.js");
12
+ const desktop_js_1 = require("../../../../../shared/desktop.js");
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");
13
16
  // ==================== Constants ====================
14
17
  const PAIRING_EXPIRY_MS = 10 * 60 * 1000;
15
- const DEFAULT_BASE_URL = 'https://ilinkai.weixin.qq.com';
16
- const DEFAULT_CDN_BASE_URL = 'https://novac2c.cdn.weixin.qq.com/c2c';
17
- const LONG_POLL_TIMEOUT_MS = 35_000;
18
- const API_TIMEOUT_MS = 15_000;
19
18
  const RETRY_DELAY_MS = 2_000;
20
19
  const BACKOFF_DELAY_MS = 30_000;
21
20
  const MAX_CONSECUTIVE_FAILURES = 3;
@@ -25,14 +24,8 @@ const WEIXIN_CONTEXT_REPLY_MAX_BYTES = 3500;
25
24
  const WEIXIN_CONTEXT_SEND_LIMIT = 10;
26
25
  const WEIXIN_RESERVED_TERMINAL_SENDS = 1;
27
26
  const WEIXIN_PROGRESS_SEND_BUDGET = WEIXIN_CONTEXT_SEND_LIMIT - WEIXIN_RESERVED_TERMINAL_SENDS;
28
- const WEIXIN_CHANNEL_VERSION = '2.1.7';
29
- const WEIXIN_ILINK_APP_ID = 'bot';
30
- const WEIXIN_ILINK_APP_CLIENT_VERSION = '131335';
31
27
  const RESPONSE_TIMEOUT_MS = 5 * 60 * 1000;
32
- const TEXT_ITEM_TYPE = 1;
33
- const IMAGE_ITEM_TYPE = 2;
34
- const VOICE_ITEM_TYPE = 3;
35
- const FILE_ITEM_TYPE = 4;
28
+ const WEIXIN_MAX_UPLOAD_FILE_SIZE = 30 * 1024 * 1024;
36
29
  // ==================== Utilities ====================
37
30
  function stripHtml(html) {
38
31
  let result = html;
@@ -57,18 +50,6 @@ function sleep(ms, signal) {
57
50
  signal?.addEventListener('abort', onAbort, { once: true });
58
51
  });
59
52
  }
60
- function safeFilePart(value) {
61
- return String(value || 'default').replace(/[^a-zA-Z0-9._-]/g, '_').slice(0, 80) || 'default';
62
- }
63
- function createWechatUin() {
64
- return Buffer.from(String(node_crypto_1.default.randomInt(0, 0xffffffff))).toString('base64');
65
- }
66
- function createIlinkHeaders() {
67
- return {
68
- 'iLink-App-Id': WEIXIN_ILINK_APP_ID,
69
- 'iLink-App-ClientVersion': WEIXIN_ILINK_APP_CLIENT_VERSION,
70
- };
71
- }
72
53
  function utf8ByteLength(value) {
73
54
  return Buffer.byteLength(value, 'utf-8');
74
55
  }
@@ -160,21 +141,25 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
160
141
  async refreshBindings() {
161
142
  const config = await this.options.readConfig();
162
143
  const bindings = this.collectBindings(config);
163
- const nextWorkspaceIds = new Set(bindings.map((b) => b.workspaceId));
164
- for (const workspaceId of [...this.runtime.keys()]) {
165
- if (!nextWorkspaceIds.has(workspaceId)) {
166
- 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);
167
148
  }
168
149
  }
169
150
  for (const binding of bindings) {
170
- 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);
171
153
  if (!binding.enabled) {
172
154
  if (current) {
173
- await this.stopWorkspace(binding.workspaceId);
155
+ await this.stopWorkspaceKey(key);
174
156
  }
175
157
  else {
176
- this.runtime.set(binding.workspaceId, {
158
+ this.runtime.set(key, {
177
159
  workspaceId: binding.workspaceId,
160
+ instanceId: binding.instanceId,
161
+ displayName: binding.displayName,
162
+ platformKey: binding.platformKey,
178
163
  enabled: false,
179
164
  status: 'disabled',
180
165
  connected: false,
@@ -190,35 +175,39 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
190
175
  }
191
176
  this.notifyRuntimeStateChanged();
192
177
  }
193
- async testConnection(workspaceId) {
194
- const binding = await this.getBinding(workspaceId);
178
+ async testConnection(workspaceId, instanceId) {
179
+ const binding = await this.getBinding(workspaceId, instanceId);
195
180
  try {
196
- const bufPath = this.getBufPath(binding);
181
+ const bufPath = (0, config_js_1.getWeixinBufPath)(binding);
197
182
  node_fs_1.default.accessSync(bufPath);
198
- return { success: true, platform: 'weixin', workspaceId, appId: binding.accountId };
183
+ return { success: true, platform: 'weixin', workspaceId, instanceId: binding.instanceId, appId: binding.accountId };
199
184
  }
200
185
  catch {
201
- 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.' };
202
187
  }
203
188
  }
204
- async enable(workspaceId) {
205
- const binding = await this.getBinding(workspaceId);
189
+ async enable(workspaceId, instanceId) {
190
+ const binding = await this.getBinding(workspaceId, instanceId);
206
191
  await this.startWorkspace(binding);
207
- return this.getStatus(workspaceId);
192
+ return this.getStatus(workspaceId, binding.instanceId);
208
193
  }
209
- async disable(workspaceId) {
210
- await this.stopWorkspace(workspaceId);
211
- return this.getStatus(workspaceId);
194
+ async disable(workspaceId, instanceId) {
195
+ await this.stopWorkspace(workspaceId, instanceId);
196
+ return this.getStatus(workspaceId, instanceId);
212
197
  }
213
- getStatus(workspaceId) {
198
+ getStatus(workspaceId, instanceId) {
214
199
  this.options.store.expirePendingPairings();
215
- 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);
216
203
  const pairings = this.options.store.listPendingPairings(workspaceId)
217
- .filter((row) => row.platform === 'weixin' && row.expires_at >= new Date().toISOString());
218
- 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);
219
206
  return {
220
207
  workspaceId,
221
208
  platform: 'weixin',
209
+ instanceId: resolved.instanceId,
210
+ displayName: binding?.displayName,
222
211
  enabled: Boolean(binding?.enabled),
223
212
  connected: Boolean(binding?.connected),
224
213
  status: binding?.status || 'disabled',
@@ -230,16 +219,20 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
230
219
  };
231
220
  }
232
221
  listStatuses() {
233
- 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));
234
225
  }
235
226
  listPendingPairings(workspaceId) {
236
227
  this.options.store.expirePendingPairings();
237
228
  return this.options.store
238
- .listPairingRequests(workspaceId, 'weixin')
229
+ .listPairingRequests(workspaceId)
230
+ .filter((item) => item.platform === 'weixin' || item.platform.startsWith('weixin:'))
239
231
  .filter((item) => item.status === 'pending' && item.expiresAt >= new Date().toISOString());
240
232
  }
241
233
  listAuthorizedUsers(workspaceId) {
242
- return this.options.store.listAuthorizedUsers(workspaceId, 'weixin');
234
+ return this.options.store.listAuthorizedUsers(workspaceId)
235
+ .filter((item) => item.platform === 'weixin' || item.platform.startsWith('weixin:'));
243
236
  }
244
237
  async start() {
245
238
  await this.refreshBindings();
@@ -248,12 +241,12 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
248
241
  this.close();
249
242
  }
250
243
  close() {
251
- 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);
252
245
  }
253
246
  // ==================== QR Code Login ====================
254
- async getQrCode(workspaceId) {
255
- const binding = await this.getBinding(workspaceId);
256
- 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`);
257
250
  if (data.errcode && data.errcode !== 0) {
258
251
  throw new Error(`Failed to get QR code: ${data.errmsg || data.errcode}`);
259
252
  }
@@ -261,11 +254,11 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
261
254
  throw new Error('No QR code returned from iLink');
262
255
  }
263
256
  const qrCodeUrl = data.qrcode_img_content || `${binding.baseUrl.replace(/\/$/, '')}/ilink/bot/qr_code/${data.qrcode}`;
264
- 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 };
265
258
  }
266
- async checkQrCodeStatus(workspaceId, ticket) {
267
- const binding = await this.getBinding(workspaceId);
268
- 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)}`);
269
262
  if (data.errcode && data.errcode !== 0) {
270
263
  throw new Error(`Failed to check QR code status: ${data.errmsg || data.errcode}`);
271
264
  }
@@ -274,14 +267,14 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
274
267
  if (!data.bot_token) {
275
268
  throw new Error('WeChat QR code confirmed but no bot token was returned.');
276
269
  }
277
- this.saveCredentials(binding, {
270
+ (0, config_js_1.saveWeixinCredentials)(binding, {
278
271
  token: data.bot_token,
279
272
  baseUrl: data.baseurl || binding.baseUrl,
280
273
  botId: data.ilink_bot_id,
281
274
  userId: data.ilink_user_id || data.user_id,
282
275
  savedAt: new Date().toISOString(),
283
276
  });
284
- await this.startWorkspace(await this.getBinding(workspaceId));
277
+ await this.startWorkspace(await this.getBinding(workspaceId, binding.instanceId));
285
278
  }
286
279
  if (!['wait', 'signed', 'confirmed', 'expired'].includes(status)) {
287
280
  return { status: 'wait' };
@@ -298,7 +291,7 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
298
291
  const pairing = this.options.store.getPairingRequest(code);
299
292
  if (!pairing)
300
293
  throw new Error(`Pairing code not found: ${code}`);
301
- if (pairing.platform !== 'weixin')
294
+ if (pairing.platform !== 'weixin' && !pairing.platform.startsWith('weixin:'))
302
295
  throw new Error(`Pairing code ${code} is not a WeChat pairing`);
303
296
  if (pairing.status !== 'pending')
304
297
  throw new Error(`Pairing code ${code} is already ${pairing.status}`);
@@ -306,13 +299,13 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
306
299
  this.options.store.updatePairingStatus(code, 'expired');
307
300
  throw new Error(`Pairing code ${code} has expired`);
308
301
  }
309
- 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);
310
303
  const userId = existing?.id || `wx-user-${(0, node_crypto_2.randomUUID)()}`;
311
304
  const authorizedAt = new Date().toISOString();
312
305
  this.options.store.createAuthorizedUser({
313
306
  id: userId,
314
307
  workspace_id: pairing.workspace_id,
315
- platform: 'weixin',
308
+ platform: pairing.platform,
316
309
  platform_user_id: pairing.platform_user_id,
317
310
  chat_id: pairing.chat_id,
318
311
  display_name: pairing.display_name,
@@ -321,7 +314,7 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
321
314
  });
322
315
  this.options.store.updatePairingStatus(code, 'approved');
323
316
  this.notifyRuntimeStateChanged();
324
- 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);
325
318
  if (!user)
326
319
  throw new Error('Authorized user lookup failed after approval');
327
320
  return user;
@@ -330,7 +323,7 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
330
323
  const pairing = this.options.store.getPairingRequest(code);
331
324
  if (!pairing)
332
325
  throw new Error(`Pairing code not found: ${code}`);
333
- if (pairing.platform !== 'weixin')
326
+ if (pairing.platform !== 'weixin' && !pairing.platform.startsWith('weixin:'))
334
327
  throw new Error(`Pairing code ${code} is not a WeChat pairing`);
335
328
  this.options.store.updatePairingStatus(code, 'rejected');
336
329
  this.notifyRuntimeStateChanged();
@@ -360,12 +353,14 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
360
353
  this.options.log?.(`localcore-weixin bridge route miss for sessionKey=${sessionKey} type=${event.type}`);
361
354
  return;
362
355
  }
363
- 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);
364
359
  if (!state?.connected) {
365
360
  this.options.log?.(`localcore-weixin bridge event ignored because workspace is not connected: ${route.workspaceId}`);
366
361
  return;
367
362
  }
368
- 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);
369
364
  if (!initialBinding) {
370
365
  this.options.log?.(`localcore-weixin bridge binding miss for workspace=${route.workspaceId}`);
371
366
  return;
@@ -383,7 +378,7 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
383
378
  const current = previous
384
379
  .catch(() => undefined)
385
380
  .then(async () => {
386
- 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);
387
382
  if (!binding)
388
383
  return;
389
384
  if (this.mutedThreadBridgeCounts.has(binding.thread_id))
@@ -438,7 +433,7 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
438
433
  return this.sendScheduledMessage(workspaceId, { type: 'channel.chat', channelId: chatId }, text);
439
434
  }
440
435
  async sendScheduledMessage(workspaceId, route, text) {
441
- const state = this.runtime.get(workspaceId);
436
+ const state = this.resolveRuntimeState(workspaceId, route.instanceId).state;
442
437
  if (!state?.connected) {
443
438
  this.options.log?.(`localcore-weixin scheduled message skipped: workspace not connected: ${workspaceId}`);
444
439
  return '';
@@ -453,8 +448,81 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
453
448
  return '';
454
449
  }
455
450
  }
451
+ async sendOutboundMessage(workspaceId, input) {
452
+ const state = this.resolveRuntimeState(workspaceId, input.route?.instanceId).state;
453
+ if (!state?.connected) {
454
+ throw new Error(`WeChat workspace is not connected: ${workspaceId}`);
455
+ }
456
+ const channelId = String(input.route?.channelId || '').trim();
457
+ if (!channelId) {
458
+ throw new Error('Missing WeChat target channel id');
459
+ }
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);
463
+ const messageIds = [];
464
+ const attachments = [];
465
+ for (const part of input.parts || []) {
466
+ if (part.type === 'text') {
467
+ const text = String(part.text || '').trim();
468
+ if (text) {
469
+ await this.sendTextMessage(state, channelId, text, contextToken);
470
+ messageIds.push(`wx_text_${(0, node_crypto_2.randomUUID)()}`);
471
+ }
472
+ continue;
473
+ }
474
+ if (part.type === 'file') {
475
+ const sent = await this.sendFilePart(binding, channelId, part, contextToken);
476
+ messageIds.push(sent.messageId);
477
+ attachments.push({
478
+ kind: 'file',
479
+ attachmentId: sent.uploaded.fileKey,
480
+ fileName: sent.file.fileName,
481
+ fileSize: sent.file.fileSize,
482
+ metadata: {
483
+ fileKey: sent.uploaded.fileKey,
484
+ encryptedQueryParam: sent.uploaded.encryptedQueryParam,
485
+ },
486
+ });
487
+ }
488
+ }
489
+ return {
490
+ platform: 'weixin',
491
+ workspaceId,
492
+ channelId,
493
+ participantId: input.route.participantId,
494
+ messageIds,
495
+ attachments,
496
+ };
497
+ }
498
+ async sendFile(workspaceId, input) {
499
+ const result = await this.sendOutboundMessage(workspaceId, {
500
+ route: {
501
+ type: 'channel.chat',
502
+ channelId: input.channelId,
503
+ participantId: input.participantId,
504
+ },
505
+ parts: [{
506
+ type: 'file',
507
+ path: input.path,
508
+ fileName: input.fileName,
509
+ }],
510
+ });
511
+ const attachment = result.attachments?.[0];
512
+ return {
513
+ platform: 'weixin',
514
+ workspaceId,
515
+ channelId: result.channelId,
516
+ messageId: result.messageIds[0] || '',
517
+ fileKey: String(attachment?.metadata?.fileKey || attachment?.attachmentId || ''),
518
+ fileName: attachment?.fileName || input.fileName || '',
519
+ fileSize: attachment?.fileSize || 0,
520
+ };
521
+ }
456
522
  // ==================== Inbound Message Handling ====================
457
523
  async handleInboundMessage(input) {
524
+ const instanceId = input.instanceId || 'default';
525
+ const platformKey = input.platformKey || (0, config_js_1.channelPlatformKey)('weixin', instanceId);
458
526
  if (this.isDuplicateInboundMessage(input)) {
459
527
  this.options.log?.(`localcore-weixin skipped duplicate inbound message workspace=${input.workspaceId} chat=${input.chatId} id=${input.contextToken || input.messageId}`);
460
528
  return { paired: true, duplicate: true };
@@ -472,28 +540,28 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
472
540
  },
473
541
  });
474
542
  this.options.store.expirePendingPairings();
475
- const binding = await this.getBinding(input.workspaceId);
476
- 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);
477
545
  if (!authorized) {
478
546
  if (binding.allowFrom === '*') {
479
547
  const authorizedAt = new Date().toISOString();
480
548
  this.options.store.createAuthorizedUser({
481
549
  id: `wx-user-${(0, node_crypto_2.randomUUID)()}`,
482
550
  workspace_id: input.workspaceId,
483
- platform: 'weixin',
551
+ platform: platformKey,
484
552
  platform_user_id: input.platformUserId,
485
553
  chat_id: input.chatId,
486
554
  display_name: input.displayName,
487
555
  thread_id: null,
488
556
  authorized_at: authorizedAt,
489
557
  });
490
- authorized = this.options.store.getAuthorizedUser(input.workspaceId, input.platformUserId, 'weixin');
558
+ authorized = this.options.store.getAuthorizedUser(input.workspaceId, input.platformUserId, platformKey);
491
559
  this.options.log?.(`localcore-weixin auto-approved user for ${input.workspaceId}: ${input.platformUserId}`);
492
560
  this.notifyRuntimeStateChanged();
493
561
  }
494
562
  }
495
563
  if (!authorized) {
496
- 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');
497
565
  let pairingCode = existingPending?.code || '';
498
566
  if (!existingPending) {
499
567
  const now = new Date();
@@ -501,7 +569,7 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
501
569
  this.options.store.createPairingRequest({
502
570
  code: pairingCode,
503
571
  workspace_id: input.workspaceId,
504
- platform: 'weixin',
572
+ platform: platformKey,
505
573
  platform_user_id: input.platformUserId,
506
574
  chat_id: input.chatId,
507
575
  display_name: input.displayName,
@@ -511,23 +579,23 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
511
579
  });
512
580
  this.notifyRuntimeStateChanged();
513
581
  }
514
- const state = this.runtime.get(input.workspaceId);
582
+ const state = this.runtime.get((0, config_js_1.runtimeKey)(input.workspaceId, instanceId));
515
583
  if (state?.connected) {
516
584
  await this.sendTextMessage(state, input.chatId, `**已收到消息**\n\n当前账号还未授权接入这个工作区。\n请在桌面端完成审批后再次发送消息。\n\n配对码:\`${pairingCode}\``, input.contextToken);
517
585
  }
518
586
  return { paired: false };
519
587
  }
520
588
  const router = this.options.getWorkspaceRouter();
521
- 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);
522
590
  let threadId = threadBinding?.thread_id || authorized.thread_id || '';
523
591
  if (!threadId) {
524
592
  const thread = await router.createThread(input.workspaceId, input.displayName || `WeChat ${input.chatId}`);
525
593
  threadId = thread.id;
526
- this.options.store.updateAuthorizedUserThread(input.workspaceId, input.platformUserId, threadId, 'weixin');
594
+ this.options.store.updateAuthorizedUserThread(input.workspaceId, input.platformUserId, threadId, platformKey);
527
595
  const now = new Date().toISOString();
528
596
  this.options.store.upsertPlatformThreadBinding({
529
597
  workspace_id: input.workspaceId,
530
- platform: 'weixin',
598
+ platform: platformKey,
531
599
  chat_id: input.chatId,
532
600
  platform_user_id: input.platformUserId,
533
601
  thread_id: threadId,
@@ -538,13 +606,15 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
538
606
  }
539
607
  const sessionKey = router.getThreadSessionKey(threadId);
540
608
  const normalizedText = String(input.text || '').trim().toLowerCase();
541
- 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) : '';
542
610
  if (permissionThreadId && permissionThreadId !== threadId) {
543
611
  threadId = permissionThreadId;
544
612
  }
545
613
  const effectiveSessionKey = router.getThreadSessionKey(threadId);
546
614
  this.threadRouting.set(effectiveSessionKey, {
547
615
  workspaceId: input.workspaceId,
616
+ instanceId,
617
+ platformKey,
548
618
  platformUserId: input.platformUserId,
549
619
  chatId: input.chatId,
550
620
  threadId,
@@ -555,10 +625,10 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
555
625
  const title = slashCommand.args.join(' ').trim() || `${input.displayName || 'WeChat'} ${new Date().toLocaleTimeString()}`;
556
626
  const nextThread = await router.createThread(input.workspaceId, title);
557
627
  const now = new Date().toISOString();
558
- this.options.store.updateAuthorizedUserThread(input.workspaceId, input.platformUserId, nextThread.id, 'weixin');
628
+ this.options.store.updateAuthorizedUserThread(input.workspaceId, input.platformUserId, nextThread.id, platformKey);
559
629
  this.options.store.upsertPlatformThreadBinding({
560
630
  workspace_id: input.workspaceId,
561
- platform: 'weixin',
631
+ platform: platformKey,
562
632
  chat_id: input.chatId,
563
633
  platform_user_id: input.platformUserId,
564
634
  thread_id: nextThread.id,
@@ -568,11 +638,13 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
568
638
  });
569
639
  this.threadRouting.set(router.getThreadSessionKey(nextThread.id), {
570
640
  workspaceId: input.workspaceId,
641
+ instanceId,
642
+ platformKey,
571
643
  platformUserId: input.platformUserId,
572
644
  chatId: input.chatId,
573
645
  threadId: nextThread.id,
574
646
  });
575
- const st = this.runtime.get(input.workspaceId);
647
+ const st = this.runtime.get((0, config_js_1.runtimeKey)(input.workspaceId, instanceId));
576
648
  if (st?.connected) {
577
649
  await this.sendTextMessage(st, input.chatId, '**已开始新会话**', input.contextToken);
578
650
  }
@@ -584,70 +656,49 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
584
656
  await router.sendThreadAction(threadId, input.text);
585
657
  return { paired: true, threadId };
586
658
  }
587
- 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);
588
660
  const wrappedText = (0, desktop_js_1.wrapUserMessageWithSchedulerProtocol)(input.text);
589
661
  await router.sendThreadMessage(threadId, this.createThreadMessageInput(wrappedText, input.contentParts));
590
662
  return { paired: true, threadId };
591
663
  }
592
664
  // ==================== Private: Bindings ====================
593
- 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) {
594
675
  const config = await this.options.readConfig();
595
- 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];
596
680
  if (!binding)
597
- throw new Error(`No WeChat binding configured for workspace "${workspaceId}"`);
681
+ throw new Error(`No WeChat binding configured for workspace "${workspaceId}"${instanceId ? ` instance "${instanceId}"` : ''}`);
598
682
  return binding;
599
683
  }
600
684
  collectBindings(config) {
601
- const projects = Array.isArray(config?.projects) ? config.projects : [];
602
- return projects.flatMap((project) => {
603
- const platforms = Array.isArray(project.platforms) ? project.platforms : [];
604
- return platforms
605
- .map((platform) => ({
606
- platformType: (0, desktop_js_1.normalizeDesktopPlatformType)(platform?.type),
607
- options: platform?.options && typeof platform.options === 'object'
608
- ? platform.options
609
- : {},
610
- }))
611
- .filter((p) => p.platformType === 'weixin')
612
- .map((p) => {
613
- const stateDir = String(p.options.state_dir || this.getDefaultStateDir()).trim();
614
- const credentials = this.loadCredentials(project.name, stateDir);
615
- const configuredToken = String(p.options.token || '').trim();
616
- const configuredBaseUrl = String(p.options.base_url || '').trim();
617
- const accountId = String(p.options.account_id || credentials?.botId || 'qr-login').trim();
618
- return {
619
- workspaceId: project.name,
620
- token: configuredToken || credentials?.token || '',
621
- accountId,
622
- baseUrl: configuredBaseUrl || credentials?.baseUrl || DEFAULT_BASE_URL,
623
- cdnBaseUrl: String(p.options.cdn_base_url || DEFAULT_CDN_BASE_URL).trim(),
624
- allowFrom: String(p.options.allow_from || '*').trim(),
625
- routeTag: String(p.options.route_tag || '').trim(),
626
- longPollTimeoutMs: Number(p.options.long_poll_timeout_ms || LONG_POLL_TIMEOUT_MS) || LONG_POLL_TIMEOUT_MS,
627
- stateDir,
628
- proxy: String(p.options.proxy || '').trim(),
629
- proxyUsername: String(p.options.proxy_username || '').trim(),
630
- proxyPassword: String(p.options.proxy_password || '').trim(),
631
- enabled: true,
632
- project,
633
- };
634
- });
635
- });
636
- }
637
- getDefaultStateDir() {
638
- return node_path_1.default.join(process.cwd(), 'weixin-monitor');
685
+ return (0, config_js_1.collectWeixinWorkspaceBindings)(config);
639
686
  }
640
687
  // ==================== Private: Workspace Lifecycle ====================
641
688
  async startWorkspace(binding) {
642
- await this.stopWorkspace(binding.workspaceId);
689
+ const key = (0, config_js_1.runtimeKey)(binding.workspaceId, binding.instanceId);
690
+ await this.stopWorkspaceKey(key);
643
691
  const status = {
644
692
  workspaceId: binding.workspaceId,
693
+ instanceId: binding.instanceId,
694
+ displayName: binding.displayName,
695
+ platformKey: binding.platformKey,
645
696
  enabled: true,
646
697
  status: 'starting',
647
698
  connected: false,
648
699
  accountId: binding.accountId,
649
700
  };
650
- this.runtime.set(binding.workspaceId, status);
701
+ this.runtime.set(key, status);
651
702
  this.notifyRuntimeStateChanged();
652
703
  try {
653
704
  const abortController = new AbortController();
@@ -682,16 +733,23 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
682
733
  }
683
734
  this.notifyRuntimeStateChanged();
684
735
  }
685
- async stopWorkspace(workspaceId) {
686
- 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);
687
742
  if (!state)
688
743
  return;
689
744
  try {
690
745
  state.abortController?.abort();
691
746
  }
692
747
  catch { }
693
- this.runtime.set(workspaceId, {
694
- workspaceId,
748
+ this.runtime.set(key, {
749
+ workspaceId: state.workspaceId,
750
+ instanceId: state.instanceId,
751
+ displayName: state.displayName,
752
+ platformKey: state.platformKey,
695
753
  enabled: false,
696
754
  status: 'stopped',
697
755
  connected: false,
@@ -700,35 +758,6 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
700
758
  this.notifyRuntimeStateChanged();
701
759
  }
702
760
  // ==================== Private: Long-poll Monitor ====================
703
- getBufPath(binding) {
704
- return node_path_1.default.join(binding.stateDir, `${binding.accountId}.buf`);
705
- }
706
- getCredentialsPath(workspaceId, stateDir) {
707
- return node_path_1.default.join(stateDir, `${safeFilePart(workspaceId)}.credentials.json`);
708
- }
709
- loadCredentials(workspaceId, stateDir) {
710
- try {
711
- const raw = node_fs_1.default.readFileSync(this.getCredentialsPath(workspaceId, stateDir), 'utf-8');
712
- const parsed = JSON.parse(raw);
713
- const token = String(parsed.token || '').trim();
714
- if (!token)
715
- return null;
716
- return {
717
- token,
718
- baseUrl: parsed.baseUrl ? String(parsed.baseUrl) : undefined,
719
- botId: parsed.botId ? String(parsed.botId) : undefined,
720
- userId: parsed.userId ? String(parsed.userId) : undefined,
721
- savedAt: parsed.savedAt ? String(parsed.savedAt) : new Date().toISOString(),
722
- };
723
- }
724
- catch {
725
- return null;
726
- }
727
- }
728
- saveCredentials(binding, credentials) {
729
- node_fs_1.default.mkdirSync(binding.stateDir, { recursive: true });
730
- node_fs_1.default.writeFileSync(this.getCredentialsPath(binding.workspaceId, binding.stateDir), JSON.stringify(credentials, null, 2), 'utf-8');
731
- }
732
761
  normalizeQrStatus(status) {
733
762
  const normalized = String(status || 'wait').trim().toLowerCase();
734
763
  if (normalized === 'scaned' || normalized === 'scanned' || normalized === 'signed')
@@ -739,32 +768,19 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
739
768
  return 'expired';
740
769
  return 'wait';
741
770
  }
742
- loadBuf(binding) {
743
- try {
744
- return node_fs_1.default.readFileSync(this.getBufPath(binding), 'utf-8');
745
- }
746
- catch {
747
- return '';
748
- }
749
- }
750
- saveBuf(binding, buf) {
751
- const dir = binding.stateDir;
752
- node_fs_1.default.mkdirSync(dir, { recursive: true });
753
- node_fs_1.default.writeFileSync(this.getBufPath(binding), buf, 'utf-8');
754
- }
755
771
  async runMonitorLoop(binding, signal) {
756
- let buf = this.loadBuf(binding);
772
+ let buf = (0, config_js_1.loadWeixinBuf)(binding);
757
773
  let consecutiveFailures = 0;
758
774
  while (!signal.aborted) {
759
775
  try {
760
- const resp = await this.getUpdates(binding, buf, signal);
776
+ const resp = await (0, transport_js_1.getWeixinUpdates)(binding, buf, signal);
761
777
  const isApiError = (resp.ret !== undefined && resp.ret !== 0) || (resp.errcode !== undefined && resp.errcode !== 0);
762
778
  if (isApiError) {
763
779
  consecutiveFailures++;
764
780
  const errorText = resp.errmsg ? ` errmsg=${resp.errmsg}` : '';
765
781
  this.options.log?.(`localcore-weixin getUpdates failed for ${binding.workspaceId}: ret=${resp.ret} errcode=${resp.errcode}${errorText} (${consecutiveFailures}/${MAX_CONSECUTIVE_FAILURES})`);
766
782
  if (resp.errcode === -14 || resp.ret === -14) {
767
- const state = this.runtime.get(binding.workspaceId);
783
+ const state = this.runtime.get((0, config_js_1.runtimeKey)(binding.workspaceId, binding.instanceId));
768
784
  if (state) {
769
785
  state.status = 'error';
770
786
  state.connected = false;
@@ -784,13 +800,13 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
784
800
  consecutiveFailures = 0;
785
801
  if (resp.get_updates_buf) {
786
802
  buf = resp.get_updates_buf;
787
- this.saveBuf(binding, buf);
803
+ (0, config_js_1.saveWeixinBuf)(binding, buf);
788
804
  }
789
805
  for (const msg of resp.msgs ?? []) {
790
806
  const items = msg.item_list ?? [];
791
- const textItem = items.find((i) => i.type === TEXT_ITEM_TYPE);
792
- const voiceTextItems = items.filter((i) => i.type === VOICE_ITEM_TYPE && i.voice_item?.text);
793
- 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);
794
810
  if (!textItem && voiceTextItems.length === 0 && mediaItems.length === 0)
795
811
  continue;
796
812
  const conversationId = msg.from_user_id ?? '';
@@ -829,6 +845,8 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
829
845
  continue;
830
846
  await this.handleInboundMessage({
831
847
  workspaceId: binding.workspaceId,
848
+ instanceId: binding.instanceId,
849
+ platformKey: binding.platformKey,
832
850
  platformUserId: conversationId,
833
851
  chatId: conversationId,
834
852
  displayName: conversationId.slice(-6),
@@ -858,65 +876,6 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
858
876
  }
859
877
  }
860
878
  // ==================== Private: HTTP API ====================
861
- async apiPost(binding, endpoint, bodyObj, timeoutMs, signal) {
862
- const url = `${binding.baseUrl.replace(/\/$/, '')}/${endpoint}`;
863
- const body = JSON.stringify(bodyObj);
864
- const controller = new AbortController();
865
- const timer = setTimeout(() => controller.abort(), timeoutMs);
866
- const onAbort = () => controller.abort();
867
- signal?.addEventListener('abort', onAbort, { once: true });
868
- try {
869
- const headers = {
870
- 'Content-Type': 'application/json',
871
- 'Content-Length': String(Buffer.byteLength(body, 'utf-8')),
872
- 'X-WECHAT-UIN': createWechatUin(),
873
- ...createIlinkHeaders(),
874
- };
875
- if (binding.token) {
876
- headers.AuthorizationType = 'ilink_bot_token';
877
- headers.Authorization = `Bearer ${binding.token}`;
878
- }
879
- const res = await fetch(url, {
880
- method: 'POST',
881
- headers,
882
- body,
883
- signal: controller.signal,
884
- });
885
- if (!res.ok)
886
- throw new Error(`HTTP ${res.status}`);
887
- return (await res.json());
888
- }
889
- finally {
890
- clearTimeout(timer);
891
- signal?.removeEventListener('abort', onAbort);
892
- }
893
- }
894
- async apiGet(binding, endpoint, timeoutMs = API_TIMEOUT_MS) {
895
- const url = `${binding.baseUrl.replace(/\/$/, '')}/${endpoint}`;
896
- const controller = new AbortController();
897
- const timer = setTimeout(() => controller.abort(), timeoutMs);
898
- try {
899
- const headers = createIlinkHeaders();
900
- if (binding.token) {
901
- headers.AuthorizationType = 'ilink_bot_token';
902
- headers.Authorization = `Bearer ${binding.token}`;
903
- }
904
- const res = await fetch(url, {
905
- method: 'GET',
906
- headers,
907
- signal: controller.signal,
908
- });
909
- if (!res.ok)
910
- throw new Error(`HTTP ${res.status}`);
911
- return (await res.json());
912
- }
913
- finally {
914
- clearTimeout(timer);
915
- }
916
- }
917
- async getUpdates(binding, buf, signal) {
918
- return this.apiPost(binding, 'ilink/bot/getupdates', { get_updates_buf: buf, base_info: { channel_version: WEIXIN_CHANNEL_VERSION } }, binding.longPollTimeoutMs, signal);
919
- }
920
879
  async sendTextMessage(state, toUserId, text, contextToken, options = {}) {
921
880
  const binding = await this.getBinding(state.workspaceId);
922
881
  const stripped = stripHtml(text);
@@ -925,32 +884,77 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
925
884
  : splitTextByUtf8Bytes(stripped, WEIXIN_TEXT_MESSAGE_MAX_BYTES);
926
885
  for (const [index, chunk] of chunks.entries()) {
927
886
  const finalChunk = options.final && index === chunks.length - 1;
928
- const resp = await this.sendTextMessageChunk(binding, toUserId, chunk, contextToken, {
887
+ const resp = await (0, transport_js_1.sendWeixinTextMessageChunk)(binding, toUserId, chunk, contextToken, {
929
888
  clientId: options.clientId,
930
889
  final: finalChunk,
931
890
  });
932
- if (this.isSendMessageError(resp)) {
891
+ if ((0, transport_js_1.isWeixinApiError)(resp)) {
933
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}`);
934
893
  }
935
894
  }
936
895
  this.options.log?.(`localcore-weixin sent message to ${toUserId} for workspace ${state.workspaceId}${chunks.length > 1 ? ` chunks=${chunks.length}` : ''}`);
937
896
  }
938
- async sendTextMessageChunk(binding, toUserId, text, contextToken, options = {}) {
939
- return this.apiPost(binding, 'ilink/bot/sendmessage', {
940
- msg: {
941
- from_user_id: '',
942
- to_user_id: toUserId,
943
- client_id: options.clientId || `openclaw-weixin-${node_crypto_1.default.randomUUID()}`,
944
- message_type: 2,
945
- message_state: options.final === false ? 1 : 2,
946
- item_list: [{ type: TEXT_ITEM_TYPE, text_item: { text } }],
947
- ...(contextToken ? { context_token: contextToken } : {}),
948
- },
949
- base_info: { channel_version: WEIXIN_CHANNEL_VERSION },
950
- }, API_TIMEOUT_MS);
897
+ async sendFileMessage(binding, toUserId, fileName, uploaded, contextToken) {
898
+ const clientId = `openclaw-weixin-${node_crypto_1.default.randomUUID()}`;
899
+ const resp = await (0, transport_js_1.sendWeixinFileMessage)(binding, toUserId, fileName, uploaded, contextToken, { clientId });
900
+ if ((0, transport_js_1.isWeixinApiError)(resp)) {
901
+ throw new Error(`WeChat send file failed: ret=${resp.ret} errcode=${resp.errcode}${resp.errmsg ? ` errmsg=${resp.errmsg}` : ''}`);
902
+ }
903
+ return clientId;
951
904
  }
952
- isSendMessageError(resp) {
953
- return (resp.ret !== undefined && resp.ret !== 0) || (resp.errcode !== undefined && resp.errcode !== 0);
905
+ async sendFilePart(binding, channelId, part, contextToken) {
906
+ const file = await (0, file_utils_js_1.prepareChannelFile)({
907
+ path: part.path,
908
+ fileName: part.fileName,
909
+ maxBytes: WEIXIN_MAX_UPLOAD_FILE_SIZE,
910
+ platformLabel: 'WeChat',
911
+ });
912
+ const uploaded = await this.uploadFileToCdn(binding, channelId, file.path);
913
+ const messageId = await this.sendFileMessage(binding, channelId, file.fileName, uploaded, contextToken);
914
+ this.options.log?.(`localcore-weixin sent file ${file.fileName} (${file.fileSize} bytes) to ${channelId}`);
915
+ return {
916
+ messageId,
917
+ uploaded,
918
+ file,
919
+ };
920
+ }
921
+ async uploadFileToCdn(binding, toUserId, filePath) {
922
+ const plaintext = await node_fs_1.default.promises.readFile(filePath);
923
+ const fileKey = node_crypto_1.default.randomBytes(16).toString('hex');
924
+ const aesKey = node_crypto_1.default.randomBytes(16);
925
+ const aesKeyHex = aesKey.toString('hex');
926
+ const rawMd5 = node_crypto_1.default.createHash('md5').update(plaintext).digest('hex');
927
+ const cipherSize = this.getAesEcbPaddedSize(plaintext.length);
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) {
930
+ throw new Error(`WeChat getuploadurl failed: ret=${uploadUrlResp.ret} errcode=${uploadUrlResp.errcode}${uploadUrlResp.errmsg ? ` errmsg=${uploadUrlResp.errmsg}` : ''}`);
931
+ }
932
+ const encryptedQueryParam = await (0, transport_js_1.uploadEncryptedBufferToWeixinCdn)(binding, plaintext, uploadUrlResp.upload_param, fileKey, aesKey);
933
+ return {
934
+ fileKey,
935
+ encryptedQueryParam,
936
+ aesKeyHex,
937
+ fileSize: plaintext.length,
938
+ cipherSize,
939
+ };
940
+ }
941
+ getAesEcbPaddedSize(size) {
942
+ return Math.ceil((size + 1) / 16) * 16;
943
+ }
944
+ resolveContextTokenForFileSend(workspaceId, channelId, participantId, platformKey = 'weixin') {
945
+ const preferredParticipantId = String(participantId || '').trim();
946
+ const direct = preferredParticipantId
947
+ ? this.options.store.getPlatformThreadBinding(workspaceId, channelId, preferredParticipantId, platformKey)
948
+ : undefined;
949
+ if (direct?.last_platform_message_id) {
950
+ return direct.last_platform_message_id;
951
+ }
952
+ const users = this.options.store.listAuthorizedUsers(workspaceId, platformKey);
953
+ const user = users.find((entry) => entry.chatId === channelId || entry.platformUserId === channelId);
954
+ if (!user) {
955
+ return '';
956
+ }
957
+ return this.options.store.getPlatformThreadBinding(workspaceId, user.chatId, user.platformUserId, platformKey)?.last_platform_message_id || '';
954
958
  }
955
959
  // ==================== Private: Attachment Download ====================
956
960
  sniffExtAndKind(buf) {
@@ -998,7 +1002,7 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
998
1002
  resultBuf = rawBuf;
999
1003
  }
1000
1004
  const { ext, kind } = this.sniffExtAndKind(resultBuf);
1001
- 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'));
1002
1006
  const safeName = declaredName.replace(/[^a-zA-Z0-9._-]/g, '_').slice(0, 64);
1003
1007
  const safeMsgId = msgId.replace(/[^a-zA-Z0-9._-]/g, '_').slice(0, 48);
1004
1008
  const fileName = `${safeMsgId}-${idx}-${safeName}${ext}`;
@@ -1174,9 +1178,9 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
1174
1178
  this.processedInboundMessages.set(key, now + PROCESSED_MESSAGE_TTL_MS);
1175
1179
  return false;
1176
1180
  }
1177
- findAwaitingPermissionThreadId(workspaceId, chatId, platformUserId) {
1181
+ findAwaitingPermissionThreadId(workspaceId, chatId, platformUserId, platformKey = 'weixin') {
1178
1182
  for (const [sessionKey, route] of this.threadRouting.entries()) {
1179
- if (route.workspaceId !== workspaceId || route.chatId !== chatId || route.platformUserId !== platformUserId)
1183
+ if (route.workspaceId !== workspaceId || route.platformKey !== platformKey || route.chatId !== chatId || route.platformUserId !== platformUserId)
1180
1184
  continue;
1181
1185
  const turn = this.outboundTurns.get(sessionKey);
1182
1186
  if (turn?.awaitingPermission && route.threadId)