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