@xmanrui/dsh-im 4.22.0 → 4.23.0
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.en.md +3 -3
- package/README.md +3 -3
- package/lib/client.js +2505 -1152
- package/lib/index.js +290 -299
- package/package.json +8 -1
- package/plugin-src/client/channel-card-meta.js +2 -0
- package/plugin-src/client/channel-logos.js +11 -0
- package/plugin-src/client/channels/dingtalk/api.js +5 -1
- package/plugin-src/client/channels/dingtalk/index.js +15 -13
- package/plugin-src/client/channels/email/api.js +52 -0
- package/plugin-src/client/channels/email/index.js +728 -0
- package/plugin-src/client/channels/email/styles.js +51 -0
- package/plugin-src/client/channels/feishu/api.js +4 -2
- package/plugin-src/client/channels/feishu/index.js +16 -3
- package/plugin-src/client/channels/imessage/index.js +2 -1
- package/plugin-src/client/channels/office/api.js +2 -0
- package/plugin-src/client/channels/office/index.js +6 -5
- package/plugin-src/client/channels/qq/api.js +7 -0
- package/plugin-src/client/channels/qq/index.js +16 -3
- package/plugin-src/client/channels/shared/token-api.js +5 -1
- package/plugin-src/client/channels/shared/token-channel.js +28 -3
- package/plugin-src/client/channels/slack/index.js +2 -1
- package/plugin-src/client/channels/wecom/api.js +7 -1
- package/plugin-src/client/channels/wecom/index.js +16 -3
- package/plugin-src/client/channels/wecom-app/api.js +6 -1
- package/plugin-src/client/channels/wecom-app/index.js +16 -3
- package/plugin-src/client/channels/weixin/api.js +2 -1
- package/plugin-src/client/channels/weixin/connection-error.js +1 -71
- package/plugin-src/client/channels/whatsapp/api.js +5 -0
- package/plugin-src/client/channels/whatsapp/index.js +16 -3
- package/plugin-src/client/connection-error.js +87 -0
- package/plugin-src/client/global-settings.js +81 -1
- package/plugin-src/client/i18n.js +61 -0
- package/plugin-src/client/index.js +30 -2
- package/plugin-src/client/last-message-error.js +2 -1
- package/plugin-src/client/styles.js +6 -0
- package/plugin-src/host/build.mjs +4 -0
- package/plugin-src/host/channels/dingtalk/production.mjs +2 -0
- package/plugin-src/host/channels/dingtalk/rpc.mjs +7 -4
- package/plugin-src/host/channels/email/availability.mjs +60 -0
- package/plugin-src/host/channels/email/index.mjs +36 -0
- package/plugin-src/host/channels/email/production.mjs +58 -0
- package/plugin-src/host/channels/email/rpc.mjs +163 -0
- package/plugin-src/host/channels/feishu/production.mjs +3 -0
- package/plugin-src/host/channels/feishu/rpc.mjs +7 -4
- package/plugin-src/host/channels/imessage/rpc.mjs +13 -6
- package/plugin-src/host/channels/office/rpc.mjs +3 -1
- package/plugin-src/host/channels/qq/production.mjs +2 -0
- package/plugin-src/host/channels/qq/rpc.mjs +5 -3
- package/plugin-src/host/channels/shared/production.mjs +52 -5
- package/plugin-src/host/channels/shared/rpc.mjs +5 -3
- package/plugin-src/host/channels/shared/startup-error.mjs +4 -3
- package/plugin-src/host/channels/shared/startup.mjs +11 -6
- package/plugin-src/host/channels/slack/production.mjs +2 -0
- package/plugin-src/host/channels/slack/rpc.mjs +5 -3
- package/plugin-src/host/channels/wecom/production.mjs +2 -0
- package/plugin-src/host/channels/wecom/rpc.mjs +5 -3
- package/plugin-src/host/channels/wecom-app/production.mjs +2 -0
- package/plugin-src/host/channels/wecom-app/rpc.mjs +5 -3
- package/plugin-src/host/channels/weixin/production.mjs +2 -0
- package/plugin-src/host/channels/whatsapp/production.mjs +2 -0
- package/plugin-src/host/channels/whatsapp/rpc.mjs +5 -3
- package/plugin-src/host/delivery-adapter.mjs +11 -0
- package/plugin-src/host/image-input-rpc.mjs +24 -0
- package/plugin-src/host/inbound-ttl-rpc.mjs +6 -1
- package/plugin-src/host/index.mjs +3 -0
- package/plugin-src/management-rpc.mjs +12 -2
- package/scripts/verify-package.mjs +3 -1
- package/src/channels/dingtalk/connection-error.mjs +5 -12
- package/src/channels/dingtalk/device-auth.mjs +4 -1
- package/src/channels/dingtalk/dingtalk-bridge.mjs +6 -4
- package/src/channels/dingtalk/dingtalk-controller.mjs +60 -45
- package/src/channels/dingtalk/dingtalk-runtime.mjs +20 -4
- package/src/channels/discord/discord-api.mjs +3 -3
- package/src/channels/discord/discord-runtime.mjs +18 -12
- package/src/channels/email/config-store.mjs +194 -0
- package/src/channels/email/email-api.mjs +20 -0
- package/src/channels/email/email-bridge.mjs +17 -0
- package/src/channels/email/email-controller.mjs +824 -0
- package/src/channels/email/email-runtime.mjs +692 -0
- package/src/channels/email/harness-client.mjs +7 -0
- package/src/channels/email/mail-format.mjs +91 -0
- package/src/channels/email/state-store.mjs +176 -0
- package/src/channels/email/transport.mjs +64 -0
- package/src/channels/email/transports/agent-mail.mjs +593 -0
- package/src/channels/email/transports/agently-cli.mjs +288 -0
- package/src/channels/email/transports/imap-smtp.mjs +165 -0
- package/src/channels/feishu/bridge.mjs +8 -7
- package/src/channels/feishu/feishu-runtime.mjs +15 -8
- package/src/channels/feishu/group-message-permission-manager.mjs +2 -1
- package/src/channels/feishu/message-utils.mjs +3 -2
- package/src/channels/feishu/multi-bot-controller.mjs +56 -47
- package/src/channels/feishu/registration-manager.mjs +8 -4
- package/src/channels/feishu/repair-manager.mjs +2 -1
- package/src/channels/imessage/runtime.mjs +9 -5
- package/src/channels/office/office-controller.mjs +23 -12
- package/src/channels/office/office-runtime.mjs +6 -3
- package/src/channels/office/office-transport.mjs +1 -0
- package/src/channels/qq/qq-bridge.mjs +7 -5
- package/src/channels/qq/qq-controller.mjs +56 -42
- package/src/channels/qq/qq-runtime.mjs +16 -11
- package/src/channels/shared/bot-workspace-store.mjs +29 -10
- package/src/channels/shared/connection-error.mjs +191 -0
- package/src/channels/shared/connection-test.mjs +3 -1
- package/src/channels/shared/conversation-state-store.mjs +46 -1
- package/src/channels/shared/diagnostic-details.mjs +95 -0
- package/src/channels/shared/harness-client.mjs +63 -18
- package/src/channels/shared/i18n-en/diagnostics.mjs +55 -0
- package/src/channels/shared/i18n-en/email.mjs +25 -0
- package/src/channels/shared/i18n-en/image-input.mjs +17 -0
- package/src/channels/shared/i18n-en/shared-a.mjs +1 -0
- package/src/channels/shared/i18n-en/shared-c.mjs +1 -1
- package/src/channels/shared/i18n-en.mjs +6 -0
- package/src/channels/shared/image-input-policy.mjs +35 -0
- package/src/channels/shared/image-input-settings-store.mjs +60 -0
- package/src/channels/shared/image-input.mjs +124 -0
- package/src/channels/shared/image-prompt.mjs +15 -7
- package/src/channels/shared/message-failure.mjs +12 -0
- package/src/channels/shared/semantic/reply-reference.mjs +3 -3
- package/src/channels/shared/text-harness-bridge.mjs +90 -11
- package/src/channels/shared/token-bot-controller.mjs +52 -47
- package/src/channels/slack/slack-api.mjs +3 -3
- package/src/channels/slack/slack-controller.mjs +59 -51
- package/src/channels/slack/slack-runtime.mjs +18 -12
- package/src/channels/telegram/telegram-api.mjs +2 -2
- package/src/channels/telegram/telegram-runtime.mjs +17 -13
- package/src/channels/wecom/wecom-bridge.mjs +15 -6
- package/src/channels/wecom/wecom-controller.mjs +51 -37
- package/src/channels/wecom/wecom-runtime.mjs +10 -6
- package/src/channels/wecom-app/wecom-app-bridge.mjs +7 -5
- package/src/channels/wecom-app/wecom-app-controller.mjs +50 -37
- package/src/channels/wecom-app/wecom-app-runtime.mjs +2 -1
- package/src/channels/weixin/connection-error.mjs +26 -92
- package/src/channels/weixin/diagnostic-details.mjs +1 -63
- package/src/channels/weixin/weixin-bridge.mjs +7 -5
- package/src/channels/whatsapp/whatsapp-controller.mjs +34 -17
- package/src/channels/whatsapp/whatsapp-runtime.mjs +35 -8
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { extractConnectionEvidence, atConnectionStage, createConnectionDiagnostics } from '../shared/connection-error.mjs';
|
|
1
2
|
import { randomUUID } from 'node:crypto';
|
|
2
3
|
|
|
3
4
|
import { deriveWecomBotIdentity, maskWecomBotId } from './config-store.mjs';
|
|
@@ -40,6 +41,7 @@ export class WecomController {
|
|
|
40
41
|
#createRuntime;
|
|
41
42
|
#deleteState;
|
|
42
43
|
#logger;
|
|
44
|
+
#diagnostics;
|
|
43
45
|
#runtimes = new Map();
|
|
44
46
|
#errors = new Map();
|
|
45
47
|
#attempts = new Map();
|
|
@@ -74,6 +76,14 @@ export class WecomController {
|
|
|
74
76
|
this.#createRuntime = createRuntime;
|
|
75
77
|
this.#deleteState = deleteState;
|
|
76
78
|
this.#logger = logger;
|
|
79
|
+
this.#diagnostics = createConnectionDiagnostics({ channel: 'wecom', logger });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
get diagnostics() { return this.#diagnostics; }
|
|
83
|
+
|
|
84
|
+
#failure(error, code, message) {
|
|
85
|
+
const stage = code.startsWith('qr-') ? 'qr.begin' : code === 'activation-failed' ? 'activation' : 'connection.start';
|
|
86
|
+
return this.#diagnostics.report(error, { reuse: true, stage, publicError: { code, message } }).publicError;
|
|
77
87
|
}
|
|
78
88
|
|
|
79
89
|
async initialize() {
|
|
@@ -82,16 +92,16 @@ export class WecomController {
|
|
|
82
92
|
await this.#withBotTransition(config.botId, async () => {
|
|
83
93
|
const existing = this.#runtimes.get(config.botId)?.status;
|
|
84
94
|
if (this.#closed || existing?.ready || existing?.wecomConnectionState === 'connecting') return;
|
|
85
|
-
const secret = await this.#resolveSecret(config.secretRef);
|
|
86
|
-
if (!secret) {
|
|
87
|
-
this.#errors.set(config.botId, safeError('missing-secret', t('企业微信机器人凭据缺失,请移除后重新扫码。')));
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
95
|
try {
|
|
96
|
+
const secret = await this.#resolveSecret(config.secretRef);
|
|
97
|
+
if (!secret) {
|
|
98
|
+
this.#errors.set(config.botId, safeError('missing-secret', t('企业微信机器人凭据缺失,请移除后重新扫码。')));
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
91
101
|
await this.#startRuntime(config, secret);
|
|
92
102
|
this.#errors.delete(config.botId);
|
|
93
103
|
} catch (error) {
|
|
94
|
-
this.#errors.set(config.botId,
|
|
104
|
+
this.#errors.set(config.botId, this.#failure(error, 'connection-failed', t('企业微信连接未就绪,插件会自动重试。')));
|
|
95
105
|
this.#logger.warn?.(`[dsh-im:wecom] bot ${config.botId} failed to initialize`);
|
|
96
106
|
} finally {
|
|
97
107
|
this.#touch();
|
|
@@ -137,7 +147,7 @@ export class WecomController {
|
|
|
137
147
|
record.state = record.controller.signal.aborted ? 'cancelled' : 'failed';
|
|
138
148
|
record.error = record.controller.signal.aborted
|
|
139
149
|
? safeError('cancelled', t('扫码绑定已取消。'))
|
|
140
|
-
:
|
|
150
|
+
: this.#failure(error, 'qr-start-failed', t('无法生成企业微信二维码,请稍后重试。'));
|
|
141
151
|
this.#finishAttempt(record);
|
|
142
152
|
throw error;
|
|
143
153
|
}
|
|
@@ -180,7 +190,7 @@ export class WecomController {
|
|
|
180
190
|
await this.#withBotTransition(identity.botId, async () => {
|
|
181
191
|
if (this.#closed) throw new Error('Enterprise WeChat controller is closed');
|
|
182
192
|
const previousConfig = this.#configStore.getByRemoteBotId(remoteBotId);
|
|
183
|
-
const previousSecret = await this.#credentials.resolve(identity.secretRef)
|
|
193
|
+
const previousSecret = await atConnectionStage('credential.read', () => this.#credentials.resolve(identity.secretRef), 'credential-store');
|
|
184
194
|
if (this.#closed) throw new Error('Enterprise WeChat controller is closed');
|
|
185
195
|
const config = {
|
|
186
196
|
botId: identity.botId,
|
|
@@ -189,9 +199,9 @@ export class WecomController {
|
|
|
189
199
|
createdAt: previousConfig?.createdAt ?? new Date().toISOString(),
|
|
190
200
|
connectedAt: new Date().toISOString(),
|
|
191
201
|
};
|
|
192
|
-
await this.#credentials.set(identity.secretRef, normalizedSecret);
|
|
202
|
+
await atConnectionStage('credential.save', () => this.#credentials.set(identity.secretRef, normalizedSecret), 'credential-store');
|
|
193
203
|
try {
|
|
194
|
-
await this.#configStore.save(config);
|
|
204
|
+
await atConnectionStage('account.save', () => this.#configStore.save(config), 'account-config');
|
|
195
205
|
} catch (error) {
|
|
196
206
|
await this.#restoreCredential(identity.secretRef, previousSecret);
|
|
197
207
|
throw error;
|
|
@@ -234,7 +244,7 @@ export class WecomController {
|
|
|
234
244
|
await this.#startRuntime(config, secret);
|
|
235
245
|
this.#errors.delete(botId);
|
|
236
246
|
} catch (error) {
|
|
237
|
-
this.#errors.set(botId,
|
|
247
|
+
this.#errors.set(botId, this.#failure(error, 'connection-failed', t('企业微信连接仍未就绪,请稍后重试。')));
|
|
238
248
|
throw error;
|
|
239
249
|
} finally {
|
|
240
250
|
this.#touch();
|
|
@@ -272,28 +282,32 @@ export class WecomController {
|
|
|
272
282
|
}
|
|
273
283
|
|
|
274
284
|
async deleteBot(botId) {
|
|
285
|
+
const warnings = [];
|
|
275
286
|
const config = this.#configStore.get(botId);
|
|
276
287
|
if (!config) throw new Error('Unknown Enterprise WeChat bot');
|
|
277
288
|
await this.#withBotTransition(botId, async () => {
|
|
278
|
-
const previous = await this.#credentials.resolve(config.secretRef)
|
|
289
|
+
const previous = await atConnectionStage('credential.read', () => this.#credentials.resolve(config.secretRef), 'credential-store');
|
|
279
290
|
await this.#stopRuntime(botId);
|
|
280
291
|
try {
|
|
281
|
-
await this.#credentials.unset(config.secretRef);
|
|
282
|
-
await this.#configStore.remove(botId);
|
|
292
|
+
await atConnectionStage('credential.remove', () => this.#credentials.unset(config.secretRef), 'credential-store');
|
|
293
|
+
await atConnectionStage('account.remove', () => this.#configStore.remove(botId), 'account-config');
|
|
283
294
|
} catch (error) {
|
|
284
|
-
if (
|
|
285
|
-
|
|
286
|
-
|
|
295
|
+
if (!this.#configStore.get(botId)) {
|
|
296
|
+
warnings.push(this.#diagnostics.report(error, { operation: 'bot.delete', stage: 'workspace.cleanup', warning: true,
|
|
297
|
+
publicError: { code: 'workspace-cleanup-failed', message: '账号已移除,但本地状态清理失败。' } }).publicError);
|
|
298
|
+
} else {
|
|
299
|
+
if (previous?.value) {
|
|
300
|
+
await atConnectionStage('credential.save', () => this.#credentials.set(config.secretRef, previous.value), 'credential-store').catch(() => undefined);
|
|
301
|
+
await this.#startRuntime(config, previous.value).catch(() => undefined);
|
|
302
|
+
}
|
|
303
|
+
throw new Error('Unable to remove the Enterprise WeChat bot safely.', { cause: error });
|
|
287
304
|
}
|
|
288
|
-
throw new Error('Unable to remove the Enterprise WeChat bot safely.', { cause: error });
|
|
289
305
|
}
|
|
290
|
-
await this.#deleteState({ botId, config }).catch(
|
|
291
|
-
this.#logger.warn?.(`[dsh-im:wecom] bot ${botId} state cleanup failed:`, error);
|
|
292
|
-
});
|
|
306
|
+
await this.#deleteState({ botId, config }).catch(error => { warnings.push(this.#diagnostics.report(error, { reuse: true, operation: 'bot.delete', stage: 'state.cleanup', resource: 'account-state', warning: true, publicError: { code: 'cleanup-failed', message: '账号已移除,但本地状态清理失败。' } }).publicError); });
|
|
293
307
|
this.#errors.delete(botId);
|
|
294
308
|
this.#touch();
|
|
295
309
|
});
|
|
296
|
-
return this.status();
|
|
310
|
+
return { ...this.status(), ...(warnings.length ? { warnings } : {}) };
|
|
297
311
|
}
|
|
298
312
|
|
|
299
313
|
status() {
|
|
@@ -324,7 +338,7 @@ export class WecomController {
|
|
|
324
338
|
messagesReplied: runtimeStatus?.messagesReplied ?? 0,
|
|
325
339
|
},
|
|
326
340
|
lastMessageError: publicMessageFailure(runtimeStatus?.lastMessageError),
|
|
327
|
-
error: structuredClone(this.#errors.get(config.botId) ?? null),
|
|
341
|
+
error: structuredClone(runtimeStatus?.error ?? this.#errors.get(config.botId) ?? null),
|
|
328
342
|
};
|
|
329
343
|
});
|
|
330
344
|
const connectedCount = bots.filter((bot) => bot.connected).length;
|
|
@@ -379,7 +393,7 @@ export class WecomController {
|
|
|
379
393
|
} catch (error) {
|
|
380
394
|
if (record.controller.signal.aborted) return;
|
|
381
395
|
record.state = 'failed';
|
|
382
|
-
record.error =
|
|
396
|
+
record.error = this.#failure(error, 'qr-connect-failed', t('企业微信扫码服务暂时不可用,请重新生成二维码。'));
|
|
383
397
|
this.#logger.warn?.('[dsh-im:wecom] QR polling failed');
|
|
384
398
|
this.#finishAttempt(record);
|
|
385
399
|
}
|
|
@@ -399,7 +413,7 @@ export class WecomController {
|
|
|
399
413
|
record.error = safeError('cancelled', t('扫码绑定已取消。'));
|
|
400
414
|
} else {
|
|
401
415
|
record.state = 'failed';
|
|
402
|
-
record.error =
|
|
416
|
+
record.error = this.#failure(error, 'activation-failed', t('企业微信已授权,但无法安全保存接入配置。'));
|
|
403
417
|
this.#logger.error?.('[dsh-im:wecom] provisioning failed');
|
|
404
418
|
}
|
|
405
419
|
} finally {
|
|
@@ -410,7 +424,7 @@ export class WecomController {
|
|
|
410
424
|
async #activateBot(record, { remoteBotId, secret }) {
|
|
411
425
|
const identity = deriveWecomBotIdentity(remoteBotId);
|
|
412
426
|
const previousConfig = this.#configStore.getByRemoteBotId(remoteBotId);
|
|
413
|
-
const previousSecret = await this.#credentials.resolve(identity.secretRef)
|
|
427
|
+
const previousSecret = await atConnectionStage('credential.read', () => this.#credentials.resolve(identity.secretRef), 'credential-store');
|
|
414
428
|
const config = {
|
|
415
429
|
botId: identity.botId,
|
|
416
430
|
remoteBotId,
|
|
@@ -419,10 +433,10 @@ export class WecomController {
|
|
|
419
433
|
connectedAt: new Date().toISOString(),
|
|
420
434
|
};
|
|
421
435
|
return this.#withBotTransition(identity.botId, async () => {
|
|
422
|
-
await this.#credentials.set(identity.secretRef, secret);
|
|
436
|
+
await atConnectionStage('credential.save', () => this.#credentials.set(identity.secretRef, secret), 'credential-store');
|
|
423
437
|
try {
|
|
424
438
|
if (record.controller.signal.aborted) throw new DOMException('Cancelled', 'AbortError');
|
|
425
|
-
await this.#configStore.save(config);
|
|
439
|
+
await atConnectionStage('account.save', () => this.#configStore.save(config), 'account-config');
|
|
426
440
|
} catch (error) {
|
|
427
441
|
await this.#restoreCredential(identity.secretRef, previousSecret);
|
|
428
442
|
throw error;
|
|
@@ -434,19 +448,19 @@ export class WecomController {
|
|
|
434
448
|
} catch (error) {
|
|
435
449
|
if (record.controller.signal.aborted) {
|
|
436
450
|
await this.#stopRuntime(identity.botId);
|
|
437
|
-
if (previousConfig) await this.#configStore.save(previousConfig).catch(() => undefined);
|
|
451
|
+
if (previousConfig) await atConnectionStage('account.save', () => this.#configStore.save(previousConfig), 'account-config').catch(() => undefined);
|
|
438
452
|
else {
|
|
439
|
-
const removed = await this.#configStore.remove(identity.botId).catch(() => null);
|
|
453
|
+
const removed = await atConnectionStage('account.remove', () => this.#configStore.remove(identity.botId), 'account-config').catch(() => null);
|
|
440
454
|
if (removed) {
|
|
441
455
|
await this.#deleteState({ botId: identity.botId, config }).catch((cleanupError) => {
|
|
442
|
-
this.#logger.warn?.('[dsh-im:wecom] cancelled bot state cleanup failed:', cleanupError);
|
|
456
|
+
this.#logger.warn?.('[dsh-im:wecom] cancelled bot state cleanup failed:', extractConnectionEvidence(cleanupError).details);
|
|
443
457
|
});
|
|
444
458
|
}
|
|
445
459
|
}
|
|
446
460
|
await this.#restoreCredential(identity.secretRef, previousSecret);
|
|
447
461
|
throw error;
|
|
448
462
|
}
|
|
449
|
-
this.#errors.set(identity.botId,
|
|
463
|
+
this.#errors.set(identity.botId, this.#failure(error, 'connection-failed', t('企业微信机器人已绑定,消息连接暂未就绪。')));
|
|
450
464
|
this.#logger.warn?.(`[dsh-im:wecom] bot ${identity.botId} activation connection failed`);
|
|
451
465
|
}
|
|
452
466
|
this.#touch();
|
|
@@ -458,7 +472,7 @@ export class WecomController {
|
|
|
458
472
|
if (this.#closed) throw new Error('Enterprise WeChat controller is closed');
|
|
459
473
|
await this.#stopRuntime(config.botId);
|
|
460
474
|
if (this.#closed) throw new Error('Enterprise WeChat controller is closed');
|
|
461
|
-
const runtime = await this.#createRuntime({ botId: config.botId, config, secret });
|
|
475
|
+
const runtime = await atConnectionStage('runtime.prepare', () => this.#createRuntime({ botId: config.botId, config, secret }));
|
|
462
476
|
if (!runtime || typeof runtime.start !== 'function' || typeof runtime.stop !== 'function') {
|
|
463
477
|
throw new TypeError('createRuntime returned an invalid Enterprise WeChat runtime');
|
|
464
478
|
}
|
|
@@ -476,18 +490,18 @@ export class WecomController {
|
|
|
476
490
|
const runtime = this.#runtimes.get(botId);
|
|
477
491
|
this.#runtimes.delete(botId);
|
|
478
492
|
await runtime?.stop().catch((error) => {
|
|
479
|
-
this.#logger.warn?.(`[dsh-im:wecom] bot ${botId} failed to stop cleanly:`, error);
|
|
493
|
+
this.#logger.warn?.(`[dsh-im:wecom] bot ${botId} failed to stop cleanly:`, extractConnectionEvidence(error).details);
|
|
480
494
|
});
|
|
481
495
|
}
|
|
482
496
|
|
|
483
497
|
async #resolveSecret(ref) {
|
|
484
|
-
const result = await this.#credentials.resolve(ref)
|
|
498
|
+
const result = await atConnectionStage('credential.read', () => this.#credentials.resolve(ref), 'credential-store');
|
|
485
499
|
return cleanString(result?.value);
|
|
486
500
|
}
|
|
487
501
|
|
|
488
502
|
async #restoreCredential(ref, previous) {
|
|
489
|
-
if (previous?.value) await this.#credentials.set(ref, previous.value).catch(() => undefined);
|
|
490
|
-
else await this.#credentials.unset(ref).catch(() => undefined);
|
|
503
|
+
if (previous?.value) await atConnectionStage('credential.save', () => this.#credentials.set(ref, previous.value), 'credential-store').catch(() => undefined);
|
|
504
|
+
else await atConnectionStage('credential.remove', () => this.#credentials.unset(ref), 'credential-store').catch(() => undefined);
|
|
491
505
|
}
|
|
492
506
|
|
|
493
507
|
#withBotTransition(botId, operation) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createConnectionDiagnostics, atConnectionStage } from '../shared/connection-error.mjs';
|
|
1
2
|
import { WSAuthFailureError, WSClient, WSReconnectExhaustedError } from '@wecom/aibot-node-sdk';
|
|
2
3
|
|
|
3
4
|
import { createWecomBridgeStatus, WecomHarnessBridge } from './wecom-bridge.mjs';
|
|
@@ -31,6 +32,7 @@ export class WecomRuntime {
|
|
|
31
32
|
#contextEnhancement;
|
|
32
33
|
#accessPolicy;
|
|
33
34
|
#logger;
|
|
35
|
+
#diagnostics;
|
|
34
36
|
#replyTimeoutMs;
|
|
35
37
|
#connectTimeoutMs;
|
|
36
38
|
#maxReconnectAttempts;
|
|
@@ -64,7 +66,7 @@ export class WecomRuntime {
|
|
|
64
66
|
this.#state = state;
|
|
65
67
|
this.#contextEnhancement = contextEnhancement;
|
|
66
68
|
this.#accessPolicy = accessPolicy;
|
|
67
|
-
this.#logger = logger;
|
|
69
|
+
this.#logger = logger; this.#diagnostics = createConnectionDiagnostics({ channel: 'wecom', logger });
|
|
68
70
|
this.#replyTimeoutMs = replyTimeoutMs;
|
|
69
71
|
this.#connectTimeoutMs = connectTimeoutMs;
|
|
70
72
|
this.#maxReconnectAttempts = maxReconnectAttempts;
|
|
@@ -94,8 +96,8 @@ export class WecomRuntime {
|
|
|
94
96
|
signal.throwIfAborted();
|
|
95
97
|
this.#status.startedAt = new Date().toISOString();
|
|
96
98
|
this.#status.wecomConnectionState = 'connecting';
|
|
97
|
-
this.#status.lastError = null;
|
|
98
|
-
await this.#harness.ensureRunning();
|
|
99
|
+
this.#status.lastError = null; this.#status.error = null; this.#diagnostics.clear();
|
|
100
|
+
await atConnectionStage('harness.check', () => this.#harness.ensureRunning());
|
|
99
101
|
this.#status.harnessReachable = true;
|
|
100
102
|
|
|
101
103
|
const silentSdkLogger = { debug() {}, info() {}, warn() {}, error() {} };
|
|
@@ -136,7 +138,7 @@ export class WecomRuntime {
|
|
|
136
138
|
this.#status.wecomConnectionState = 'connected';
|
|
137
139
|
this.#status.lastCheckedAt = now;
|
|
138
140
|
this.#status.lastConnectedAt = now;
|
|
139
|
-
this.#status.lastError = null;
|
|
141
|
+
this.#status.lastError = null; this.#status.error = null; this.#diagnostics.clear();
|
|
140
142
|
readyResolve();
|
|
141
143
|
};
|
|
142
144
|
const onDisconnected = () => {
|
|
@@ -159,7 +161,8 @@ export class WecomRuntime {
|
|
|
159
161
|
this.#status.ready = false;
|
|
160
162
|
this.#status.wecomConnectionState = 'failed';
|
|
161
163
|
}
|
|
162
|
-
this.#status.
|
|
164
|
+
this.#status.error = this.#diagnostics.report(error, { operation: 'connection.monitor', botId: this.#config?.botId, automatic: true }).publicError;
|
|
165
|
+
this.#status.lastError = this.#status.error.message;
|
|
163
166
|
this.#logger.warn?.(`[dsh-im:wecom] bot ${this.#config.botId} connection error`);
|
|
164
167
|
};
|
|
165
168
|
const onMessage = (frame) => this.#bridge?.accept(frame);
|
|
@@ -191,7 +194,8 @@ export class WecomRuntime {
|
|
|
191
194
|
}
|
|
192
195
|
this.#status.ready = false;
|
|
193
196
|
this.#status.wecomConnectionState = 'failed';
|
|
194
|
-
this.#status.
|
|
197
|
+
this.#status.error = this.#diagnostics.report(error, { operation: 'connection.monitor', botId: this.#config?.botId, automatic: true }).publicError;
|
|
198
|
+
this.#status.lastError = this.#status.error.message;
|
|
195
199
|
await this.#stopActive();
|
|
196
200
|
throw error;
|
|
197
201
|
} finally {
|
|
@@ -52,6 +52,7 @@ import {
|
|
|
52
52
|
promptContentForInboundMessage,
|
|
53
53
|
} from '../shared/semantic/reply-reference.mjs';
|
|
54
54
|
import {
|
|
55
|
+
messageFailureDiagnostic,
|
|
55
56
|
channelDeliveryFailure,
|
|
56
57
|
clearLastMessageFailure,
|
|
57
58
|
messageFailureText,
|
|
@@ -325,7 +326,7 @@ export class WecomAppBridge {
|
|
|
325
326
|
const failure = setLastMessageFailure(this.#status, error);
|
|
326
327
|
this.#logger.error?.(
|
|
327
328
|
`[dsh-im:wecom-app] failed to process a command [${failure.referenceId}]:`,
|
|
328
|
-
error,
|
|
329
|
+
messageFailureDiagnostic(error, failure),
|
|
329
330
|
);
|
|
330
331
|
return this.#send(sender, messageFailureText(failure)).catch(() => undefined);
|
|
331
332
|
})
|
|
@@ -426,7 +427,7 @@ export class WecomAppBridge {
|
|
|
426
427
|
const failure = setLastMessageFailure(this.#status, error);
|
|
427
428
|
this.#logger.error?.(
|
|
428
429
|
`[dsh-im:wecom-app] failed to process a batch input message [${failure.referenceId}]:`,
|
|
429
|
-
error,
|
|
430
|
+
messageFailureDiagnostic(error, failure),
|
|
430
431
|
);
|
|
431
432
|
await this.#send(sender, messageFailureText(failure)).catch(() => undefined);
|
|
432
433
|
}).finally(() => {
|
|
@@ -592,7 +593,7 @@ export class WecomAppBridge {
|
|
|
592
593
|
let artifacts = [];
|
|
593
594
|
try {
|
|
594
595
|
let content = hasImages || hasReply
|
|
595
|
-
? await promptContentForInboundMessage(promptMessage, { signal: this.#signal })
|
|
596
|
+
? await promptContentForInboundMessage(promptMessage, { signal: this.#signal, deferImages: true })
|
|
596
597
|
: undefined;
|
|
597
598
|
const snapshot = this.#acceptedMessageIds.get(messageId);
|
|
598
599
|
let contextEnhanced = false;
|
|
@@ -638,6 +639,7 @@ export class WecomAppBridge {
|
|
|
638
639
|
await this.#handleInteractionResolved(resolution);
|
|
639
640
|
},
|
|
640
641
|
files: promptMessage.files,
|
|
642
|
+
images: promptMessage.images,
|
|
641
643
|
},
|
|
642
644
|
}));
|
|
643
645
|
if (batchSubmission) {
|
|
@@ -720,7 +722,7 @@ export class WecomAppBridge {
|
|
|
720
722
|
});
|
|
721
723
|
this.#logger.error?.(
|
|
722
724
|
`[dsh-im:wecom-app] failed to process an inbound message [${failure.referenceId}]:`,
|
|
723
|
-
error,
|
|
725
|
+
messageFailureDiagnostic(error, failure),
|
|
724
726
|
);
|
|
725
727
|
try {
|
|
726
728
|
await this.#send(
|
|
@@ -1006,7 +1008,7 @@ export class WecomAppBridge {
|
|
|
1006
1008
|
const failure = setLastMessageFailure(this.#status, error);
|
|
1007
1009
|
this.#logger.error?.(
|
|
1008
1010
|
`[dsh-im:wecom-app] failed to process an interaction reply [${failure.referenceId}]:`,
|
|
1009
|
-
error,
|
|
1011
|
+
messageFailureDiagnostic(error, failure),
|
|
1010
1012
|
);
|
|
1011
1013
|
if (!this.#state.hasSeen(messageId)) {
|
|
1012
1014
|
await this.#state.markSeen(messageId).catch(() => undefined);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { extractConnectionEvidence, atConnectionStage, createConnectionDiagnostics } from '../shared/connection-error.mjs';
|
|
1
2
|
import {
|
|
2
3
|
deriveWecomAppIdentity,
|
|
3
4
|
generateCallbackSecret,
|
|
@@ -42,6 +43,7 @@ export class WecomAppController {
|
|
|
42
43
|
#createRuntime;
|
|
43
44
|
#deleteState;
|
|
44
45
|
#logger;
|
|
46
|
+
#diagnostics;
|
|
45
47
|
#buildCallbackUrl;
|
|
46
48
|
#runtimes = new Map();
|
|
47
49
|
#errors = new Map();
|
|
@@ -71,25 +73,33 @@ export class WecomAppController {
|
|
|
71
73
|
this.#createRuntime = createRuntime;
|
|
72
74
|
this.#deleteState = deleteState;
|
|
73
75
|
this.#logger = logger;
|
|
76
|
+
this.#diagnostics = createConnectionDiagnostics({ channel: 'wecom-app', logger });
|
|
74
77
|
this.#buildCallbackUrl = typeof buildCallbackUrl === 'function' ? buildCallbackUrl : null;
|
|
75
78
|
}
|
|
76
79
|
|
|
80
|
+
get diagnostics() { return this.#diagnostics; }
|
|
81
|
+
|
|
82
|
+
#failure(error, code, message) {
|
|
83
|
+
const stage = code.startsWith('qr-') ? 'qr.begin' : code === 'activation-failed' ? 'activation' : 'connection.start';
|
|
84
|
+
return this.#diagnostics.report(error, { reuse: true, stage, publicError: { code, message } }).publicError;
|
|
85
|
+
}
|
|
86
|
+
|
|
77
87
|
async initialize() {
|
|
78
88
|
if (this.#closed) return this.status();
|
|
79
89
|
for (const config of this.#configStore.list()) {
|
|
80
90
|
await this.#withBotTransition(config.botId, async () => {
|
|
81
91
|
if (this.#runtimes.has(config.botId)) return;
|
|
82
|
-
const secrets = await this.#resolveSecrets(config);
|
|
83
|
-
if (!secrets) {
|
|
84
|
-
this.#errors.set(config.botId, safeError('missing-credentials', t('企业微信应用凭据缺失,请重新绑定。')));
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
92
|
try {
|
|
93
|
+
const secrets = await this.#resolveSecrets(config);
|
|
94
|
+
if (!secrets) {
|
|
95
|
+
this.#errors.set(config.botId, safeError('missing-credentials', t('企业微信应用凭据缺失,请重新绑定。')));
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
88
98
|
await this.#startRuntime(config, secrets);
|
|
89
99
|
this.#errors.delete(config.botId);
|
|
90
100
|
} catch (error) {
|
|
91
|
-
this.#errors.set(config.botId,
|
|
92
|
-
|
|
101
|
+
this.#errors.set(config.botId, this.#failure(error, 'connection-failed', t('企业微信应用连接未就绪,插件会自动重试。')));
|
|
102
|
+
|
|
93
103
|
} finally {
|
|
94
104
|
this.#touch();
|
|
95
105
|
}
|
|
@@ -147,7 +157,7 @@ export class WecomAppController {
|
|
|
147
157
|
encodingAESKey: normalizedAesKey,
|
|
148
158
|
});
|
|
149
159
|
try {
|
|
150
|
-
await this.#configStore.save(config);
|
|
160
|
+
await atConnectionStage('account.save', () => this.#configStore.save(config), 'account-config');
|
|
151
161
|
} catch (error) {
|
|
152
162
|
await this.#restoreSecrets(identity, previous);
|
|
153
163
|
throw error;
|
|
@@ -160,8 +170,8 @@ export class WecomAppController {
|
|
|
160
170
|
});
|
|
161
171
|
this.#errors.delete(botId);
|
|
162
172
|
} catch (error) {
|
|
163
|
-
this.#errors.set(botId,
|
|
164
|
-
|
|
173
|
+
this.#errors.set(botId, this.#failure(error, 'connection-failed', t('企业微信应用已绑定,但尚未就绪。')));
|
|
174
|
+
|
|
165
175
|
}
|
|
166
176
|
this.#touch();
|
|
167
177
|
});
|
|
@@ -202,8 +212,8 @@ export class WecomAppController {
|
|
|
202
212
|
try {
|
|
203
213
|
await this.#startRuntime(updated, secrets);
|
|
204
214
|
} catch (error) {
|
|
205
|
-
this.#errors.set(botId,
|
|
206
|
-
|
|
215
|
+
this.#errors.set(botId, this.#failure(error, 'connection-failed', t('企业微信应用已更新,但尚未就绪。')));
|
|
216
|
+
|
|
207
217
|
}
|
|
208
218
|
}
|
|
209
219
|
}
|
|
@@ -224,8 +234,7 @@ export class WecomAppController {
|
|
|
224
234
|
try {
|
|
225
235
|
await this.#startRuntime(updated, secrets);
|
|
226
236
|
} catch (error) {
|
|
227
|
-
this.#errors.set(botId,
|
|
228
|
-
this.#logger.warn?.(`[dsh-im:wecom-app] bot ${botId} restart after secret reset failed`, error);
|
|
237
|
+
this.#errors.set(botId, this.#failure(error, 'connection-failed', t('回调密钥已重置,但机器人重启失败,请手动重连。')));
|
|
229
238
|
}
|
|
230
239
|
}
|
|
231
240
|
this.#touch();
|
|
@@ -243,7 +252,7 @@ export class WecomAppController {
|
|
|
243
252
|
await this.#startRuntime(config, secrets);
|
|
244
253
|
this.#errors.delete(botId);
|
|
245
254
|
} catch (error) {
|
|
246
|
-
this.#errors.set(botId,
|
|
255
|
+
this.#errors.set(botId, this.#failure(error, 'connection-failed', t('企业微信应用仍未就绪,请稍后重试。')));
|
|
247
256
|
throw error;
|
|
248
257
|
} finally {
|
|
249
258
|
this.#touch();
|
|
@@ -282,30 +291,34 @@ export class WecomAppController {
|
|
|
282
291
|
}
|
|
283
292
|
|
|
284
293
|
async deleteBot(botId) {
|
|
294
|
+
const warnings = [];
|
|
285
295
|
const config = this.#configStore.get(botId);
|
|
286
296
|
if (!config) throw new Error('Unknown Enterprise WeChat app bot');
|
|
287
297
|
await this.#withBotTransition(botId, async () => {
|
|
288
298
|
const previous = await this.#resolveSecrets(config);
|
|
289
299
|
await this.#stopRuntime(botId);
|
|
290
300
|
try {
|
|
291
|
-
await this.#credentials.unset(config.secretRef);
|
|
292
|
-
await this.#credentials.unset(config.callbackTokenRef);
|
|
293
|
-
await this.#credentials.unset(config.callbackKeyRef);
|
|
294
|
-
await this.#configStore.remove(botId);
|
|
301
|
+
await atConnectionStage('credential.remove', () => this.#credentials.unset(config.secretRef), 'credential-store');
|
|
302
|
+
await atConnectionStage('credential.remove', () => this.#credentials.unset(config.callbackTokenRef), 'credential-store');
|
|
303
|
+
await atConnectionStage('credential.remove', () => this.#credentials.unset(config.callbackKeyRef), 'credential-store');
|
|
304
|
+
await atConnectionStage('account.remove', () => this.#configStore.remove(botId), 'account-config');
|
|
295
305
|
} catch (error) {
|
|
296
|
-
if (
|
|
297
|
-
|
|
298
|
-
|
|
306
|
+
if (!this.#configStore.get(botId)) {
|
|
307
|
+
warnings.push(this.#diagnostics.report(error, { operation: 'bot.delete', stage: 'workspace.cleanup', warning: true,
|
|
308
|
+
publicError: { code: 'workspace-cleanup-failed', message: '账号已移除,但本地状态清理失败。' } }).publicError);
|
|
309
|
+
} else {
|
|
310
|
+
if (previous) {
|
|
311
|
+
await this.#setSecrets(config, previous).catch(() => undefined);
|
|
312
|
+
await this.#startRuntime(config, previous).catch(() => undefined);
|
|
313
|
+
}
|
|
314
|
+
throw new Error('Unable to remove the Enterprise WeChat app bot safely.', { cause: error });
|
|
299
315
|
}
|
|
300
|
-
throw new Error('Unable to remove the Enterprise WeChat app bot safely.', { cause: error });
|
|
301
316
|
}
|
|
302
|
-
await this.#deleteState({ botId, config }).catch(
|
|
303
|
-
this.#logger.warn?.(`[dsh-im:wecom-app] bot ${botId} state cleanup failed:`, error);
|
|
304
|
-
});
|
|
317
|
+
await this.#deleteState({ botId, config }).catch(error => { warnings.push(this.#diagnostics.report(error, { reuse: true, operation: 'bot.delete', stage: 'state.cleanup', resource: 'account-state', warning: true, publicError: { code: 'cleanup-failed', message: '账号已移除,但本地状态清理失败。' } }).publicError); });
|
|
305
318
|
this.#errors.delete(botId);
|
|
306
319
|
this.#touch();
|
|
307
320
|
});
|
|
308
|
-
return this.status();
|
|
321
|
+
return { ...this.status(), ...(warnings.length ? { warnings } : {}) };
|
|
309
322
|
}
|
|
310
323
|
|
|
311
324
|
status() {
|
|
@@ -347,7 +360,7 @@ export class WecomAppController {
|
|
|
347
360
|
messagesReplied: runtimeStatus?.messagesReplied ?? 0,
|
|
348
361
|
},
|
|
349
362
|
lastMessageError: publicMessageFailure(runtimeStatus?.lastMessageError),
|
|
350
|
-
error: structuredClone(this.#errors.get(config.botId) ?? null),
|
|
363
|
+
error: structuredClone(runtimeStatus?.error ?? this.#errors.get(config.botId) ?? null),
|
|
351
364
|
};
|
|
352
365
|
});
|
|
353
366
|
const connectedCount = bots.filter((bot) => bot.connected).length;
|
|
@@ -373,7 +386,7 @@ export class WecomAppController {
|
|
|
373
386
|
if (this.#closed) throw new Error('Enterprise WeChat app controller is closed');
|
|
374
387
|
await this.#stopRuntime(config.botId);
|
|
375
388
|
if (this.#closed) throw new Error('Enterprise WeChat app controller is closed');
|
|
376
|
-
const runtime = await this.#createRuntime({ botId: config.botId, config, secrets });
|
|
389
|
+
const runtime = await atConnectionStage('runtime.prepare', () => this.#createRuntime({ botId: config.botId, config, secrets }));
|
|
377
390
|
if (!runtime || typeof runtime.start !== 'function' || typeof runtime.stop !== 'function') {
|
|
378
391
|
throw new TypeError('createRuntime returned an invalid Enterprise WeChat app runtime');
|
|
379
392
|
}
|
|
@@ -391,7 +404,7 @@ export class WecomAppController {
|
|
|
391
404
|
const runtime = this.#runtimes.get(botId);
|
|
392
405
|
this.#runtimes.delete(botId);
|
|
393
406
|
await runtime?.stop().catch((error) => {
|
|
394
|
-
this.#logger.warn?.(`[dsh-im:wecom-app] bot ${botId} failed to stop cleanly:`, error);
|
|
407
|
+
this.#logger.warn?.(`[dsh-im:wecom-app] bot ${botId} failed to stop cleanly:`, extractConnectionEvidence(error).details);
|
|
395
408
|
});
|
|
396
409
|
}
|
|
397
410
|
|
|
@@ -404,13 +417,13 @@ export class WecomAppController {
|
|
|
404
417
|
}
|
|
405
418
|
|
|
406
419
|
async #setSecrets(identity, { corpSecret, token, encodingAESKey }) {
|
|
407
|
-
await this.#credentials.set(identity.secretRef, corpSecret);
|
|
408
|
-
await this.#credentials.set(identity.callbackTokenRef, token);
|
|
409
|
-
await this.#credentials.set(identity.callbackKeyRef, encodingAESKey);
|
|
420
|
+
await atConnectionStage('credential.save', () => this.#credentials.set(identity.secretRef, corpSecret), 'credential-store');
|
|
421
|
+
await atConnectionStage('credential.save', () => this.#credentials.set(identity.callbackTokenRef, token), 'credential-store');
|
|
422
|
+
await atConnectionStage('credential.save', () => this.#credentials.set(identity.callbackKeyRef, encodingAESKey), 'credential-store');
|
|
410
423
|
}
|
|
411
424
|
|
|
412
425
|
async #resolveSecret(ref) {
|
|
413
|
-
const result = await this.#credentials.resolve(ref)
|
|
426
|
+
const result = await atConnectionStage('credential.read', () => this.#credentials.resolve(ref), 'credential-store');
|
|
414
427
|
return cleanString(result?.value);
|
|
415
428
|
}
|
|
416
429
|
|
|
@@ -419,9 +432,9 @@ export class WecomAppController {
|
|
|
419
432
|
await this.#setSecrets(identity, previous).catch(() => undefined);
|
|
420
433
|
return;
|
|
421
434
|
}
|
|
422
|
-
await this.#credentials.unset(identity.secretRef).catch(() => undefined);
|
|
423
|
-
await this.#credentials.unset(identity.callbackTokenRef).catch(() => undefined);
|
|
424
|
-
await this.#credentials.unset(identity.callbackKeyRef).catch(() => undefined);
|
|
435
|
+
await atConnectionStage('credential.remove', () => this.#credentials.unset(identity.secretRef), 'credential-store').catch(() => undefined);
|
|
436
|
+
await atConnectionStage('credential.remove', () => this.#credentials.unset(identity.callbackTokenRef), 'credential-store').catch(() => undefined);
|
|
437
|
+
await atConnectionStage('credential.remove', () => this.#credentials.unset(identity.callbackKeyRef), 'credential-store').catch(() => undefined);
|
|
425
438
|
}
|
|
426
439
|
|
|
427
440
|
#withBotTransition(botId, operation) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { atConnectionStage } from '../shared/connection-error.mjs';
|
|
1
2
|
import {
|
|
2
3
|
WecomAppApi,
|
|
3
4
|
WecomAppError,
|
|
@@ -104,7 +105,7 @@ export class WecomAppRuntime {
|
|
|
104
105
|
this.#status.startedAt = new Date().toISOString();
|
|
105
106
|
this.#status.lastError = null;
|
|
106
107
|
this.#status.wecomAppConnectionState = 'connecting';
|
|
107
|
-
await this.#harness.ensureRunning({ signal: this.#signal() });
|
|
108
|
+
await atConnectionStage('harness.check', () => this.#harness.ensureRunning({ signal: this.#signal() }));
|
|
108
109
|
this.#status.harnessReachable = true;
|
|
109
110
|
|
|
110
111
|
this.#api = new WecomAppApi({
|