@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, createConnectionDiagnostics, atConnectionStage } from '../shared/connection-error.mjs';
|
|
1
2
|
import { randomInt } from 'node:crypto';
|
|
2
3
|
|
|
3
4
|
import { createEditableMessageStream, splitMessageText } from '../shared/editable-message-stream.mjs';
|
|
@@ -451,7 +452,7 @@ class TelegramDeliveryStream {
|
|
|
451
452
|
try {
|
|
452
453
|
return await this.#update(block);
|
|
453
454
|
} catch (error) {
|
|
454
|
-
this.#logger.warn?.('[dsh-im:telegram] rich stream update failed:', error);
|
|
455
|
+
this.#logger.warn?.('[dsh-im:telegram] rich stream update failed:', extractConnectionEvidence(error).details);
|
|
455
456
|
return undefined;
|
|
456
457
|
}
|
|
457
458
|
});
|
|
@@ -467,7 +468,7 @@ class TelegramDeliveryStream {
|
|
|
467
468
|
try {
|
|
468
469
|
return await this.#update(this.#lastBlock);
|
|
469
470
|
} catch (error) {
|
|
470
|
-
this.#logger.warn?.('[dsh-im:telegram] rich stream refresh failed:', error);
|
|
471
|
+
this.#logger.warn?.('[dsh-im:telegram] rich stream refresh failed:', extractConnectionEvidence(error).details);
|
|
471
472
|
return undefined;
|
|
472
473
|
}
|
|
473
474
|
});
|
|
@@ -926,6 +927,7 @@ export class TelegramRuntime {
|
|
|
926
927
|
#contextEnhancement;
|
|
927
928
|
#accessPolicy;
|
|
928
929
|
#logger;
|
|
930
|
+
#diagnostics;
|
|
929
931
|
#replyTimeoutMs;
|
|
930
932
|
#createApi;
|
|
931
933
|
#createHttpTransport;
|
|
@@ -968,7 +970,7 @@ export class TelegramRuntime {
|
|
|
968
970
|
this.#state = state;
|
|
969
971
|
this.#contextEnhancement = contextEnhancement;
|
|
970
972
|
this.#accessPolicy = accessPolicy;
|
|
971
|
-
this.#logger = logger;
|
|
973
|
+
this.#logger = logger; this.#diagnostics = createConnectionDiagnostics({ channel: 'telegram', logger });
|
|
972
974
|
this.#replyTimeoutMs = replyTimeoutMs;
|
|
973
975
|
this.#createApi = createApi;
|
|
974
976
|
this.#createHttpTransport = createHttpTransport;
|
|
@@ -1026,8 +1028,8 @@ export class TelegramRuntime {
|
|
|
1026
1028
|
this.#connecting = true;
|
|
1027
1029
|
this.#status.startedAt = new Date().toISOString();
|
|
1028
1030
|
this.#status.connectionState = 'connecting';
|
|
1029
|
-
this.#status.lastError = null;
|
|
1030
|
-
await this.#harness.ensureRunning();
|
|
1031
|
+
this.#status.lastError = null; this.#status.error = null; this.#diagnostics.clear();
|
|
1032
|
+
await atConnectionStage('harness.check', () => this.#harness.ensureRunning());
|
|
1031
1033
|
this.#status.harnessReachable = true;
|
|
1032
1034
|
|
|
1033
1035
|
const controller = new AbortController();
|
|
@@ -1057,7 +1059,7 @@ export class TelegramRuntime {
|
|
|
1057
1059
|
} catch (error) {
|
|
1058
1060
|
this.#logger.warn?.(
|
|
1059
1061
|
`[dsh-im:telegram] bot ${this.#config.botId} command menu setup failed:`,
|
|
1060
|
-
error,
|
|
1062
|
+
extractConnectionEvidence(error).details,
|
|
1061
1063
|
);
|
|
1062
1064
|
}
|
|
1063
1065
|
const client = new TelegramBotClient({
|
|
@@ -1099,7 +1101,7 @@ export class TelegramRuntime {
|
|
|
1099
1101
|
} catch (error) {
|
|
1100
1102
|
this.#logger.warn?.(
|
|
1101
1103
|
`[dsh-im:telegram] bot ${this.#config.botId} command menu catch-up failed:`,
|
|
1102
|
-
error,
|
|
1104
|
+
extractConnectionEvidence(error).details,
|
|
1103
1105
|
);
|
|
1104
1106
|
}
|
|
1105
1107
|
}
|
|
@@ -1108,14 +1110,16 @@ export class TelegramRuntime {
|
|
|
1108
1110
|
if (controller.signal.aborted) return;
|
|
1109
1111
|
this.#status.ready = false;
|
|
1110
1112
|
this.#status.connectionState = 'failed';
|
|
1111
|
-
this.#status.
|
|
1112
|
-
this.#
|
|
1113
|
+
this.#status.error = this.#diagnostics.report(error, { operation: 'connection.monitor', botId: this.#config?.botId, automatic: true }).publicError;
|
|
1114
|
+
this.#status.lastError = this.#status.error.message;
|
|
1115
|
+
|
|
1113
1116
|
});
|
|
1114
1117
|
return this.status;
|
|
1115
1118
|
} catch (error) {
|
|
1116
1119
|
this.#status.ready = false;
|
|
1117
1120
|
this.#status.connectionState = 'failed';
|
|
1118
|
-
this.#status.
|
|
1121
|
+
this.#status.error = this.#diagnostics.report(error, { operation: 'connection.restore', reuse: true, botId: this.#config?.botId, automatic: true }).publicError;
|
|
1122
|
+
this.#status.lastError = this.#status.error.message;
|
|
1119
1123
|
await this.stop();
|
|
1120
1124
|
throw error;
|
|
1121
1125
|
} finally {
|
|
@@ -1160,7 +1164,7 @@ export class TelegramRuntime {
|
|
|
1160
1164
|
} catch (error) {
|
|
1161
1165
|
this.#logger.warn?.(
|
|
1162
1166
|
`[dsh-im:telegram] bot ${this.#config.botId} command menu refresh failed:`,
|
|
1163
|
-
error,
|
|
1167
|
+
extractConnectionEvidence(error).details,
|
|
1164
1168
|
);
|
|
1165
1169
|
return false;
|
|
1166
1170
|
}
|
|
@@ -1210,7 +1214,7 @@ export class TelegramRuntime {
|
|
|
1210
1214
|
if (signal.aborted) return;
|
|
1211
1215
|
this.#logger.error?.(
|
|
1212
1216
|
`[dsh-im:telegram] bot ${this.#config.botId} callback handling failed:`,
|
|
1213
|
-
error,
|
|
1217
|
+
extractConnectionEvidence(error).details,
|
|
1214
1218
|
);
|
|
1215
1219
|
});
|
|
1216
1220
|
}
|
|
@@ -1230,7 +1234,7 @@ export class TelegramRuntime {
|
|
|
1230
1234
|
if (signal.aborted) return;
|
|
1231
1235
|
this.#logger.error?.(
|
|
1232
1236
|
`[dsh-im:telegram] bot ${this.#config.botId} message handling failed:`,
|
|
1233
|
-
error,
|
|
1237
|
+
extractConnectionEvidence(error).details,
|
|
1234
1238
|
);
|
|
1235
1239
|
});
|
|
1236
1240
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { imageInputLimits } from '../shared/image-input-policy.mjs';
|
|
2
|
+
import { imageDownloadLimitMessage } from '../shared/image-prompt.mjs';
|
|
1
3
|
import { createDeferredDeliveryCoordinator, deferredOutcomeText } from '../shared/deferred-delivery-coordinator.mjs';
|
|
2
4
|
import { generateReqId } from '@wecom/aibot-node-sdk';
|
|
3
5
|
import { randomUUID } from 'node:crypto';
|
|
@@ -216,7 +218,7 @@ function imageSource(client, image) {
|
|
|
216
218
|
throw new ImagePromptError(
|
|
217
219
|
'image-too-large',
|
|
218
220
|
`Enterprise WeChat image exceeds ${maxBytes} bytes`,
|
|
219
|
-
|
|
221
|
+
imageDownloadLimitMessage(maxBytes),
|
|
220
222
|
);
|
|
221
223
|
}
|
|
222
224
|
return { data, name: result?.filename };
|
|
@@ -280,12 +282,17 @@ function prefetchInboundFiles(message, signal) {
|
|
|
280
282
|
};
|
|
281
283
|
}
|
|
282
284
|
|
|
283
|
-
function prefetchInboundImages(message, signal) {
|
|
285
|
+
function prefetchInboundImages(message, signal, limitsPromise) {
|
|
284
286
|
if (!hasInboundImages(message)) return message;
|
|
285
287
|
return {
|
|
286
288
|
...message,
|
|
287
289
|
images: message.images.map((source) => {
|
|
288
|
-
const download =
|
|
290
|
+
const download = Promise.resolve(limitsPromise).then((limits) => {
|
|
291
|
+
if (message.images.length > limits.maxImages) throw new ImagePromptError(
|
|
292
|
+
'too-many-images', 'Too many inbound images',
|
|
293
|
+
t('一次最多只能处理 {maxImages} 张图片。', { maxImages: limits.maxImages }));
|
|
294
|
+
return source.load({ signal, maxBytes: limits.maxDownloadBytes });
|
|
295
|
+
});
|
|
289
296
|
// The conversation queue may not consume this promise immediately. Keep
|
|
290
297
|
// an attached rejection handler while preserving the original outcome.
|
|
291
298
|
download.catch(() => undefined);
|
|
@@ -301,7 +308,7 @@ function prefetchInboundImages(message, signal) {
|
|
|
301
308
|
throw new ImagePromptError(
|
|
302
309
|
'image-too-large',
|
|
303
310
|
`Enterprise WeChat image exceeds ${maxBytes} bytes`,
|
|
304
|
-
|
|
311
|
+
imageDownloadLimitMessage(maxBytes),
|
|
305
312
|
);
|
|
306
313
|
}
|
|
307
314
|
return result;
|
|
@@ -1060,7 +1067,8 @@ export class WecomHarnessBridge {
|
|
|
1060
1067
|
if (this.#prefetchedImageCount + imageCount <= MAX_PREFETCHED_IMAGES) {
|
|
1061
1068
|
reservedImages = imageCount;
|
|
1062
1069
|
this.#prefetchedImageCount += reservedImages;
|
|
1063
|
-
preparedMessage = prefetchInboundImages(inboundMessage, this.#signal
|
|
1070
|
+
preparedMessage = prefetchInboundImages(inboundMessage, this.#signal,
|
|
1071
|
+
this.#harness.getImageInputLimits?.() ?? imageInputLimits());
|
|
1064
1072
|
} else {
|
|
1065
1073
|
preparedMessage = imageQueueFullMessage(inboundMessage);
|
|
1066
1074
|
}
|
|
@@ -1356,7 +1364,7 @@ export class WecomHarnessBridge {
|
|
|
1356
1364
|
}
|
|
1357
1365
|
|
|
1358
1366
|
let content = hasImages || hasReply
|
|
1359
|
-
? await promptContentForInboundMessage(message, { signal: this.#signal })
|
|
1367
|
+
? await promptContentForInboundMessage(message, { signal: this.#signal, deferImages: true })
|
|
1360
1368
|
: undefined;
|
|
1361
1369
|
const snapshot = this.#acceptedMessageIds.get(messageId);
|
|
1362
1370
|
let contextEnhanced = false;
|
|
@@ -1408,6 +1416,7 @@ export class WecomHarnessBridge {
|
|
|
1408
1416
|
}),
|
|
1409
1417
|
onInteractionResolved: (resolution) => this.#handleInteractionResolved(resolution),
|
|
1410
1418
|
files: message.files,
|
|
1419
|
+
images: message.images,
|
|
1411
1420
|
},
|
|
1412
1421
|
});
|
|
1413
1422
|
if (batchSubmission) {
|
|
@@ -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);
|