@xmanrui/dsh-im 4.22.0 → 4.24.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 +5 -4
- package/README.md +5 -4
- package/lib/client.js +2584 -1153
- package/lib/index.js +290 -298
- package/package.json +14 -2
- 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 +23 -7
- 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 +8 -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/telegram/index.js +3 -0
- package/plugin-src/client/channels/telegram/styles.js +12 -0
- package/plugin-src/client/channels/telegram/thinking-traces.js +25 -0
- 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 +69 -1
- package/plugin-src/client/index.js +40 -4
- package/plugin-src/client/last-message-error.js +2 -1
- package/plugin-src/client/styles.js +8 -2
- 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 +14 -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/shared/thinking-traces-rpc.mjs +11 -0
- 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/host/modern-harness-api.mjs +7 -2
- package/plugin-src/management-rpc.mjs +12 -2
- package/scripts/verify-package.mjs +13 -5
- 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 +699 -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 +604 -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 +39 -11
- package/src/channels/feishu/feishu-channel.mjs +35 -0
- package/src/channels/feishu/feishu-runtime.mjs +15 -8
- package/src/channels/feishu/group-message-permission-manager.mjs +2 -1
- package/src/channels/feishu/live-cot.mjs +260 -0
- 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/feishu/slash-command-registry.mjs +17 -0
- package/src/channels/feishu/step-push-mode.mjs +10 -4
- 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 +287 -57
- 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 +150 -25
- 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/config-store.mjs +4 -1
- package/src/channels/telegram/telegram-api.mjs +2 -2
- package/src/channels/telegram/telegram-controller.mjs +13 -1
- package/src/channels/telegram/telegram-runtime.mjs +215 -14
- 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,6 +1,7 @@
|
|
|
1
|
+
import { extractConnectionEvidence, createConnectionDiagnostics, atConnectionStage } from '../shared/connection-error.mjs';
|
|
1
2
|
import { randomInt } from 'node:crypto';
|
|
2
3
|
|
|
3
|
-
import { createEditableMessageStream
|
|
4
|
+
import { createEditableMessageStream } from '../shared/editable-message-stream.mjs';
|
|
4
5
|
import { createTextDeliveryBlock } from '../shared/semantic/delivery.mjs';
|
|
5
6
|
import { t } from '../shared/i18n.mjs';
|
|
6
7
|
import { captureContextEnhancement } from '../shared/context-enhancement.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
|
});
|
|
@@ -498,6 +499,65 @@ class TelegramDeliveryStream {
|
|
|
498
499
|
}
|
|
499
500
|
}
|
|
500
501
|
|
|
502
|
+
// Thinking-trace display rules. Truncation fallback (spoiler / inline-button
|
|
503
|
+
// expansion are follow-ups gated on three-platform client verification):
|
|
504
|
+
// tool traces cap the argument summary, reasoning caps the first paragraph.
|
|
505
|
+
const TOOL_TRACE_ARG_KEYS = Object.freeze([
|
|
506
|
+
'command', 'pattern', 'query', 'url', 'file_path', 'path', 'text', 'prompt', 'message', 'description',
|
|
507
|
+
]);
|
|
508
|
+
const TOOL_TRACE_SUMMARY_LIMIT = 120;
|
|
509
|
+
const THINKING_LINE_LIMIT = 200;
|
|
510
|
+
|
|
511
|
+
function truncateCapped(value, limit) {
|
|
512
|
+
return value.length > limit ? `${value.slice(0, limit)}…` : value;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/** Extract a one-line argument summary from a tool call's `arguments`. */
|
|
516
|
+
export function toolTraceSummary(argumentsValue) {
|
|
517
|
+
let record = null;
|
|
518
|
+
if (argumentsValue && typeof argumentsValue === 'object' && !Array.isArray(argumentsValue)) {
|
|
519
|
+
record = argumentsValue;
|
|
520
|
+
} else if (typeof argumentsValue === 'string') {
|
|
521
|
+
const trimmed = argumentsValue.trim();
|
|
522
|
+
if (!trimmed) return null;
|
|
523
|
+
let parsed = null;
|
|
524
|
+
try {
|
|
525
|
+
parsed = JSON.parse(trimmed);
|
|
526
|
+
} catch {
|
|
527
|
+
return truncateCapped(trimmed.replace(/\s+/g, ' ').trim(), TOOL_TRACE_SUMMARY_LIMIT);
|
|
528
|
+
}
|
|
529
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
530
|
+
record = parsed;
|
|
531
|
+
} else {
|
|
532
|
+
return truncateCapped(trimmed.replace(/\s+/g, ' ').trim(), TOOL_TRACE_SUMMARY_LIMIT);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
if (!record) return null;
|
|
536
|
+
for (const key of TOOL_TRACE_ARG_KEYS) {
|
|
537
|
+
const value = record[key];
|
|
538
|
+
if (typeof value === 'string' && value.trim()) {
|
|
539
|
+
return truncateCapped(value.replace(/\s+/g, ' ').trim(), TOOL_TRACE_SUMMARY_LIMIT);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
return null;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/** Format one tool-trace line: `🔧 <name> → <summary>` (summary optional). */
|
|
546
|
+
export function formatToolTrace(name, argumentsValue) {
|
|
547
|
+
const toolName = typeof name === 'string' && name.trim() ? name.trim() : t('工具');
|
|
548
|
+
const summary = toolTraceSummary(argumentsValue);
|
|
549
|
+
return summary ? `🔧 ${toolName} → ${summary}` : `🔧 ${toolName}`;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/** Format one reasoning line: `💭 <first paragraph>` (200-char cap). */
|
|
553
|
+
export function formatThinkingLine(text) {
|
|
554
|
+
const trimmed = typeof text === 'string' ? text.trim() : '';
|
|
555
|
+
if (!trimmed) return null;
|
|
556
|
+
const firstParagraph = trimmed.split(/\n{2,}/)[0].trim();
|
|
557
|
+
if (!firstParagraph) return null;
|
|
558
|
+
return `💭 ${truncateCapped(firstParagraph, THINKING_LINE_LIMIT)}`;
|
|
559
|
+
}
|
|
560
|
+
|
|
501
561
|
export class TelegramBotClient {
|
|
502
562
|
#api;
|
|
503
563
|
#signal;
|
|
@@ -903,6 +963,142 @@ export class TelegramBotClient {
|
|
|
903
963
|
});
|
|
904
964
|
return stream.start();
|
|
905
965
|
}
|
|
966
|
+
|
|
967
|
+
/**
|
|
968
|
+
* Thinking-trace stream: every intermediate line is a permanent, separate
|
|
969
|
+
* Telegram message (no placeholder editing), so the user watches the
|
|
970
|
+
* "think → act" chain build up. Trace lines are best-effort; the final
|
|
971
|
+
* answer must still land, falling back to plain sendText delivery.
|
|
972
|
+
*/
|
|
973
|
+
openThinkingStream(target) {
|
|
974
|
+
const client = this;
|
|
975
|
+
const providerMessageIds = [];
|
|
976
|
+
let firstMessageId = null;
|
|
977
|
+
let closed = false;
|
|
978
|
+
const logger = this.#logger;
|
|
979
|
+
const warnFailure = (label, error) => {
|
|
980
|
+
logger?.warn?.(`[dsh-im:telegram] thinking stream ${label} failed:`, error);
|
|
981
|
+
};
|
|
982
|
+
const sendLine = async (text) => {
|
|
983
|
+
const message = await this.#api.sendMessage({
|
|
984
|
+
chatId: target.chatId,
|
|
985
|
+
text,
|
|
986
|
+
replyToMessageId: firstMessageId === null ? target.replyToMessageId : undefined,
|
|
987
|
+
messageThreadId: target.messageThreadId,
|
|
988
|
+
signal: this.#signal,
|
|
989
|
+
});
|
|
990
|
+
const id = message?.message_id;
|
|
991
|
+
if (Number.isSafeInteger(id)) {
|
|
992
|
+
if (firstMessageId === null) firstMessageId = id;
|
|
993
|
+
providerMessageIds.push(String(id));
|
|
994
|
+
}
|
|
995
|
+
};
|
|
996
|
+
return {
|
|
997
|
+
get messageId() {
|
|
998
|
+
return firstMessageId;
|
|
999
|
+
},
|
|
1000
|
+
get providerMessageIds() {
|
|
1001
|
+
return [...providerMessageIds];
|
|
1002
|
+
},
|
|
1003
|
+
presentation: 'telegram-thinking',
|
|
1004
|
+
// The chain is made of permanent messages, but long tool runs still
|
|
1005
|
+
// want the typing indicator; the bridge re-sends it every 4 seconds.
|
|
1006
|
+
keepalive: true,
|
|
1007
|
+
update: async () => {},
|
|
1008
|
+
refresh: async () => {},
|
|
1009
|
+
sendLine,
|
|
1010
|
+
sendToolTrace: (name, argumentsValue) => (async () => {
|
|
1011
|
+
if (closed) return;
|
|
1012
|
+
try {
|
|
1013
|
+
await sendLine(formatToolTrace(name, argumentsValue));
|
|
1014
|
+
} catch (error) {
|
|
1015
|
+
warnFailure('tool trace', error);
|
|
1016
|
+
}
|
|
1017
|
+
})(),
|
|
1018
|
+
sendThinking: (text) => (async () => {
|
|
1019
|
+
if (closed) return;
|
|
1020
|
+
const line = formatThinkingLine(text);
|
|
1021
|
+
if (!line) return;
|
|
1022
|
+
try {
|
|
1023
|
+
await sendLine(line);
|
|
1024
|
+
} catch (error) {
|
|
1025
|
+
warnFailure('thinking line', error);
|
|
1026
|
+
}
|
|
1027
|
+
})(),
|
|
1028
|
+
async finish(answer) {
|
|
1029
|
+
if (closed) throw new Error('Message stream is already closed');
|
|
1030
|
+
closed = true;
|
|
1031
|
+
const format = answer && typeof answer === 'object' && answer.format === 'markdown'
|
|
1032
|
+
? 'markdown'
|
|
1033
|
+
: 'plain';
|
|
1034
|
+
const text = answer && typeof answer === 'object' && typeof answer.text === 'string'
|
|
1035
|
+
? answer.text
|
|
1036
|
+
: typeof answer === 'string' ? answer : '';
|
|
1037
|
+
const trimmed = text.trim();
|
|
1038
|
+
const answerText = trimmed || t('处理完成。');
|
|
1039
|
+
// The final answer keeps the channel's normal delivery path, so a
|
|
1040
|
+
// markdown answer renders rich (bold, code fences) exactly like
|
|
1041
|
+
// non-trace mode; trace lines above stay plain permanent messages.
|
|
1042
|
+
// Markdown goes to #sendRich in one shot: splitTelegramRichMarkdown
|
|
1043
|
+
// is fence-aware, so code blocks with inner blank lines stay intact
|
|
1044
|
+
// and no blank line is rewritten. A blank-line pre-split would break
|
|
1045
|
+
// both: unfinished fences fall back to plain, and rejoining with a
|
|
1046
|
+
// fixed separator collapses \n{3,} inside multi-line strings. Plain
|
|
1047
|
+
// answers use the whitespace-preserving regular-text splitter.
|
|
1048
|
+
const chunks = format === 'markdown'
|
|
1049
|
+
? [answerText]
|
|
1050
|
+
: splitTelegramRegularText(answerText);
|
|
1051
|
+
const remember = (ids) => {
|
|
1052
|
+
for (const id of ids ?? []) {
|
|
1053
|
+
if (!providerMessageIds.includes(id)) providerMessageIds.push(id);
|
|
1054
|
+
}
|
|
1055
|
+
};
|
|
1056
|
+
for (let index = 0; index < chunks.length; index += 1) {
|
|
1057
|
+
let result;
|
|
1058
|
+
try {
|
|
1059
|
+
result = await client.#sendRich(target, createTextDeliveryBlock(chunks[index], format));
|
|
1060
|
+
} catch (error) {
|
|
1061
|
+
const failure = telegramFailure(error);
|
|
1062
|
+
warnFailure('final answer', error);
|
|
1063
|
+
if (failure.outcome === 'unknown') {
|
|
1064
|
+
return deliveryResult('telegram-thinking', providerMessageIds, 'unknown', failure.reason);
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
if (result?.deliveryOutcome === 'sent') {
|
|
1068
|
+
remember(result.providerMessageIds);
|
|
1069
|
+
continue;
|
|
1070
|
+
}
|
|
1071
|
+
if (result?.deliveryOutcome === 'unknown') {
|
|
1072
|
+
// 结果未知(超时等):可能已送达,重发会造成重复,保留 unknown 状态。
|
|
1073
|
+
remember(result.providerMessageIds);
|
|
1074
|
+
return deliveryResult('telegram-thinking', providerMessageIds, 'unknown', result.reason);
|
|
1075
|
+
}
|
|
1076
|
+
// 明确失败:该分片确认未送达。纯文本分片再走一次 plain 重发
|
|
1077
|
+
// (markdown 分片的 plain 回退已在 #sendRich 内尝试过),只重发
|
|
1078
|
+
// 尚未发送的尾部内容,已成功的分片不重复。
|
|
1079
|
+
remember(result?.providerMessageIds);
|
|
1080
|
+
if (format === 'markdown') {
|
|
1081
|
+
return deliveryResult('telegram-thinking', providerMessageIds, 'failed', result.reason);
|
|
1082
|
+
}
|
|
1083
|
+
warnFailure('final answer chunk', new Error('chunk definitively rejected'));
|
|
1084
|
+
try {
|
|
1085
|
+
// Regular-text chunks already retain the original whitespace.
|
|
1086
|
+
const fallback = await client.sendText(target, chunks.slice(index).join(''));
|
|
1087
|
+
remember(fallback.providerMessageIds);
|
|
1088
|
+
return deliveryResult('telegram-thinking', providerMessageIds);
|
|
1089
|
+
} catch (error) {
|
|
1090
|
+
const failure = telegramFailure(error);
|
|
1091
|
+
warnFailure('final answer fallback', error);
|
|
1092
|
+
return deliveryResult('telegram-thinking', providerMessageIds, failure.outcome, failure.reason);
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
return deliveryResult('telegram-thinking', providerMessageIds);
|
|
1096
|
+
},
|
|
1097
|
+
cancel() {
|
|
1098
|
+
closed = true;
|
|
1099
|
+
},
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
906
1102
|
}
|
|
907
1103
|
|
|
908
1104
|
export function createTelegramRuntimeStatus() {
|
|
@@ -926,6 +1122,7 @@ export class TelegramRuntime {
|
|
|
926
1122
|
#contextEnhancement;
|
|
927
1123
|
#accessPolicy;
|
|
928
1124
|
#logger;
|
|
1125
|
+
#diagnostics;
|
|
929
1126
|
#replyTimeoutMs;
|
|
930
1127
|
#createApi;
|
|
931
1128
|
#createHttpTransport;
|
|
@@ -968,7 +1165,7 @@ export class TelegramRuntime {
|
|
|
968
1165
|
this.#state = state;
|
|
969
1166
|
this.#contextEnhancement = contextEnhancement;
|
|
970
1167
|
this.#accessPolicy = accessPolicy;
|
|
971
|
-
this.#logger = logger;
|
|
1168
|
+
this.#logger = logger; this.#diagnostics = createConnectionDiagnostics({ channel: 'telegram', logger });
|
|
972
1169
|
this.#replyTimeoutMs = replyTimeoutMs;
|
|
973
1170
|
this.#createApi = createApi;
|
|
974
1171
|
this.#createHttpTransport = createHttpTransport;
|
|
@@ -1026,8 +1223,8 @@ export class TelegramRuntime {
|
|
|
1026
1223
|
this.#connecting = true;
|
|
1027
1224
|
this.#status.startedAt = new Date().toISOString();
|
|
1028
1225
|
this.#status.connectionState = 'connecting';
|
|
1029
|
-
this.#status.lastError = null;
|
|
1030
|
-
await this.#harness.ensureRunning();
|
|
1226
|
+
this.#status.lastError = null; this.#status.error = null; this.#diagnostics.clear();
|
|
1227
|
+
await atConnectionStage('harness.check', () => this.#harness.ensureRunning());
|
|
1031
1228
|
this.#status.harnessReachable = true;
|
|
1032
1229
|
|
|
1033
1230
|
const controller = new AbortController();
|
|
@@ -1057,7 +1254,7 @@ export class TelegramRuntime {
|
|
|
1057
1254
|
} catch (error) {
|
|
1058
1255
|
this.#logger.warn?.(
|
|
1059
1256
|
`[dsh-im:telegram] bot ${this.#config.botId} command menu setup failed:`,
|
|
1060
|
-
error,
|
|
1257
|
+
extractConnectionEvidence(error).details,
|
|
1061
1258
|
);
|
|
1062
1259
|
}
|
|
1063
1260
|
const client = new TelegramBotClient({
|
|
@@ -1071,6 +1268,8 @@ export class TelegramRuntime {
|
|
|
1071
1268
|
state: this.#state,
|
|
1072
1269
|
contextEnhancement: this.#contextEnhancement,
|
|
1073
1270
|
accessPolicy: this.#accessPolicy,
|
|
1271
|
+
// Default ON; an explicit false in the bot config opts out.
|
|
1272
|
+
thinkingTraces: this.#config?.thinkingTraces !== false,
|
|
1074
1273
|
status: this.#status,
|
|
1075
1274
|
logger: this.#logger,
|
|
1076
1275
|
replyTimeoutMs: this.#replyTimeoutMs,
|
|
@@ -1099,7 +1298,7 @@ export class TelegramRuntime {
|
|
|
1099
1298
|
} catch (error) {
|
|
1100
1299
|
this.#logger.warn?.(
|
|
1101
1300
|
`[dsh-im:telegram] bot ${this.#config.botId} command menu catch-up failed:`,
|
|
1102
|
-
error,
|
|
1301
|
+
extractConnectionEvidence(error).details,
|
|
1103
1302
|
);
|
|
1104
1303
|
}
|
|
1105
1304
|
}
|
|
@@ -1108,14 +1307,16 @@ export class TelegramRuntime {
|
|
|
1108
1307
|
if (controller.signal.aborted) return;
|
|
1109
1308
|
this.#status.ready = false;
|
|
1110
1309
|
this.#status.connectionState = 'failed';
|
|
1111
|
-
this.#status.
|
|
1112
|
-
this.#
|
|
1310
|
+
this.#status.error = this.#diagnostics.report(error, { operation: 'connection.monitor', botId: this.#config?.botId, automatic: true }).publicError;
|
|
1311
|
+
this.#status.lastError = this.#status.error.message;
|
|
1312
|
+
|
|
1113
1313
|
});
|
|
1114
1314
|
return this.status;
|
|
1115
1315
|
} catch (error) {
|
|
1116
1316
|
this.#status.ready = false;
|
|
1117
1317
|
this.#status.connectionState = 'failed';
|
|
1118
|
-
this.#status.
|
|
1318
|
+
this.#status.error = this.#diagnostics.report(error, { operation: 'connection.restore', reuse: true, botId: this.#config?.botId, automatic: true }).publicError;
|
|
1319
|
+
this.#status.lastError = this.#status.error.message;
|
|
1119
1320
|
await this.stop();
|
|
1120
1321
|
throw error;
|
|
1121
1322
|
} finally {
|
|
@@ -1160,7 +1361,7 @@ export class TelegramRuntime {
|
|
|
1160
1361
|
} catch (error) {
|
|
1161
1362
|
this.#logger.warn?.(
|
|
1162
1363
|
`[dsh-im:telegram] bot ${this.#config.botId} command menu refresh failed:`,
|
|
1163
|
-
error,
|
|
1364
|
+
extractConnectionEvidence(error).details,
|
|
1164
1365
|
);
|
|
1165
1366
|
return false;
|
|
1166
1367
|
}
|
|
@@ -1210,7 +1411,7 @@ export class TelegramRuntime {
|
|
|
1210
1411
|
if (signal.aborted) return;
|
|
1211
1412
|
this.#logger.error?.(
|
|
1212
1413
|
`[dsh-im:telegram] bot ${this.#config.botId} callback handling failed:`,
|
|
1213
|
-
error,
|
|
1414
|
+
extractConnectionEvidence(error).details,
|
|
1214
1415
|
);
|
|
1215
1416
|
});
|
|
1216
1417
|
}
|
|
@@ -1230,7 +1431,7 @@ export class TelegramRuntime {
|
|
|
1230
1431
|
if (signal.aborted) return;
|
|
1231
1432
|
this.#logger.error?.(
|
|
1232
1433
|
`[dsh-im:telegram] bot ${this.#config.botId} message handling failed:`,
|
|
1233
|
-
error,
|
|
1434
|
+
extractConnectionEvidence(error).details,
|
|
1234
1435
|
);
|
|
1235
1436
|
});
|
|
1236
1437
|
}
|
|
@@ -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) {
|