@xmanrui/dsh-im 4.21.2 → 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 +10 -5
- package/README.md +10 -5
- package/docs/client-integration.md +81 -0
- package/lib/client.js +2565 -1134
- package/lib/index.js +292 -301
- package/package.json +9 -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 +62 -0
- package/plugin-src/client/index.js +122 -28
- package/plugin-src/client/last-message-error.js +2 -1
- package/plugin-src/client/panel-error-boundary.js +21 -0
- package/plugin-src/client/styles.js +6 -0
- package/plugin-src/host/build.mjs +4 -0
- package/plugin-src/host/channels/dingtalk/production.mjs +5 -1
- 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 +6 -1
- 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 +5 -1
- package/plugin-src/host/channels/qq/rpc.mjs +5 -3
- package/plugin-src/host/channels/shared/production.mjs +55 -6
- 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 +5 -1
- package/plugin-src/host/channels/slack/rpc.mjs +5 -3
- package/plugin-src/host/channels/wecom/production.mjs +5 -1
- package/plugin-src/host/channels/wecom/rpc.mjs +5 -3
- package/plugin-src/host/channels/wecom-app/production.mjs +5 -1
- package/plugin-src/host/channels/wecom-app/rpc.mjs +5 -3
- package/plugin-src/host/channels/weixin/production.mjs +5 -1
- package/plugin-src/host/channels/whatsapp/production.mjs +5 -1
- 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 +37 -21
- 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/default-workspace.mjs +28 -0
- package/src/channels/shared/diagnostic-details.mjs +95 -0
- package/src/channels/shared/harness-client.mjs +111 -20
- package/src/channels/shared/harness-session-binding.mjs +13 -8
- 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 {
|
|
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({
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import packageInfo from '../../../package.json' with { type: 'json' };
|
|
1
|
+
import { createConnectionDiagnostics, connectionHint } from '../shared/connection-error.mjs';
|
|
3
2
|
import { t } from '../shared/i18n.mjs';
|
|
4
3
|
import { CONFIG_ISSUE_LABELS, normalizeWeixinDiagnosticDetails } from './diagnostic-details.mjs';
|
|
5
4
|
import { configReadErrorDetails } from '../shared/config-read-error.mjs';
|
|
@@ -73,7 +72,7 @@ const MESSAGES = Object.freeze({
|
|
|
73
72
|
});
|
|
74
73
|
|
|
75
74
|
const ownedStages = new WeakMap();
|
|
76
|
-
|
|
75
|
+
|
|
77
76
|
const CODE_STAGES = {
|
|
78
77
|
'credential-read-failed': ['credential.read', 'credential-store'],
|
|
79
78
|
'credential-save-failed': ['credential.save', 'credential-store'],
|
|
@@ -97,7 +96,7 @@ export function knownWeixinErrorCode(code) { return Object.hasOwn(MESSAGES, code
|
|
|
97
96
|
|
|
98
97
|
/** Annotate at the operation that knows the failing resource; do not inspect exception prose. */
|
|
99
98
|
export function weixinStageError(code, cause, stage) {
|
|
100
|
-
if (
|
|
99
|
+
if (cause?.publicError) return cause;
|
|
101
100
|
const error = new Error('Weixin operation failed', { cause });
|
|
102
101
|
error.code = knownWeixinErrorCode(code) ? code : 'weixin-operation-failed';
|
|
103
102
|
const defaults = CODE_STAGES[error.code] ?? [];
|
|
@@ -105,17 +104,6 @@ export function weixinStageError(code, cause, stage) {
|
|
|
105
104
|
return error;
|
|
106
105
|
}
|
|
107
106
|
|
|
108
|
-
function chainOf(error) {
|
|
109
|
-
const chain = [];
|
|
110
|
-
const seen = new Set();
|
|
111
|
-
while (error && typeof error === 'object' && !seen.has(error) && chain.length < 4) {
|
|
112
|
-
seen.add(error);
|
|
113
|
-
chain.push(error);
|
|
114
|
-
error = error.cause;
|
|
115
|
-
}
|
|
116
|
-
return chain;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
107
|
function hintFor(code, details) {
|
|
120
108
|
if (details.rollback === 'failed' || details.rollback === 'unknown') return t('原账号状态尚未确认,请重新读取状态,并反馈诊断信息。');
|
|
121
109
|
if (details.rollback === 'succeeded') return t('操作未完成,已恢复之前的本机状态。请处理上述问题后重试。');
|
|
@@ -136,84 +124,30 @@ function hintFor(code, details) {
|
|
|
136
124
|
if (code === 'stale-token' || code === 'missing-token') return t('请移除失效接入并重新扫码绑定。');
|
|
137
125
|
if (details.httpStatus === 429) return t('微信服务限流,请稍后重试。');
|
|
138
126
|
if (details.httpStatus === 401 || details.httpStatus === 403) return t('访问被拒绝,请检查服务访问限制;HTTP 状态本身不能确认登录凭据已失效。');
|
|
139
|
-
if (code === 'network-error' || code === 'timeout'
|
|
127
|
+
if (code === 'network-error' || code === 'timeout' || details.reason === 'multiple-causes') return connectionHint(details);
|
|
140
128
|
return t('请按参考号查看 DSH 日志,并复制诊断信息反馈。');
|
|
141
129
|
}
|
|
142
130
|
|
|
143
|
-
export function createWeixinDiagnostics(
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
});
|
|
166
|
-
const botId = /^wx_[a-f0-9]{24}$/.test(context.botId ?? '') ? context.botId : undefined;
|
|
167
|
-
const key = JSON.stringify([botId, details.operation, details.stage, code, details.reason, details.httpStatus, details.providerCode,
|
|
168
|
-
details.resource, details.file, details.field, details.issue]);
|
|
169
|
-
const previous = recent.get(key);
|
|
170
|
-
const time = now();
|
|
171
|
-
if (context.automatic && previous && time - previous.time < 60_000) {
|
|
172
|
-
previous.retries += 1;
|
|
173
|
-
const wrapped = new Error(previous.publicError.message, { cause });
|
|
174
|
-
wrapped.code = code;
|
|
175
|
-
wrapped.publicError = structuredClone(previous.publicError);
|
|
176
|
-
reportedErrors.set(wrapped, true);
|
|
177
|
-
return wrapped;
|
|
178
|
-
}
|
|
179
|
-
if (previous?.retries) write('warn', { event: 'connection-retries', referenceId: previous.publicError.details.referenceId, retries: previous.retries });
|
|
180
|
-
details.referenceId = `WX-CONN-${randomUUID().replaceAll('-', '').slice(0, 8).toUpperCase()}`;
|
|
181
|
-
details.occurredAt = new Date(time).toISOString();
|
|
182
|
-
details.hint = hintFor(code, details);
|
|
183
|
-
const message = code === 'weixin-startup-config-invalid' && details.file
|
|
184
|
-
? t('微信配置格式错误:{file}。请查看诊断详情,修复后重启 DSH。', { file: details.file })
|
|
185
|
-
: t(MESSAGES[code], { status: details.httpStatus ?? '?' });
|
|
186
|
-
const publicError = { code, message, details };
|
|
187
|
-
const wrapped = new Error(publicError.message, { cause });
|
|
188
|
-
wrapped.code = code;
|
|
189
|
-
wrapped.publicError = publicError;
|
|
190
|
-
reportedErrors.set(wrapped, true);
|
|
191
|
-
if (context.automatic) {
|
|
192
|
-
if (recent.size >= 128) recent.delete(recent.keys().next().value);
|
|
193
|
-
recent.set(key, { time, publicError: structuredClone(publicError), retries: 0, botId });
|
|
194
|
-
}
|
|
195
|
-
write(context.warning ? 'warn' : 'error', {
|
|
196
|
-
event: context.warning ? 'connection-warning' : 'connection-failure', code, ...details,
|
|
197
|
-
...(botId ? { botId } : {}), pluginVersion: packageInfo.version, nodeVersion: process.versions.node, platform: process.platform,
|
|
198
|
-
...(/^WX-CONN-[A-F0-9]{8}$/.test(context.parentReferenceId ?? '') ? { parentReferenceId: context.parentReferenceId } : {}),
|
|
199
|
-
...Object.fromEntries(['durationMs', 'timeoutMs'].filter(field => Number.isFinite(cause?.[field]) && cause[field] >= 0).map(field => [field, cause[field]])),
|
|
200
|
-
});
|
|
201
|
-
return wrapped;
|
|
202
|
-
}
|
|
203
|
-
function clear(botId) {
|
|
204
|
-
for (const [key, entry] of recent) {
|
|
205
|
-
if (botId !== undefined && entry.botId !== botId) continue;
|
|
206
|
-
if (entry.retries) write('info', { event: 'connection-retries', referenceId: entry.publicError.details.referenceId, retries: entry.retries });
|
|
207
|
-
recent.delete(key);
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
function outcome(error, rollback) {
|
|
211
|
-
if (!reportedErrors.has(error)) return;
|
|
212
|
-
const details = normalizeWeixinDiagnosticDetails({ rollback });
|
|
213
|
-
if (!details.rollback) return;
|
|
214
|
-
error.publicError.details.rollback = details.rollback;
|
|
215
|
-
error.publicError.details.hint = hintFor(error.code, error.publicError.details);
|
|
216
|
-
write('info', { event: 'connection-outcome', referenceId: error.publicError.details.referenceId, ...details });
|
|
217
|
-
}
|
|
218
|
-
return { report, clear, outcome };
|
|
131
|
+
export function createWeixinDiagnostics(options = {}) {
|
|
132
|
+
return createConnectionDiagnostics({ ...options, channel: 'weixin', prefix: 'WX-CONN',
|
|
133
|
+
describe(cause, context, evidence) {
|
|
134
|
+
const chain = evidence.chain;
|
|
135
|
+
const selected = chain.find(error => knownWeixinErrorCode(error.code));
|
|
136
|
+
const code = selected?.code ?? (knownWeixinErrorCode(context.code) ? context.code : 'weixin-operation-failed');
|
|
137
|
+
const staged = chain.map(error => ownedStages.get(error)).find(Boolean) ?? {};
|
|
138
|
+
const configDetails = chain.map(configReadErrorDetails).find(Boolean) ?? {};
|
|
139
|
+
const defaults = CODE_STAGES[code] ?? [];
|
|
140
|
+
const details = normalizeWeixinDiagnosticDetails({
|
|
141
|
+
...context, ...evidence.details, ...configDetails,
|
|
142
|
+
file: { 'account-config': 'config.json', 'workspace-config': 'workspaces.json' }[configDetails.resource],
|
|
143
|
+
stage: staged.stage ?? defaults[0] ?? (code.startsWith('harness-') ? 'harness.check' : context.stage),
|
|
144
|
+
resource: staged.resource ?? defaults[1] ?? configDetails.resource ?? context.resource,
|
|
145
|
+
reason: evidence.details.reason === 'unknown' ? configDetails.reason ?? context.reason ?? 'unknown' : evidence.details.reason,
|
|
146
|
+
});
|
|
147
|
+
const message = code === 'weixin-startup-config-invalid' && details.file
|
|
148
|
+
? t('微信配置格式错误:{file}。请查看诊断详情,修复后重启 DSH。', { file: details.file })
|
|
149
|
+
: t(MESSAGES[code], { status: details.httpStatus ?? '?' });
|
|
150
|
+
return { publicError: { code, message }, details, hint: hintFor(code, details) };
|
|
151
|
+
},
|
|
152
|
+
});
|
|
219
153
|
}
|
|
@@ -1,63 +1 @@
|
|
|
1
|
-
|
|
2
|
-
const OPERATIONS = new Set([
|
|
3
|
-
'startup', 'connection.restore', 'connection.monitor', 'connection.close', 'connection.status',
|
|
4
|
-
'provision.begin', 'provision.poll', 'provision.verify', 'provision.cancel', 'bot.reconnect', 'bot.delete',
|
|
5
|
-
'bot.workspace.set', 'bot.model.set', 'bot.agent-preset.set', 'bot.context-enhancement.set', 'bot.access-policy.set', 'bot.alias.set',
|
|
6
|
-
]);
|
|
7
|
-
const STAGES = new Set([
|
|
8
|
-
'startup.load', 'qr.begin', 'qr.encode', 'qr.poll', 'qr.verify', 'qr.cancel', 'credential.read', 'credential.save', 'credential.remove',
|
|
9
|
-
'account.save', 'account.remove', 'state.load', 'state.write', 'state.cleanup', 'workspace.write', 'workspace.cleanup',
|
|
10
|
-
'runtime.prepare', 'activation', 'harness.check', 'connection.start', 'connection.poll', 'connection.stop', 'status.read', 'rollback', 'management.request',
|
|
11
|
-
]);
|
|
12
|
-
const REASONS = new Set([
|
|
13
|
-
'ENOTFOUND', 'EAI_AGAIN', 'ECONNREFUSED', 'ECONNRESET', 'EHOSTUNREACH', 'ENETUNREACH', 'ETIMEDOUT', 'EPIPE',
|
|
14
|
-
'UND_ERR_CONNECT_TIMEOUT', 'UND_ERR_HEADERS_TIMEOUT', 'UND_ERR_BODY_TIMEOUT', 'UND_ERR_SOCKET',
|
|
15
|
-
'CERT_HAS_EXPIRED', 'CERT_NOT_YET_VALID', 'DEPTH_ZERO_SELF_SIGNED_CERT', 'SELF_SIGNED_CERT_IN_CHAIN',
|
|
16
|
-
'UNABLE_TO_VERIFY_LEAF_SIGNATURE', 'UNABLE_TO_GET_ISSUER_CERT_LOCALLY', 'ERR_TLS_CERT_ALTNAME_INVALID', 'ERR_SSL_WRONG_VERSION_NUMBER',
|
|
17
|
-
'ENOENT', 'EACCES', 'EPERM', 'ENOSPC', 'EROFS', 'ENOTDIR', 'EISDIR', 'EBUSY', 'EIO', 'EXDEV', 'EMFILE', 'ENFILE', 'EEXIST',
|
|
18
|
-
'invalid-json', 'invalid-config', 'read-only',
|
|
19
|
-
]);
|
|
20
|
-
const RESOURCES = new Set(['credential-store', 'account-config', 'account-state', 'workspace-config', 'workspace-directory']);
|
|
21
|
-
const CONFIG_FILES = new Set(['config.json', 'workspaces.json']);
|
|
22
|
-
export const CONFIG_ISSUE_LABELS = Object.freeze({
|
|
23
|
-
'expected-object': '应为 JSON 对象。',
|
|
24
|
-
'expected-array': '应为 JSON 数组。',
|
|
25
|
-
'unsupported-version': '配置版本缺失或不受当前插件支持。',
|
|
26
|
-
'invalid-string': '字段缺失或不是非空字符串。',
|
|
27
|
-
'invalid-identifier': '标识符格式不符合要求。',
|
|
28
|
-
'identity-mismatch': '标识符与 accountId 派生结果不一致。',
|
|
29
|
-
'duplicate-identity': '账号标识重复。',
|
|
30
|
-
'invalid-api-url': '微信服务地址不是有效 URL。',
|
|
31
|
-
'untrusted-api-url': '微信服务地址必须使用受信任的 HTTPS 域名和端口。',
|
|
32
|
-
'invalid-workspace-path': '工作区路径必须是当前操作系统的绝对路径。',
|
|
33
|
-
'invalid-agent-preset': 'Agent Preset 标识无效。',
|
|
34
|
-
'invalid-model-selection': '模型设置须包含有效的 provider、model 和可选 reasoningEffort。',
|
|
35
|
-
'invalid-delivery-target': '投递目标的标识、结构或路由不符合当前配置版本要求。',
|
|
36
|
-
'unexpected-field': '当前配置版本不允许此字段。',
|
|
37
|
-
});
|
|
38
|
-
// Only schema-owned field names and numeric entry positions may leave the Host.
|
|
39
|
-
// Map keys are replaced with zero-based positions to avoid exposing identities.
|
|
40
|
-
const CONFIG_FIELD = /^(?:\$|version|accounts(?:\[\d{1,10}\](?:\.(?:accountId|ownerUserId|botId|tokenRef|baseUrl))?)?|(?:workspaces|agentPresets|models)(?:\[\d{1,10}\]\.(?:key|value))?|deliveryTargets(?:\[\d{1,10}\]\.(?:key|targets(?:\[\d{1,10}\])?))?)$/;
|
|
41
|
-
|
|
42
|
-
export function normalizeWeixinDiagnosticDetails(value) {
|
|
43
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) return {};
|
|
44
|
-
const result = {};
|
|
45
|
-
for (const [field, allowed] of [['operation', OPERATIONS], ['stage', STAGES], ['reason', REASONS], ['resource', RESOURCES]]) {
|
|
46
|
-
if (allowed.has(value[field])) result[field] = value[field];
|
|
47
|
-
}
|
|
48
|
-
if (CONFIG_FILES.has(value.file)) result.file = value.file;
|
|
49
|
-
if (typeof value.field === 'string' && CONFIG_FIELD.test(value.field)) result.field = value.field;
|
|
50
|
-
if (typeof value.issue === 'string' && Object.hasOwn(CONFIG_ISSUE_LABELS, value.issue)) result.issue = value.issue;
|
|
51
|
-
if (/^WX-CONN-[A-F0-9]{8}$/.test(value.referenceId ?? '')) result.referenceId = value.referenceId;
|
|
52
|
-
if (typeof value.occurredAt === 'string' && /^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d\.\d{3}Z$/.test(value.occurredAt)
|
|
53
|
-
&& Number.isFinite(Date.parse(value.occurredAt))) result.occurredAt = value.occurredAt;
|
|
54
|
-
if (Number.isInteger(value.httpStatus) && value.httpStatus >= 100 && value.httpStatus <= 599) result.httpStatus = value.httpStatus;
|
|
55
|
-
const provider = typeof value.providerCode === 'number' && Number.isSafeInteger(value.providerCode)
|
|
56
|
-
? String(value.providerCode) : value.providerCode;
|
|
57
|
-
if (typeof provider === 'string' && /^-?\d{1,12}$/.test(provider)) result.providerCode = provider;
|
|
58
|
-
if (typeof value.pluginVersion === 'string' && /^\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$/.test(value.pluginVersion)
|
|
59
|
-
&& value.pluginVersion.length <= 64) result.pluginVersion = value.pluginVersion;
|
|
60
|
-
if (['succeeded', 'failed', 'not-attempted', 'unknown'].includes(value.rollback)) result.rollback = value.rollback;
|
|
61
|
-
if (typeof value.hint === 'string' && value.hint.trim()) result.hint = value.hint.trim().slice(0, 500);
|
|
62
|
-
return result;
|
|
63
|
-
}
|
|
1
|
+
export { CONFIG_ISSUE_LABELS, normalizeDiagnosticDetails as normalizeWeixinDiagnosticDetails } from '../shared/diagnostic-details.mjs';
|
|
@@ -63,6 +63,7 @@ import {
|
|
|
63
63
|
} from '../shared/semantic/delivery.mjs';
|
|
64
64
|
import { recoverAssistantTextByTimestamp } from '../shared/session-reply-recovery.mjs';
|
|
65
65
|
import {
|
|
66
|
+
messageFailureDiagnostic,
|
|
66
67
|
channelDeliveryFailure,
|
|
67
68
|
clearLastMessageFailure,
|
|
68
69
|
messageFailureText,
|
|
@@ -467,7 +468,7 @@ export class WeixinHarnessBridge {
|
|
|
467
468
|
const failure = setLastMessageFailure(this.#status, error);
|
|
468
469
|
this.#logger.error?.(
|
|
469
470
|
`[dsh-weixin] failed to process a command [${failure.referenceId}]:`,
|
|
470
|
-
error,
|
|
471
|
+
messageFailureDiagnostic(error, failure),
|
|
471
472
|
);
|
|
472
473
|
return this.#sendOutOfBand(key, sender, messageFailureText(failure), contextToken, runId)
|
|
473
474
|
.catch(() => undefined);
|
|
@@ -623,7 +624,7 @@ export class WeixinHarnessBridge {
|
|
|
623
624
|
const failure = setLastMessageFailure(this.#status, error);
|
|
624
625
|
this.#logger.error?.(
|
|
625
626
|
`[dsh-weixin] failed to process a batch input message [${failure.referenceId}]:`,
|
|
626
|
-
error,
|
|
627
|
+
messageFailureDiagnostic(error, failure),
|
|
627
628
|
);
|
|
628
629
|
await this.#sendOutOfBand(key, sender, messageFailureText(failure), contextToken, runId)
|
|
629
630
|
.catch(() => undefined);
|
|
@@ -814,7 +815,7 @@ export class WeixinHarnessBridge {
|
|
|
814
815
|
await this.#startTyping(sender, contextToken);
|
|
815
816
|
try {
|
|
816
817
|
let content = hasImages || hasReply
|
|
817
|
-
? await promptContentForInboundMessage(promptMessage, { signal: this.#signal })
|
|
818
|
+
? await promptContentForInboundMessage(promptMessage, { signal: this.#signal, deferImages: true })
|
|
818
819
|
: undefined;
|
|
819
820
|
const snapshot = this.#acceptedMessageIds.get(messageId);
|
|
820
821
|
let contextEnhanced = false;
|
|
@@ -857,6 +858,7 @@ export class WeixinHarnessBridge {
|
|
|
857
858
|
await this.#resumeTyping(key, sender, contextToken);
|
|
858
859
|
},
|
|
859
860
|
files: promptMessage.files,
|
|
861
|
+
images: promptMessage.images,
|
|
860
862
|
},
|
|
861
863
|
}));
|
|
862
864
|
if (batchSubmission) {
|
|
@@ -934,7 +936,7 @@ export class WeixinHarnessBridge {
|
|
|
934
936
|
});
|
|
935
937
|
this.#logger.error?.(
|
|
936
938
|
`[dsh-weixin] failed to process an inbound message [${failure.referenceId}]:`,
|
|
937
|
-
error,
|
|
939
|
+
messageFailureDiagnostic(error, failure),
|
|
938
940
|
);
|
|
939
941
|
try {
|
|
940
942
|
await this.#send(
|
|
@@ -1251,7 +1253,7 @@ export class WeixinHarnessBridge {
|
|
|
1251
1253
|
const failure = setLastMessageFailure(this.#status, error);
|
|
1252
1254
|
this.#logger.error?.(
|
|
1253
1255
|
`[dsh-weixin] failed to process an interaction reply [${failure.referenceId}]:`,
|
|
1254
|
-
error,
|
|
1256
|
+
messageFailureDiagnostic(error, failure),
|
|
1255
1257
|
);
|
|
1256
1258
|
if (!this.#state.hasSeen(messageId)) {
|
|
1257
1259
|
await this.#state.markSeen(messageId).catch(() => undefined);
|