@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.
- package/README.md +9 -0
- package/dist/renderer/assets/{Badge-BRmQIqbd.js → Badge-C2K5apfV.js} +1 -1
- package/dist/renderer/assets/{Chat-D94ODABB.js → Chat-DI2YeVKb.js} +1 -1
- package/dist/renderer/assets/{ChatMarkdown-DqW7OUvV.js → ChatMarkdown-DMAr15Oi.js} +1 -1
- package/dist/renderer/assets/{Config-9lgzpTCp.js → Config-7rP3EuUf.js} +2 -2
- package/dist/renderer/assets/CronList-81QslYE8.js +1 -0
- package/dist/renderer/assets/{Dashboard-COISXbQy.js → Dashboard-Db2T7rSZ.js} +2 -2
- package/dist/renderer/assets/{EmptyState-w5-I9zg7.js → EmptyState-BBJ3cTvB.js} +1 -1
- package/dist/renderer/assets/{KnowledgeDetail-DVxwrTLw.js → KnowledgeDetail-Dz_6flG3.js} +2 -2
- package/dist/renderer/assets/{KnowledgeHome-BKkubcwi.js → KnowledgeHome-DIh046mN.js} +3 -3
- package/dist/renderer/assets/Logs-BXBtHJLf.js +1 -0
- package/dist/renderer/assets/{Modal-Ceu_MP2e.js → Modal-BB2Av_Xs.js} +1 -1
- package/dist/renderer/assets/{Page-CpSczAz8.js → Page-DPSdvMgU.js} +1 -1
- package/dist/renderer/assets/{ProjectDetail-FJ_bNXLF.js → ProjectDetail-C6EWGvGg.js} +1 -1
- package/dist/renderer/assets/{ProjectList-DZKL4K9C.js → ProjectList-BBo5ygnS.js} +1 -1
- package/dist/renderer/assets/{Select-BHQk6qCU.js → Select-CNX-Tx_O.js} +2 -7
- package/dist/renderer/assets/SessionChat-BIn46jqZ.js +1 -0
- package/dist/renderer/assets/SessionList-CZt6f1Rd.js +1 -0
- package/dist/renderer/assets/{ThreadChat-D3s4GXGh.js → ThreadChat-BotD0gKQ.js} +7 -7
- package/dist/renderer/assets/Workspace-C_4tdpjX.js +14 -0
- package/dist/renderer/assets/{arrow-left-Dtal-eOi.js → arrow-left-cHr5wkpq.js} +1 -1
- package/dist/renderer/assets/{book-open-q814R_Ov.js → book-open-4nx5C8T-.js} +1 -1
- package/dist/renderer/assets/{check-DjfCdLvw.js → check-exTS-R-x.js} +1 -1
- package/dist/renderer/assets/chevron-down-DSvxXQa0.js +6 -0
- package/dist/renderer/assets/{circle-vfhmyE8v.js → circle-DyvEovdw.js} +1 -1
- package/dist/renderer/assets/desktop-B-KVT10D.js +1 -0
- package/dist/renderer/assets/desktop-CVibRyAw.js +4 -0
- package/dist/renderer/assets/{funnel-CUQibo1I.js → funnel-DA9qVdTy.js} +1 -1
- package/dist/renderer/assets/{heart-DG9_b8P1.js → heart-DuEV0ZRJ.js} +1 -1
- package/dist/renderer/assets/{index-Ds0IN5rP.js → index-BgflqkCN.js} +3 -3
- package/dist/renderer/assets/index-Biz-aexG.js +177 -0
- package/dist/renderer/assets/{index-3CzgyQYB.css → index-DFiJR0vv.css} +1 -1
- package/dist/renderer/assets/{pencil-Dij0LlRY.js → pencil-DBDxeNmO.js} +1 -1
- package/dist/renderer/assets/{play-CPFGjIyJ.js → play-IzlP83_7.js} +1 -1
- package/dist/renderer/assets/{plug-DH3RKQyn.js → plug-CuT-qFnW.js} +1 -1
- package/dist/renderer/assets/{plus-afkK9_Y8.js → plus-DEqgkn63.js} +1 -1
- package/dist/renderer/assets/projects-DDtDN1rc.js +1 -0
- package/dist/renderer/assets/{save-B-cKgWcy.js → save-D4sRo07y.js} +1 -1
- package/dist/renderer/assets/{search-DR6PeYkt.js → search-DbPZLuV8.js} +1 -1
- package/dist/renderer/assets/{server-BM6KOB5a.js → server-D1jUQmwN.js} +1 -1
- package/dist/renderer/assets/{session-utils-BDuThM0a.js → session-utils-Aife8ep2.js} +1 -1
- package/dist/renderer/assets/{sessions-C1yKps4T.js → sessions-DaH3H7gC.js} +1 -1
- package/dist/renderer/assets/{shield-check-BFjMG-r2.js → shield-check-BGwUFWNr.js} +1 -1
- package/dist/renderer/assets/{trash-2-CW1heMCf.js → trash-2-CIFBioBa.js} +1 -1
- package/dist/renderer/assets/{user-r_2sHG7m.js → user-CnKcvHdo.js} +1 -1
- package/dist/renderer/assets/{wifi-off-VMUokMpE.js → wifi-off-ClO53WGa.js} +1 -1
- package/dist/renderer/index.html +2 -2
- package/dist-electron/electron/local-core-refactor.test.js +812 -3
- 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/{gateway → channel/lark}/local-core-lark-gateway.js +365 -383
- 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/file-utils.js +33 -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/{gateway → channel/weixin}/local-core-weixin-gateway.js +262 -258
- 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/cli/lac.js +41 -1
- 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 +19 -40
- package/dist-electron/services/local-ai-core/src/plugins/builtin/channel-weixin-plugin.js +19 -40
- package/dist-electron/services/local-ai-core/src/runtime/local-core-controller.js +96 -44
- package/dist-electron/services/local-ai-core/src/runtime/server.js +20 -8
- package/dist-electron/shared/desktop.js +24 -1
- package/package.json +1 -1
- package/dist/renderer/assets/CronList-BYZ-x_bp.js +0 -1
- package/dist/renderer/assets/Logs-D7HxZ8Nb.js +0 -1
- package/dist/renderer/assets/SessionChat-DpgSXF3m.js +0 -1
- package/dist/renderer/assets/SessionList-CU6drQ4N.js +0 -1
- package/dist/renderer/assets/Workspace-B7looTVo.js +0 -14
- package/dist/renderer/assets/desktop-BPIxWbkM.js +0 -1
- package/dist/renderer/assets/desktop-DZHC4PDB.js +0 -3
- package/dist/renderer/assets/index-Bn8ane6_.js +0 -177
- 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("
|
|
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
|
|
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
|
|
164
|
-
for (const
|
|
165
|
-
if (!
|
|
166
|
-
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);
|
|
167
148
|
}
|
|
168
149
|
}
|
|
169
150
|
for (const binding of bindings) {
|
|
170
|
-
const
|
|
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.
|
|
155
|
+
await this.stopWorkspaceKey(key);
|
|
174
156
|
}
|
|
175
157
|
else {
|
|
176
|
-
this.runtime.set(
|
|
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 =
|
|
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
|
|
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 ===
|
|
218
|
-
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);
|
|
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.
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
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,
|
|
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:
|
|
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,
|
|
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
|
|
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,
|
|
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,
|
|
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.
|
|
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,
|
|
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:
|
|
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,
|
|
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 ===
|
|
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:
|
|
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,
|
|
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,
|
|
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:
|
|
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,
|
|
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:
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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(
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
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
|
-
|
|
953
|
-
|
|
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)
|