@xmanrui/dsh-im 4.22.0 → 4.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +3 -3
- package/README.md +3 -3
- package/lib/client.js +2505 -1152
- package/lib/index.js +290 -299
- package/package.json +8 -1
- package/plugin-src/client/channel-card-meta.js +2 -0
- package/plugin-src/client/channel-logos.js +11 -0
- package/plugin-src/client/channels/dingtalk/api.js +5 -1
- package/plugin-src/client/channels/dingtalk/index.js +15 -13
- package/plugin-src/client/channels/email/api.js +52 -0
- package/plugin-src/client/channels/email/index.js +728 -0
- package/plugin-src/client/channels/email/styles.js +51 -0
- package/plugin-src/client/channels/feishu/api.js +4 -2
- package/plugin-src/client/channels/feishu/index.js +16 -3
- package/plugin-src/client/channels/imessage/index.js +2 -1
- package/plugin-src/client/channels/office/api.js +2 -0
- package/plugin-src/client/channels/office/index.js +6 -5
- package/plugin-src/client/channels/qq/api.js +7 -0
- package/plugin-src/client/channels/qq/index.js +16 -3
- package/plugin-src/client/channels/shared/token-api.js +5 -1
- package/plugin-src/client/channels/shared/token-channel.js +28 -3
- package/plugin-src/client/channels/slack/index.js +2 -1
- package/plugin-src/client/channels/wecom/api.js +7 -1
- package/plugin-src/client/channels/wecom/index.js +16 -3
- package/plugin-src/client/channels/wecom-app/api.js +6 -1
- package/plugin-src/client/channels/wecom-app/index.js +16 -3
- package/plugin-src/client/channels/weixin/api.js +2 -1
- package/plugin-src/client/channels/weixin/connection-error.js +1 -71
- package/plugin-src/client/channels/whatsapp/api.js +5 -0
- package/plugin-src/client/channels/whatsapp/index.js +16 -3
- package/plugin-src/client/connection-error.js +87 -0
- package/plugin-src/client/global-settings.js +81 -1
- package/plugin-src/client/i18n.js +61 -0
- package/plugin-src/client/index.js +30 -2
- package/plugin-src/client/last-message-error.js +2 -1
- package/plugin-src/client/styles.js +6 -0
- package/plugin-src/host/build.mjs +4 -0
- package/plugin-src/host/channels/dingtalk/production.mjs +2 -0
- package/plugin-src/host/channels/dingtalk/rpc.mjs +7 -4
- package/plugin-src/host/channels/email/availability.mjs +60 -0
- package/plugin-src/host/channels/email/index.mjs +36 -0
- package/plugin-src/host/channels/email/production.mjs +58 -0
- package/plugin-src/host/channels/email/rpc.mjs +163 -0
- package/plugin-src/host/channels/feishu/production.mjs +3 -0
- package/plugin-src/host/channels/feishu/rpc.mjs +7 -4
- package/plugin-src/host/channels/imessage/rpc.mjs +13 -6
- package/plugin-src/host/channels/office/rpc.mjs +3 -1
- package/plugin-src/host/channels/qq/production.mjs +2 -0
- package/plugin-src/host/channels/qq/rpc.mjs +5 -3
- package/plugin-src/host/channels/shared/production.mjs +52 -5
- package/plugin-src/host/channels/shared/rpc.mjs +5 -3
- package/plugin-src/host/channels/shared/startup-error.mjs +4 -3
- package/plugin-src/host/channels/shared/startup.mjs +11 -6
- package/plugin-src/host/channels/slack/production.mjs +2 -0
- package/plugin-src/host/channels/slack/rpc.mjs +5 -3
- package/plugin-src/host/channels/wecom/production.mjs +2 -0
- package/plugin-src/host/channels/wecom/rpc.mjs +5 -3
- package/plugin-src/host/channels/wecom-app/production.mjs +2 -0
- package/plugin-src/host/channels/wecom-app/rpc.mjs +5 -3
- package/plugin-src/host/channels/weixin/production.mjs +2 -0
- package/plugin-src/host/channels/whatsapp/production.mjs +2 -0
- package/plugin-src/host/channels/whatsapp/rpc.mjs +5 -3
- package/plugin-src/host/delivery-adapter.mjs +11 -0
- package/plugin-src/host/image-input-rpc.mjs +24 -0
- package/plugin-src/host/inbound-ttl-rpc.mjs +6 -1
- package/plugin-src/host/index.mjs +3 -0
- package/plugin-src/management-rpc.mjs +12 -2
- package/scripts/verify-package.mjs +3 -1
- package/src/channels/dingtalk/connection-error.mjs +5 -12
- package/src/channels/dingtalk/device-auth.mjs +4 -1
- package/src/channels/dingtalk/dingtalk-bridge.mjs +6 -4
- package/src/channels/dingtalk/dingtalk-controller.mjs +60 -45
- package/src/channels/dingtalk/dingtalk-runtime.mjs +20 -4
- package/src/channels/discord/discord-api.mjs +3 -3
- package/src/channels/discord/discord-runtime.mjs +18 -12
- package/src/channels/email/config-store.mjs +194 -0
- package/src/channels/email/email-api.mjs +20 -0
- package/src/channels/email/email-bridge.mjs +17 -0
- package/src/channels/email/email-controller.mjs +824 -0
- package/src/channels/email/email-runtime.mjs +692 -0
- package/src/channels/email/harness-client.mjs +7 -0
- package/src/channels/email/mail-format.mjs +91 -0
- package/src/channels/email/state-store.mjs +176 -0
- package/src/channels/email/transport.mjs +64 -0
- package/src/channels/email/transports/agent-mail.mjs +593 -0
- package/src/channels/email/transports/agently-cli.mjs +288 -0
- package/src/channels/email/transports/imap-smtp.mjs +165 -0
- package/src/channels/feishu/bridge.mjs +8 -7
- package/src/channels/feishu/feishu-runtime.mjs +15 -8
- package/src/channels/feishu/group-message-permission-manager.mjs +2 -1
- package/src/channels/feishu/message-utils.mjs +3 -2
- package/src/channels/feishu/multi-bot-controller.mjs +56 -47
- package/src/channels/feishu/registration-manager.mjs +8 -4
- package/src/channels/feishu/repair-manager.mjs +2 -1
- package/src/channels/imessage/runtime.mjs +9 -5
- package/src/channels/office/office-controller.mjs +23 -12
- package/src/channels/office/office-runtime.mjs +6 -3
- package/src/channels/office/office-transport.mjs +1 -0
- package/src/channels/qq/qq-bridge.mjs +7 -5
- package/src/channels/qq/qq-controller.mjs +56 -42
- package/src/channels/qq/qq-runtime.mjs +16 -11
- package/src/channels/shared/bot-workspace-store.mjs +29 -10
- package/src/channels/shared/connection-error.mjs +191 -0
- package/src/channels/shared/connection-test.mjs +3 -1
- package/src/channels/shared/conversation-state-store.mjs +46 -1
- package/src/channels/shared/diagnostic-details.mjs +95 -0
- package/src/channels/shared/harness-client.mjs +63 -18
- package/src/channels/shared/i18n-en/diagnostics.mjs +55 -0
- package/src/channels/shared/i18n-en/email.mjs +25 -0
- package/src/channels/shared/i18n-en/image-input.mjs +17 -0
- package/src/channels/shared/i18n-en/shared-a.mjs +1 -0
- package/src/channels/shared/i18n-en/shared-c.mjs +1 -1
- package/src/channels/shared/i18n-en.mjs +6 -0
- package/src/channels/shared/image-input-policy.mjs +35 -0
- package/src/channels/shared/image-input-settings-store.mjs +60 -0
- package/src/channels/shared/image-input.mjs +124 -0
- package/src/channels/shared/image-prompt.mjs +15 -7
- package/src/channels/shared/message-failure.mjs +12 -0
- package/src/channels/shared/semantic/reply-reference.mjs +3 -3
- package/src/channels/shared/text-harness-bridge.mjs +90 -11
- package/src/channels/shared/token-bot-controller.mjs +52 -47
- package/src/channels/slack/slack-api.mjs +3 -3
- package/src/channels/slack/slack-controller.mjs +59 -51
- package/src/channels/slack/slack-runtime.mjs +18 -12
- package/src/channels/telegram/telegram-api.mjs +2 -2
- package/src/channels/telegram/telegram-runtime.mjs +17 -13
- package/src/channels/wecom/wecom-bridge.mjs +15 -6
- package/src/channels/wecom/wecom-controller.mjs +51 -37
- package/src/channels/wecom/wecom-runtime.mjs +10 -6
- package/src/channels/wecom-app/wecom-app-bridge.mjs +7 -5
- package/src/channels/wecom-app/wecom-app-controller.mjs +50 -37
- package/src/channels/wecom-app/wecom-app-runtime.mjs +2 -1
- package/src/channels/weixin/connection-error.mjs +26 -92
- package/src/channels/weixin/diagnostic-details.mjs +1 -63
- package/src/channels/weixin/weixin-bridge.mjs +7 -5
- package/src/channels/whatsapp/whatsapp-controller.mjs +34 -17
- package/src/channels/whatsapp/whatsapp-runtime.mjs +35 -8
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { connectionErrorChain, connectionStageError } from '../shared/connection-error.mjs';
|
|
1
2
|
import { randomUUID } from 'node:crypto';
|
|
2
3
|
import { createRequire } from 'node:module';
|
|
3
4
|
|
|
@@ -62,17 +63,7 @@ export function installedDingtalkConnectionDependencies() {
|
|
|
62
63
|
return installedDependencyVersions;
|
|
63
64
|
}
|
|
64
65
|
|
|
65
|
-
function errorChain(error) {
|
|
66
|
-
const chain = [];
|
|
67
|
-
const seen = new Set();
|
|
68
|
-
let current = error;
|
|
69
|
-
while (current && typeof current === 'object' && chain.length < 4 && !seen.has(current)) {
|
|
70
|
-
seen.add(current);
|
|
71
|
-
chain.push(current);
|
|
72
|
-
current = current.cause;
|
|
73
|
-
}
|
|
74
|
-
return chain;
|
|
75
|
-
}
|
|
66
|
+
function errorChain(error) { return connectionErrorChain(error).chain; }
|
|
76
67
|
|
|
77
68
|
function statusFrom(error) {
|
|
78
69
|
if (Number.isInteger(error?.status)) return error.status;
|
|
@@ -153,7 +144,9 @@ export function dingtalkRuntimeStartError(code, cause) {
|
|
|
153
144
|
);
|
|
154
145
|
error.name = 'DingtalkRuntimeStartError';
|
|
155
146
|
error.code = STAGE_CODES.has(code) ? code : 'dingtalk-runtime-prepare-failed';
|
|
156
|
-
return error
|
|
147
|
+
return connectionStageError(error, ({ 'dingtalk-harness-connect-failed': 'harness.check',
|
|
148
|
+
'dingtalk-runtime-prepare-failed': 'runtime.prepare', 'dingtalk-stream-client-load-failed': 'sdk.load',
|
|
149
|
+
'dingtalk-stream-listener-failed': 'runtime.prepare', 'dingtalk-stream-connect-failed': 'connection.start' })[error.code]);
|
|
157
150
|
}
|
|
158
151
|
|
|
159
152
|
/** Creates browser-safe guidance plus a redacted Host-log diagnostic for one connection failure. */
|
|
@@ -68,6 +68,8 @@ export class DingtalkDeviceAuthError extends Error {
|
|
|
68
68
|
this.name = 'DingtalkDeviceAuthError';
|
|
69
69
|
this.code = code;
|
|
70
70
|
this.action = action;
|
|
71
|
+
if (Number.isInteger(options.status)) this.status = options.status;
|
|
72
|
+
if (Number.isFinite(options.timeoutMs)) this.timeoutMs = options.timeoutMs;
|
|
71
73
|
}
|
|
72
74
|
}
|
|
73
75
|
|
|
@@ -199,7 +201,7 @@ export class DingtalkDeviceAuth {
|
|
|
199
201
|
'timeout',
|
|
200
202
|
`DingTalk ${action} request timed out`,
|
|
201
203
|
action,
|
|
202
|
-
{ cause: error },
|
|
204
|
+
{ cause: error, timeoutMs: this.#timeoutMs },
|
|
203
205
|
);
|
|
204
206
|
}
|
|
205
207
|
if (error?.name === 'AbortError') throw error;
|
|
@@ -215,6 +217,7 @@ export class DingtalkDeviceAuth {
|
|
|
215
217
|
'http-error',
|
|
216
218
|
`DingTalk ${action} request failed`,
|
|
217
219
|
action,
|
|
220
|
+
{ status: response?.status },
|
|
218
221
|
);
|
|
219
222
|
}
|
|
220
223
|
let value;
|
|
@@ -65,6 +65,7 @@ import {
|
|
|
65
65
|
import { recoverAssistantTextByTimestamp } from '../shared/session-reply-recovery.mjs';
|
|
66
66
|
import { DINGTALK_RECENT_OUTBOUND_MATCH_TOLERANCE_MS } from './state-store.mjs';
|
|
67
67
|
import {
|
|
68
|
+
messageFailureDiagnostic,
|
|
68
69
|
channelDeliveryFailure,
|
|
69
70
|
clearLastMessageFailure,
|
|
70
71
|
messageFailureText,
|
|
@@ -740,7 +741,7 @@ export class DingtalkHarnessBridge {
|
|
|
740
741
|
const failure = setLastMessageFailure(this.#status, error);
|
|
741
742
|
this.#logger.error?.(
|
|
742
743
|
`[dsh-dingtalk] failed to process a command [${failure.referenceId}]`,
|
|
743
|
-
|
|
744
|
+
messageFailureDiagnostic(error, failure),
|
|
744
745
|
);
|
|
745
746
|
return this.#send(sessionWebhook, messageFailureText(failure), this.#atUsersFor(message)).catch(() => undefined);
|
|
746
747
|
}).finally(() => {
|
|
@@ -1207,7 +1208,7 @@ export class DingtalkHarnessBridge {
|
|
|
1207
1208
|
const failure = setLastMessageFailure(this.#status, error);
|
|
1208
1209
|
this.#logger.error?.(
|
|
1209
1210
|
`[dsh-dingtalk] failed to process a batch input message [${failure.referenceId}]`,
|
|
1210
|
-
|
|
1211
|
+
messageFailureDiagnostic(error, failure),
|
|
1211
1212
|
);
|
|
1212
1213
|
await this.#send(sessionWebhook, messageFailureText(failure)).catch(() => undefined);
|
|
1213
1214
|
}).finally(() => {
|
|
@@ -1338,7 +1339,7 @@ export class DingtalkHarnessBridge {
|
|
|
1338
1339
|
signal: this.#signal,
|
|
1339
1340
|
});
|
|
1340
1341
|
let content = hasInboundImages(modelMessage) || hasReply
|
|
1341
|
-
? await promptContentForInboundMessage(modelMessage, { signal: this.#signal })
|
|
1342
|
+
? await promptContentForInboundMessage(modelMessage, { signal: this.#signal, deferImages: true })
|
|
1342
1343
|
: undefined;
|
|
1343
1344
|
const snapshot = this.#acceptedMessageIds.get(messageId);
|
|
1344
1345
|
let contextEnhanced = false;
|
|
@@ -1395,6 +1396,7 @@ export class DingtalkHarnessBridge {
|
|
|
1395
1396
|
}),
|
|
1396
1397
|
onInteractionResolved: (resolution) => this.#handleInteractionResolved(resolution),
|
|
1397
1398
|
files: modelMessage.files,
|
|
1399
|
+
images: modelMessage.images,
|
|
1398
1400
|
},
|
|
1399
1401
|
});
|
|
1400
1402
|
if (batchSubmission) {
|
|
@@ -1486,7 +1488,7 @@ export class DingtalkHarnessBridge {
|
|
|
1486
1488
|
});
|
|
1487
1489
|
this.#logger.error?.(
|
|
1488
1490
|
`[dsh-dingtalk] failed to process an inbound message [${failure.referenceId}]`,
|
|
1489
|
-
|
|
1491
|
+
messageFailureDiagnostic(error, failure),
|
|
1490
1492
|
);
|
|
1491
1493
|
try {
|
|
1492
1494
|
const errorText = messageFailureText(failure);
|
|
@@ -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 {
|
|
@@ -135,6 +136,7 @@ export class DingtalkController {
|
|
|
135
136
|
#createRuntime;
|
|
136
137
|
#deleteState;
|
|
137
138
|
#logger;
|
|
139
|
+
#diagnostics;
|
|
138
140
|
#clock;
|
|
139
141
|
#runtimes = new Map();
|
|
140
142
|
#errors = new Map();
|
|
@@ -193,10 +195,17 @@ export class DingtalkController {
|
|
|
193
195
|
this.#createRuntime = createRuntime;
|
|
194
196
|
this.#deleteState = deleteState;
|
|
195
197
|
this.#logger = logger;
|
|
198
|
+
this.#diagnostics = createConnectionDiagnostics({ channel: 'dingtalk', prefix: 'DT-CONN', logger });
|
|
196
199
|
this.#clock = clock;
|
|
197
200
|
}
|
|
198
201
|
|
|
199
202
|
/** Starts all configured DingTalk runtimes whose secrets are available. */
|
|
203
|
+
get diagnostics() { return this.#diagnostics; }
|
|
204
|
+
#failure(error, code, message) {
|
|
205
|
+
const stage = code.startsWith('poll-') ? 'qr.poll' : code === 'activation-failed' ? 'activation' : 'qr.begin';
|
|
206
|
+
return this.#diagnostics.report(error, { reuse: true, stage, publicError: { code, message } }).publicError;
|
|
207
|
+
}
|
|
208
|
+
|
|
200
209
|
async initialize() {
|
|
201
210
|
if (this.#closed) return this.status();
|
|
202
211
|
for (const config of this.#configStore.list()) {
|
|
@@ -209,16 +218,17 @@ export class DingtalkController {
|
|
|
209
218
|
await this.#withBotTransition(config.botId, async () => {
|
|
210
219
|
const latest = this.#configStore.get(config.botId);
|
|
211
220
|
if (!latest || this.#closed) return;
|
|
212
|
-
|
|
213
|
-
if (!clientSecret) {
|
|
214
|
-
this.#errors.set(
|
|
215
|
-
latest.botId,
|
|
216
|
-
safeError('missing-secret', t('钉钉机器人凭据缺失,请移除后重新扫码。')),
|
|
217
|
-
);
|
|
218
|
-
this.#touch();
|
|
219
|
-
return;
|
|
220
|
-
}
|
|
221
|
+
let clientSecret;
|
|
221
222
|
try {
|
|
223
|
+
clientSecret = await this.#resolveSecret(latest.secretRef);
|
|
224
|
+
if (!clientSecret) {
|
|
225
|
+
this.#errors.set(
|
|
226
|
+
latest.botId,
|
|
227
|
+
safeError('missing-secret', t('钉钉机器人凭据缺失,请移除后重新扫码。')),
|
|
228
|
+
);
|
|
229
|
+
this.#touch();
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
222
232
|
await this.#startRuntime(latest, clientSecret);
|
|
223
233
|
this.#errors.delete(latest.botId);
|
|
224
234
|
} catch (error) {
|
|
@@ -282,7 +292,7 @@ export class DingtalkController {
|
|
|
282
292
|
record.error = safeError('cancelled', t('扫码接入已取消。'));
|
|
283
293
|
} else {
|
|
284
294
|
record.state = 'failed';
|
|
285
|
-
record.error =
|
|
295
|
+
record.error = this.#failure(error, 'qr-start-failed', t('无法生成钉钉二维码,请稍后重试。'));
|
|
286
296
|
}
|
|
287
297
|
if (this.#activeAttemptId === record.id) this.#activeAttemptId = null;
|
|
288
298
|
this.#touch();
|
|
@@ -307,7 +317,7 @@ export class DingtalkController {
|
|
|
307
317
|
await this.#withBotTransition(identity.botId, async () => {
|
|
308
318
|
if (this.#closed) throw abortError();
|
|
309
319
|
const previousConfig = this.#configStore.getByClientId(normalizedClientId);
|
|
310
|
-
const previousSecret = await this.#credentials.resolve(identity.secretRef)
|
|
320
|
+
const previousSecret = await atConnectionStage('credential.read', () => this.#credentials.resolve(identity.secretRef), 'credential-store');
|
|
311
321
|
if (this.#closed) throw abortError();
|
|
312
322
|
const config = {
|
|
313
323
|
botId: identity.botId,
|
|
@@ -315,9 +325,9 @@ export class DingtalkController {
|
|
|
315
325
|
secretRef: identity.secretRef,
|
|
316
326
|
approvedSenders: previousConfig?.approvedSenders ?? [],
|
|
317
327
|
};
|
|
318
|
-
await this.#credentials.set(identity.secretRef, normalizedSecret);
|
|
328
|
+
await atConnectionStage('credential.save', () => this.#credentials.set(identity.secretRef, normalizedSecret), 'credential-store');
|
|
319
329
|
try {
|
|
320
|
-
await this.#configStore.save(config);
|
|
330
|
+
await atConnectionStage('account.save', () => this.#configStore.save(config), 'account-config');
|
|
321
331
|
} catch (error) {
|
|
322
332
|
await this.#restoreCredential(identity.secretRef, previousSecret);
|
|
323
333
|
throw error;
|
|
@@ -434,30 +444,37 @@ export class DingtalkController {
|
|
|
434
444
|
|
|
435
445
|
/** Removes one bot, its secret, runtime, and local conversation state. */
|
|
436
446
|
async deleteBot(botId) {
|
|
447
|
+
const warnings = [];
|
|
437
448
|
const config = this.#configStore.get(botId);
|
|
438
449
|
if (!config) throw new Error('Unknown DingTalk bot');
|
|
439
450
|
await this.#withBotTransition(botId, async () => {
|
|
440
|
-
const previousSecret = await this.#credentials.resolve(config.secretRef)
|
|
451
|
+
const previousSecret = await atConnectionStage('credential.read', () => this.#credentials.resolve(config.secretRef), 'credential-store');
|
|
441
452
|
await this.#stopRuntime(botId);
|
|
442
453
|
try {
|
|
443
|
-
await this.#credentials.unset(config.secretRef);
|
|
444
|
-
await this.#configStore.remove(botId);
|
|
454
|
+
await atConnectionStage('credential.remove', () => this.#credentials.unset(config.secretRef), 'credential-store');
|
|
455
|
+
await atConnectionStage('account.remove', () => this.#configStore.remove(botId), 'account-config');
|
|
445
456
|
} catch (error) {
|
|
446
|
-
if (
|
|
447
|
-
|
|
448
|
-
|
|
457
|
+
if (!this.#configStore.get(botId)) {
|
|
458
|
+
warnings.push(this.#diagnostics.report(error, { operation: 'bot.delete', stage: 'workspace.cleanup', warning: true,
|
|
459
|
+
publicError: { code: 'workspace-cleanup-failed', message: '账号已移除,但本地状态清理失败。' } }).publicError);
|
|
460
|
+
} else {
|
|
461
|
+
if (cleanString(previousSecret?.value)) {
|
|
462
|
+
await atConnectionStage('credential.save', () => this.#credentials.set(config.secretRef, previousSecret.value), 'credential-store').catch(() => undefined);
|
|
463
|
+
await this.#startRuntime(config, previousSecret.value).catch(() => undefined);
|
|
464
|
+
}
|
|
465
|
+
throw new Error('Unable to remove the DingTalk bot safely.', { cause: error });
|
|
449
466
|
}
|
|
450
|
-
throw new Error('Unable to remove the DingTalk bot safely.', { cause: error });
|
|
451
467
|
}
|
|
452
468
|
try {
|
|
453
469
|
await this.#deleteState({ botId, config });
|
|
454
|
-
} catch {
|
|
455
|
-
this.#
|
|
470
|
+
} catch (error) {
|
|
471
|
+
warnings.push(this.#diagnostics.report(error, { reuse: true, operation: 'bot.delete', stage: 'state.cleanup', warning: true,
|
|
472
|
+
publicError: { code: 'cleanup-failed', message: '账号已移除,但本地状态清理失败。' } }).publicError);
|
|
456
473
|
}
|
|
457
474
|
this.#errors.delete(botId);
|
|
458
475
|
this.#touch();
|
|
459
476
|
});
|
|
460
|
-
return this.status();
|
|
477
|
+
return { ...this.status(), ...(warnings.length ? { warnings } : {}) };
|
|
461
478
|
}
|
|
462
479
|
|
|
463
480
|
/** Approves one opaque pending-sender request for a bot. */
|
|
@@ -517,7 +534,7 @@ export class DingtalkController {
|
|
|
517
534
|
currentStatus = { state: 'error' };
|
|
518
535
|
}
|
|
519
536
|
const connected = isRuntimeConnected(runtime, currentStatus);
|
|
520
|
-
const accountError = this.#errors.get(config.botId);
|
|
537
|
+
const accountError = currentStatus.error ?? this.#errors.get(config.botId);
|
|
521
538
|
const state = connected ? 'connected' : accountError ? 'error' : 'offline';
|
|
522
539
|
const approvedIds = new Set(config.approvedSenders.map((sender) => sender.staffId));
|
|
523
540
|
const pending = internalPendingSenders(currentStatus)
|
|
@@ -626,7 +643,7 @@ export class DingtalkController {
|
|
|
626
643
|
if (this.#activeAttemptId === record.id) this.#activeAttemptId = null;
|
|
627
644
|
} else if (record.state === 'connecting') {
|
|
628
645
|
record.state = 'failed';
|
|
629
|
-
record.error =
|
|
646
|
+
record.error = this.#failure(error,
|
|
630
647
|
'activation-failed',
|
|
631
648
|
t('钉钉已授权,但无法安全保存接入配置。'),
|
|
632
649
|
);
|
|
@@ -634,7 +651,7 @@ export class DingtalkController {
|
|
|
634
651
|
this.#logger.error?.('[dsh-dingtalk] bot activation failed');
|
|
635
652
|
} else {
|
|
636
653
|
record.state = 'pending';
|
|
637
|
-
record.error =
|
|
654
|
+
record.error = this.#failure(error, 'poll-failed', t('钉钉授权查询暂时失败,正在重试。'));
|
|
638
655
|
}
|
|
639
656
|
} finally {
|
|
640
657
|
this.#touch();
|
|
@@ -645,7 +662,7 @@ export class DingtalkController {
|
|
|
645
662
|
async #activateBot(record, { clientId, clientSecret }) {
|
|
646
663
|
const identity = deriveDingtalkBotIdentity(clientId);
|
|
647
664
|
const previousConfig = this.#configStore.getByClientId(clientId);
|
|
648
|
-
const previousSecret = await this.#credentials.resolve(identity.secretRef)
|
|
665
|
+
const previousSecret = await atConnectionStage('credential.read', () => this.#credentials.resolve(identity.secretRef), 'credential-store');
|
|
649
666
|
const config = {
|
|
650
667
|
botId: identity.botId,
|
|
651
668
|
clientId,
|
|
@@ -655,12 +672,12 @@ export class DingtalkController {
|
|
|
655
672
|
return this.#withBotTransition(identity.botId, async () => {
|
|
656
673
|
const rollback = async () => {
|
|
657
674
|
await this.#stopRuntime(identity.botId);
|
|
658
|
-
if (previousConfig) await this.#configStore.save(previousConfig).catch(() => undefined);
|
|
675
|
+
if (previousConfig) await atConnectionStage('account.save', () => this.#configStore.save(previousConfig), 'account-config').catch(() => undefined);
|
|
659
676
|
else if (this.#configStore.get(identity.botId)) {
|
|
660
|
-
const removed = await this.#configStore.remove(identity.botId).catch(() => null);
|
|
677
|
+
const removed = await atConnectionStage('account.remove', () => this.#configStore.remove(identity.botId), 'account-config').catch(() => null);
|
|
661
678
|
if (removed) {
|
|
662
679
|
await this.#deleteState({ botId: identity.botId, config }).catch((cleanupError) => {
|
|
663
|
-
this.#logger.warn?.('[dsh-dingtalk] failed to clean up cancelled bot state:', cleanupError);
|
|
680
|
+
this.#logger.warn?.('[dsh-dingtalk] failed to clean up cancelled bot state:', extractConnectionEvidence(cleanupError).details);
|
|
664
681
|
});
|
|
665
682
|
}
|
|
666
683
|
}
|
|
@@ -669,10 +686,10 @@ export class DingtalkController {
|
|
|
669
686
|
await this.#startRuntime(previousConfig, previousSecret.value).catch(() => undefined);
|
|
670
687
|
}
|
|
671
688
|
};
|
|
672
|
-
await this.#credentials.set(identity.secretRef, clientSecret);
|
|
689
|
+
await atConnectionStage('credential.save', () => this.#credentials.set(identity.secretRef, clientSecret), 'credential-store');
|
|
673
690
|
try {
|
|
674
691
|
this.#assertAttemptActive(record);
|
|
675
|
-
await this.#configStore.save(config);
|
|
692
|
+
await atConnectionStage('account.save', () => this.#configStore.save(config), 'account-config');
|
|
676
693
|
this.#assertAttemptActive(record);
|
|
677
694
|
} catch (error) {
|
|
678
695
|
await rollback();
|
|
@@ -703,12 +720,12 @@ export class DingtalkController {
|
|
|
703
720
|
return this.#withBotTransition(previousConfig.botId, async () => {
|
|
704
721
|
const clientSecret = await this.#resolveSecret(previousConfig.secretRef);
|
|
705
722
|
if (!clientSecret) throw new Error('The DingTalk client secret is missing');
|
|
706
|
-
await this.#configStore.save(nextConfig);
|
|
723
|
+
await atConnectionStage('account.save', () => this.#configStore.save(nextConfig), 'account-config');
|
|
707
724
|
try {
|
|
708
725
|
await this.#startRuntime(nextConfig, clientSecret);
|
|
709
726
|
this.#errors.delete(previousConfig.botId);
|
|
710
727
|
} catch (error) {
|
|
711
|
-
await this.#configStore.save(previousConfig).catch(() => undefined);
|
|
728
|
+
await atConnectionStage('account.save', () => this.#configStore.save(previousConfig), 'account-config').catch(() => undefined);
|
|
712
729
|
await this.#startRuntime(previousConfig, clientSecret).catch(() => undefined);
|
|
713
730
|
const publicError = this.#rememberConnectionFailure({
|
|
714
731
|
config: previousConfig,
|
|
@@ -730,11 +747,11 @@ export class DingtalkController {
|
|
|
730
747
|
if (this.#closed) throw abortError();
|
|
731
748
|
let runtime;
|
|
732
749
|
try {
|
|
733
|
-
runtime = await this.#createRuntime({
|
|
750
|
+
runtime = await atConnectionStage('runtime.prepare', () => this.#createRuntime({
|
|
734
751
|
botId: config.botId,
|
|
735
752
|
config: structuredClone(config),
|
|
736
753
|
clientSecret,
|
|
737
|
-
});
|
|
754
|
+
}));
|
|
738
755
|
} catch (error) {
|
|
739
756
|
throw dingtalkRuntimeStartError('dingtalk-runtime-prepare-failed', error);
|
|
740
757
|
}
|
|
@@ -774,12 +791,10 @@ export class DingtalkController {
|
|
|
774
791
|
clientId: config.clientId,
|
|
775
792
|
clientSecret,
|
|
776
793
|
});
|
|
777
|
-
this.#
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
);
|
|
782
|
-
return failure.publicError;
|
|
794
|
+
const reported = this.#diagnostics.report(error, { reuse: true, botId: config.botId, stage: 'connection.start', publicError: { ...failure.publicError, details: { dependencies: failure.diagnostic.dependencies, proxyConfigured: failure.diagnostic.proxy.configured } } }).publicError;
|
|
795
|
+
const visible = { ...failure.publicError, details: reported.details };
|
|
796
|
+
this.#errors.set(config.botId, visible);
|
|
797
|
+
return visible;
|
|
783
798
|
}
|
|
784
799
|
|
|
785
800
|
async #stopRuntime(botId) {
|
|
@@ -791,14 +806,14 @@ export class DingtalkController {
|
|
|
791
806
|
}
|
|
792
807
|
|
|
793
808
|
async #resolveSecret(secretRef) {
|
|
794
|
-
const result = await this.#credentials.resolve(secretRef)
|
|
809
|
+
const result = await atConnectionStage('credential.read', () => this.#credentials.resolve(secretRef), 'credential-store');
|
|
795
810
|
return cleanString(result?.value);
|
|
796
811
|
}
|
|
797
812
|
|
|
798
813
|
async #restoreCredential(secretRef, previous) {
|
|
799
814
|
try {
|
|
800
|
-
if (cleanString(previous?.value)) await this.#credentials.set(secretRef, previous.value);
|
|
801
|
-
else await this.#credentials.unset(secretRef);
|
|
815
|
+
if (cleanString(previous?.value)) await atConnectionStage('credential.save', () => this.#credentials.set(secretRef, previous.value), 'credential-store');
|
|
816
|
+
else await atConnectionStage('credential.remove', () => this.#credentials.unset(secretRef), 'credential-store');
|
|
802
817
|
} catch {
|
|
803
818
|
this.#logger.error?.(`[dsh-dingtalk] failed to restore credential ${secretRef}`);
|
|
804
819
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { atConnectionStage, createConnectionDiagnostics } from '../shared/connection-error.mjs';
|
|
1
2
|
import { createDingtalkApi } from './dingtalk-api.mjs';
|
|
2
3
|
import {
|
|
3
4
|
createDingtalkBridgeStatus,
|
|
@@ -87,7 +88,7 @@ async function connectStream(client, timeoutMs, pollIntervalMs, signal) {
|
|
|
87
88
|
void connectTask.then(() => client.disconnect()).catch(() => undefined);
|
|
88
89
|
}
|
|
89
90
|
if (signal.aborted) throw signal.reason;
|
|
90
|
-
throw new Error(`DingTalk Stream handshake timed out after ${timeoutMs}ms`);
|
|
91
|
+
throw Object.assign(new Error(`DingTalk Stream handshake timed out after ${timeoutMs}ms`), { code: 'ETIMEDOUT', timeoutMs, cause: error });
|
|
91
92
|
}
|
|
92
93
|
throw error;
|
|
93
94
|
}
|
|
@@ -134,6 +135,7 @@ export class DingtalkRuntime {
|
|
|
134
135
|
#contextEnhancement;
|
|
135
136
|
#accessPolicy;
|
|
136
137
|
#logger;
|
|
138
|
+
#diagnostics;
|
|
137
139
|
#replyTimeoutMs;
|
|
138
140
|
#maxMessageChars;
|
|
139
141
|
#connectTimeoutMs;
|
|
@@ -176,6 +178,7 @@ export class DingtalkRuntime {
|
|
|
176
178
|
this.#contextEnhancement = contextEnhancement;
|
|
177
179
|
this.#accessPolicy = accessPolicy;
|
|
178
180
|
this.#logger = logger;
|
|
181
|
+
this.#diagnostics = createConnectionDiagnostics({ channel: 'dingtalk', logger, prefix: 'DT-CONN' });
|
|
179
182
|
this.#replyTimeoutMs = replyTimeoutMs;
|
|
180
183
|
this.#maxMessageChars = maxMessageChars;
|
|
181
184
|
this.#connectTimeoutMs = connectTimeoutMs;
|
|
@@ -228,7 +231,7 @@ export class DingtalkRuntime {
|
|
|
228
231
|
let startStage = 'dingtalk-harness-connect-failed';
|
|
229
232
|
|
|
230
233
|
try {
|
|
231
|
-
await this.#harness.ensureRunning({ signal });
|
|
234
|
+
await atConnectionStage('harness.check', () => this.#harness.ensureRunning({ signal }));
|
|
232
235
|
this.#status.harnessReachable = true;
|
|
233
236
|
startStage = 'dingtalk-runtime-prepare-failed';
|
|
234
237
|
if (typeof this.#state.removePendingSenderByStaffId === 'function') {
|
|
@@ -348,12 +351,22 @@ export class DingtalkRuntime {
|
|
|
348
351
|
this.#status.lastConnectedAt = Date.now();
|
|
349
352
|
this.#status.lastCheckedAt = Date.now();
|
|
350
353
|
this.#status.lastError = null;
|
|
354
|
+
this.#status.error = null;
|
|
355
|
+
this.#diagnostics.clear();
|
|
351
356
|
this.#connectionMonitor = setInterval(() => {
|
|
352
357
|
const connected = streamIsOpen(client);
|
|
353
358
|
this.#status.ready = connected;
|
|
354
359
|
this.#status.dingtalkStreamState = connected ? 'connected' : 'reconnecting';
|
|
355
360
|
this.#status.lastCheckedAt = Date.now();
|
|
356
|
-
if (connected)
|
|
361
|
+
if (connected) {
|
|
362
|
+
this.#status.lastError = null;
|
|
363
|
+
this.#status.error = null;
|
|
364
|
+
this.#diagnostics.clear();
|
|
365
|
+
} else {
|
|
366
|
+
this.#status.error = this.#diagnostics.report(null, { operation: 'connection.monitor', automatic: true, botId: this.#config.botId,
|
|
367
|
+
publicError: { code: 'stream-connect-failed', message: t('钉钉连接未就绪,请稍后重试。') } }).publicError;
|
|
368
|
+
this.#status.lastError = this.#status.error.message;
|
|
369
|
+
}
|
|
357
370
|
}, 1_000);
|
|
358
371
|
this.#connectionMonitor.unref?.();
|
|
359
372
|
return this.status;
|
|
@@ -362,7 +375,7 @@ export class DingtalkRuntime {
|
|
|
362
375
|
const failure = aborted ? error : dingtalkRuntimeStartError(startStage, error);
|
|
363
376
|
this.#status.ready = false;
|
|
364
377
|
this.#status.dingtalkStreamState = aborted ? 'idle' : 'failed';
|
|
365
|
-
this.#status.lastError = aborted ? null : (
|
|
378
|
+
this.#status.lastError = aborted ? null : t('钉钉连接未就绪,请稍后重试。');
|
|
366
379
|
await this.stop({ preserveError: !aborted });
|
|
367
380
|
throw failure;
|
|
368
381
|
}
|
|
@@ -370,6 +383,7 @@ export class DingtalkRuntime {
|
|
|
370
383
|
|
|
371
384
|
async stop({ preserveError = false } = {}) {
|
|
372
385
|
const lastError = preserveError ? this.#status.lastError : null;
|
|
386
|
+
const publicError = preserveError ? this.#status.error : null;
|
|
373
387
|
const abortController = this.#abortController;
|
|
374
388
|
this.#abortController = null;
|
|
375
389
|
abortController?.abort(new DOMException('DingTalk runtime stopped', 'AbortError'));
|
|
@@ -393,6 +407,8 @@ export class DingtalkRuntime {
|
|
|
393
407
|
this.#bridge = null;
|
|
394
408
|
this.#status.dingtalkStreamState = preserveError ? 'failed' : 'idle';
|
|
395
409
|
this.#status.lastError = lastError;
|
|
410
|
+
this.#status.error = publicError;
|
|
411
|
+
if (!preserveError) this.#diagnostics.clear();
|
|
396
412
|
return this.status;
|
|
397
413
|
}
|
|
398
414
|
|
|
@@ -295,16 +295,16 @@ export class DiscordApi {
|
|
|
295
295
|
});
|
|
296
296
|
} catch (error) {
|
|
297
297
|
if (error?.name === 'AbortError' || error?.name === 'TimeoutError') throw error;
|
|
298
|
-
throw new Error(`Discord ${method} transport failed
|
|
298
|
+
throw new Error(`Discord ${method} transport failed`, { cause: error });
|
|
299
299
|
}
|
|
300
300
|
|
|
301
301
|
let parsed = null;
|
|
302
302
|
if (expectBody || response.status === 429 || !response.ok) {
|
|
303
303
|
try {
|
|
304
304
|
parsed = await response.json();
|
|
305
|
-
} catch {
|
|
305
|
+
} catch (cause) {
|
|
306
306
|
if (expectBody) {
|
|
307
|
-
const error = new Error(`Discord ${method} returned invalid JSON
|
|
307
|
+
const error = new Error(`Discord ${method} returned invalid JSON`, { cause });
|
|
308
308
|
error.status = response?.status;
|
|
309
309
|
throw error;
|
|
310
310
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { extractConnectionEvidence, createConnectionDiagnostics, atConnectionStage } from '../shared/connection-error.mjs';
|
|
1
2
|
import { createEditableMessageStream, splitMessageText } from '../shared/editable-message-stream.mjs';
|
|
2
3
|
import { fetchFileStream } from '../shared/file-download.mjs';
|
|
3
4
|
import { fetchImageBuffer } from '../shared/image-prompt.mjs';
|
|
@@ -534,6 +535,7 @@ export class DiscordRuntime {
|
|
|
534
535
|
#contextEnhancement;
|
|
535
536
|
#accessPolicy;
|
|
536
537
|
#logger;
|
|
538
|
+
#diagnostics;
|
|
537
539
|
#replyTimeoutMs;
|
|
538
540
|
#connectTimeoutMs;
|
|
539
541
|
#createApi;
|
|
@@ -582,7 +584,7 @@ export class DiscordRuntime {
|
|
|
582
584
|
this.#state = state;
|
|
583
585
|
this.#contextEnhancement = contextEnhancement;
|
|
584
586
|
this.#accessPolicy = accessPolicy;
|
|
585
|
-
this.#logger = logger;
|
|
587
|
+
this.#logger = logger; this.#diagnostics = createConnectionDiagnostics({ channel: 'discord', logger });
|
|
586
588
|
this.#replyTimeoutMs = replyTimeoutMs;
|
|
587
589
|
this.#connectTimeoutMs = connectTimeoutMs;
|
|
588
590
|
this.#createApi = createApi;
|
|
@@ -639,8 +641,8 @@ export class DiscordRuntime {
|
|
|
639
641
|
this.#routing.clear();
|
|
640
642
|
this.#status.startedAt = new Date().toISOString();
|
|
641
643
|
this.#status.connectionState = 'connecting';
|
|
642
|
-
this.#status.lastError = null;
|
|
643
|
-
await this.#harness.ensureRunning();
|
|
644
|
+
this.#status.lastError = null; this.#status.error = null; this.#diagnostics.clear();
|
|
645
|
+
await atConnectionStage('harness.check', () => this.#harness.ensureRunning());
|
|
644
646
|
this.#status.harnessReachable = true;
|
|
645
647
|
const controller = new AbortController();
|
|
646
648
|
this.#abortController = controller;
|
|
@@ -672,7 +674,7 @@ export class DiscordRuntime {
|
|
|
672
674
|
await Promise.race([
|
|
673
675
|
this.#openSocket(false),
|
|
674
676
|
new Promise((_, reject) => {
|
|
675
|
-
timer = setTimeout(() => reject(new Error('Discord Gateway did not become ready in time')), this.#connectTimeoutMs);
|
|
677
|
+
timer = setTimeout(() => reject(Object.assign(new Error('Discord Gateway did not become ready in time'), { code: 'ETIMEDOUT', timeoutMs: this.#connectTimeoutMs })), this.#connectTimeoutMs);
|
|
676
678
|
}),
|
|
677
679
|
]);
|
|
678
680
|
} finally {
|
|
@@ -682,7 +684,8 @@ export class DiscordRuntime {
|
|
|
682
684
|
} catch (error) {
|
|
683
685
|
this.#status.ready = false;
|
|
684
686
|
this.#status.connectionState = 'failed';
|
|
685
|
-
this.#status.
|
|
687
|
+
this.#status.error = this.#diagnostics.report(error, { operation: 'connection.restore', reuse: true, botId: this.#config?.botId, automatic: true }).publicError;
|
|
688
|
+
this.#status.lastError = this.#status.error.message;
|
|
686
689
|
await this.stop();
|
|
687
690
|
throw error;
|
|
688
691
|
}
|
|
@@ -705,7 +708,7 @@ export class DiscordRuntime {
|
|
|
705
708
|
this.#status.connectionState = 'connected';
|
|
706
709
|
this.#status.lastCheckedAt = now;
|
|
707
710
|
this.#status.lastConnectedAt = now;
|
|
708
|
-
this.#status.lastError = null;
|
|
711
|
+
this.#status.lastError = null; this.#status.error = null; this.#diagnostics.clear();
|
|
709
712
|
resolve();
|
|
710
713
|
};
|
|
711
714
|
addSocketListener(socket, 'message', (event) => {
|
|
@@ -790,7 +793,7 @@ export class DiscordRuntime {
|
|
|
790
793
|
if (generation !== this.#generation || this.#stopped) return;
|
|
791
794
|
this.#logger.error?.(
|
|
792
795
|
`[dsh-im:discord] bot ${this.#config.botId} message handling failed:`,
|
|
793
|
-
error,
|
|
796
|
+
extractConnectionEvidence(error).details,
|
|
794
797
|
);
|
|
795
798
|
});
|
|
796
799
|
}
|
|
@@ -807,7 +810,8 @@ export class DiscordRuntime {
|
|
|
807
810
|
const error = gatewayCloseError(Number(event.code) || 0);
|
|
808
811
|
this.#status.ready = false;
|
|
809
812
|
this.#status.connectionState = 'connecting';
|
|
810
|
-
this.#status.
|
|
813
|
+
this.#status.error = this.#diagnostics.report(error, { operation: 'connection.monitor', botId: this.#config?.botId, automatic: true }).publicError;
|
|
814
|
+
this.#status.lastError = this.#status.error.message;
|
|
811
815
|
if (!settled) {
|
|
812
816
|
settled = true;
|
|
813
817
|
reject(error);
|
|
@@ -818,9 +822,11 @@ export class DiscordRuntime {
|
|
|
818
822
|
}
|
|
819
823
|
this.#scheduleReconnect();
|
|
820
824
|
});
|
|
821
|
-
addSocketListener(socket, 'error', () => {
|
|
825
|
+
addSocketListener(socket, 'error', (event) => {
|
|
826
|
+
const error = event?.error ?? new Error('WebSocket error');
|
|
822
827
|
if (generation !== this.#generation || this.#stopped) return;
|
|
823
|
-
this.#status.
|
|
828
|
+
this.#status.error = this.#diagnostics.report(error, { operation: 'connection.monitor', botId: this.#config?.botId, automatic: true }).publicError;
|
|
829
|
+
this.#status.lastError = this.#status.error.message;
|
|
824
830
|
});
|
|
825
831
|
});
|
|
826
832
|
}
|
|
@@ -931,7 +937,7 @@ export class DiscordRuntime {
|
|
|
931
937
|
this.#reconnectTimer = null;
|
|
932
938
|
void this.#openSocket(Boolean(this.#sessionId)).catch((error) => {
|
|
933
939
|
if (this.#stopped) return;
|
|
934
|
-
this.#logger.warn?.('[dsh-im:discord] Gateway reconnect failed:', error);
|
|
940
|
+
this.#logger.warn?.('[dsh-im:discord] Gateway reconnect failed:', extractConnectionEvidence(error).details);
|
|
935
941
|
this.#scheduleReconnect();
|
|
936
942
|
});
|
|
937
943
|
}, delay);
|
|
@@ -955,7 +961,7 @@ export class DiscordRuntime {
|
|
|
955
961
|
try {
|
|
956
962
|
if (socket && socket.readyState < 2) socket.close(1000, 'Plugin stopped');
|
|
957
963
|
} catch (error) {
|
|
958
|
-
this.#logger.warn?.(`[dsh-im:discord] bot ${this.#config.botId} failed to close Gateway:`, error);
|
|
964
|
+
this.#logger.warn?.(`[dsh-im:discord] bot ${this.#config.botId} failed to close Gateway:`, extractConnectionEvidence(error).details);
|
|
959
965
|
}
|
|
960
966
|
await Promise.race([
|
|
961
967
|
bridge?.waitForIdle() ?? Promise.resolve(),
|