@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
|
@@ -57,6 +57,7 @@ import {
|
|
|
57
57
|
providerMessageIdsFor,
|
|
58
58
|
} from '../shared/semantic/delivery.mjs';
|
|
59
59
|
import {
|
|
60
|
+
messageFailureDiagnostic,
|
|
60
61
|
channelDeliveryFailure,
|
|
61
62
|
clearLastMessageFailure,
|
|
62
63
|
messageFailureText,
|
|
@@ -612,7 +613,7 @@ export class QqHarnessBridge {
|
|
|
612
613
|
const failure = setLastMessageFailure(this.#status, error);
|
|
613
614
|
this.#logger.error?.(
|
|
614
615
|
`[dsh-im:qq] failed to process a command [${failure.referenceId}]:`,
|
|
615
|
-
error,
|
|
616
|
+
messageFailureDiagnostic(error, failure),
|
|
616
617
|
);
|
|
617
618
|
return this.#bot.sendText(message.replyTarget, messageFailureText(failure))
|
|
618
619
|
.catch(() => undefined);
|
|
@@ -876,7 +877,7 @@ export class QqHarnessBridge {
|
|
|
876
877
|
const failure = setLastMessageFailure(this.#status, error);
|
|
877
878
|
this.#logger.error?.(
|
|
878
879
|
`[dsh-im:qq] failed to process a batch input message [${failure.referenceId}]:`,
|
|
879
|
-
error,
|
|
880
|
+
messageFailureDiagnostic(error, failure),
|
|
880
881
|
);
|
|
881
882
|
await this.#bot.sendText(message.replyTarget, messageFailureText(failure))
|
|
882
883
|
.catch(() => undefined);
|
|
@@ -1039,7 +1040,7 @@ export class QqHarnessBridge {
|
|
|
1039
1040
|
}
|
|
1040
1041
|
|
|
1041
1042
|
let content = hasImages || hasReply
|
|
1042
|
-
? await promptContentForInboundMessage(promptMessage, { signal: this.#signal })
|
|
1043
|
+
? await promptContentForInboundMessage(promptMessage, { signal: this.#signal, deferImages: true })
|
|
1043
1044
|
: undefined;
|
|
1044
1045
|
const snapshot = this.#acceptedMessageIds.get(messageId);
|
|
1045
1046
|
let contextEnhanced = false;
|
|
@@ -1098,6 +1099,7 @@ export class QqHarnessBridge {
|
|
|
1098
1099
|
}),
|
|
1099
1100
|
onInteractionResolved: (resolution) => this.#handleInteractionResolved(resolution),
|
|
1100
1101
|
files: promptMessage.files,
|
|
1102
|
+
images: promptMessage.images,
|
|
1101
1103
|
},
|
|
1102
1104
|
}));
|
|
1103
1105
|
if (batchSubmission) {
|
|
@@ -1208,7 +1210,7 @@ export class QqHarnessBridge {
|
|
|
1208
1210
|
});
|
|
1209
1211
|
this.#logger.error?.(
|
|
1210
1212
|
`[dsh-im:qq] failed to process an inbound message [${failure.referenceId}]:`,
|
|
1211
|
-
error,
|
|
1213
|
+
messageFailureDiagnostic(error, failure),
|
|
1212
1214
|
);
|
|
1213
1215
|
try {
|
|
1214
1216
|
const errorMessage = messageFailureText(failure);
|
|
@@ -1494,7 +1496,7 @@ export class QqHarnessBridge {
|
|
|
1494
1496
|
const failure = setLastMessageFailure(this.#status, error);
|
|
1495
1497
|
this.#logger.error?.(
|
|
1496
1498
|
`[dsh-im:qq] failed to process an interaction reply [${failure.referenceId}]:`,
|
|
1497
|
-
error,
|
|
1499
|
+
messageFailureDiagnostic(error, failure),
|
|
1498
1500
|
);
|
|
1499
1501
|
if (!this.#state.hasSeen(messageId)) {
|
|
1500
1502
|
await this.#state.markSeen(messageId).catch(() => undefined);
|
|
@@ -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 { connectionTestMessage } from '../shared/connection-test.mjs';
|
|
@@ -43,6 +44,7 @@ export class QqController {
|
|
|
43
44
|
#createRuntime;
|
|
44
45
|
#deleteState;
|
|
45
46
|
#logger;
|
|
47
|
+
#diagnostics;
|
|
46
48
|
#runtimes = new Map();
|
|
47
49
|
#errors = new Map();
|
|
48
50
|
#attempts = new Map();
|
|
@@ -75,6 +77,14 @@ export class QqController {
|
|
|
75
77
|
this.#createRuntime = createRuntime;
|
|
76
78
|
this.#deleteState = deleteState;
|
|
77
79
|
this.#logger = logger;
|
|
80
|
+
this.#diagnostics = createConnectionDiagnostics({ channel: 'qq', logger });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
get diagnostics() { return this.#diagnostics; }
|
|
84
|
+
|
|
85
|
+
#failure(error, code, message) {
|
|
86
|
+
const stage = code.startsWith('qr-') ? 'qr.begin' : code === 'activation-failed' ? 'activation' : 'connection.start';
|
|
87
|
+
return this.#diagnostics.report(error, { reuse: true, stage, publicError: { code, message } }).publicError;
|
|
78
88
|
}
|
|
79
89
|
|
|
80
90
|
async initialize() {
|
|
@@ -82,17 +92,17 @@ export class QqController {
|
|
|
82
92
|
for (const config of this.#configStore.list()) {
|
|
83
93
|
await this.#withBotTransition(config.botId, async () => {
|
|
84
94
|
if (this.#closed || this.#runtimes.get(config.botId)?.status?.ready) return;
|
|
85
|
-
const appSecret = await this.#resolveSecret(config.secretRef);
|
|
86
|
-
if (!appSecret) {
|
|
87
|
-
this.#errors.set(config.botId, safeError('missing-secret', t('QQ 机器人凭据缺失,请移除后重新扫码。')));
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
95
|
try {
|
|
96
|
+
const appSecret = await this.#resolveSecret(config.secretRef);
|
|
97
|
+
if (!appSecret) {
|
|
98
|
+
this.#errors.set(config.botId, safeError('missing-secret', t('QQ 机器人凭据缺失,请移除后重新扫码。')));
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
91
101
|
await this.#startRuntime(config, appSecret);
|
|
92
102
|
this.#errors.delete(config.botId);
|
|
93
103
|
} catch (error) {
|
|
94
|
-
this.#errors.set(config.botId, connectionError(error, t('QQ 连接未就绪,插件会自动重试。')));
|
|
95
|
-
|
|
104
|
+
this.#errors.set(config.botId, this.#diagnostics.report(error, { reuse: true, publicError: connectionError(error, t('QQ 连接未就绪,插件会自动重试。')) , stage: 'connection.start' }).publicError);
|
|
105
|
+
|
|
96
106
|
} finally {
|
|
97
107
|
this.#touch();
|
|
98
108
|
}
|
|
@@ -154,7 +164,7 @@ export class QqController {
|
|
|
154
164
|
onFailure: (error) => {
|
|
155
165
|
if (record.controller.signal.aborted || TERMINAL_ATTEMPT_STATES.has(record.state)) return;
|
|
156
166
|
record.state = 'failed';
|
|
157
|
-
record.error =
|
|
167
|
+
record.error = this.#failure(error, 'qr-connect-failed', t('QQ 扫码服务暂时不可用,请重新生成二维码。'));
|
|
158
168
|
if (this.#activeAttemptId === record.id) this.#activeAttemptId = null;
|
|
159
169
|
this.#touch();
|
|
160
170
|
firstQrReject(error);
|
|
@@ -168,7 +178,7 @@ export class QqController {
|
|
|
168
178
|
record.error = safeError('cancelled', t('扫码绑定已取消。'));
|
|
169
179
|
} else if (!TERMINAL_ATTEMPT_STATES.has(record.state)) {
|
|
170
180
|
record.state = 'failed';
|
|
171
|
-
record.error =
|
|
181
|
+
record.error = this.#failure(error, 'qr-start-failed', t('无法生成 QQ 二维码,请稍后重试。'));
|
|
172
182
|
}
|
|
173
183
|
if (this.#activeAttemptId === record.id) this.#activeAttemptId = null;
|
|
174
184
|
this.#touch();
|
|
@@ -193,7 +203,7 @@ export class QqController {
|
|
|
193
203
|
await this.#withBotTransition(identity.botId, async () => {
|
|
194
204
|
if (this.#closed) throw new Error('QQ controller is closed');
|
|
195
205
|
const previousConfig = this.#configStore.getByAppId(normalizedAppId);
|
|
196
|
-
const previousSecret = await this.#credentials.resolve(identity.secretRef)
|
|
206
|
+
const previousSecret = await atConnectionStage('credential.read', () => this.#credentials.resolve(identity.secretRef), 'credential-store');
|
|
197
207
|
if (this.#closed) throw new Error('QQ controller is closed');
|
|
198
208
|
const config = {
|
|
199
209
|
botId: identity.botId,
|
|
@@ -203,9 +213,9 @@ export class QqController {
|
|
|
203
213
|
createdAt: previousConfig?.createdAt ?? new Date().toISOString(),
|
|
204
214
|
connectedAt: new Date().toISOString(),
|
|
205
215
|
};
|
|
206
|
-
await this.#credentials.set(identity.secretRef, normalizedSecret);
|
|
216
|
+
await atConnectionStage('credential.save', () => this.#credentials.set(identity.secretRef, normalizedSecret), 'credential-store');
|
|
207
217
|
try {
|
|
208
|
-
await this.#configStore.save(config);
|
|
218
|
+
await atConnectionStage('account.save', () => this.#configStore.save(config), 'account-config');
|
|
209
219
|
} catch (error) {
|
|
210
220
|
await this.#restoreCredential(identity.secretRef, previousSecret);
|
|
211
221
|
throw error;
|
|
@@ -214,8 +224,8 @@ export class QqController {
|
|
|
214
224
|
await this.#startRuntime(config, normalizedSecret);
|
|
215
225
|
this.#errors.delete(identity.botId);
|
|
216
226
|
} catch (error) {
|
|
217
|
-
this.#errors.set(identity.botId, connectionError(error, t('QQ 机器人已绑定,消息连接暂未就绪。')));
|
|
218
|
-
|
|
227
|
+
this.#errors.set(identity.botId, this.#diagnostics.report(error, { reuse: true, publicError: connectionError(error, t('QQ 机器人已绑定,消息连接暂未就绪。')) , stage: 'connection.start' }).publicError);
|
|
228
|
+
|
|
219
229
|
}
|
|
220
230
|
this.#touch();
|
|
221
231
|
});
|
|
@@ -247,7 +257,7 @@ export class QqController {
|
|
|
247
257
|
await this.#startRuntime(config, secret);
|
|
248
258
|
this.#errors.delete(botId);
|
|
249
259
|
} catch (error) {
|
|
250
|
-
this.#errors.set(botId, connectionError(error, t('QQ 连接仍未就绪,请稍后重试。')));
|
|
260
|
+
this.#errors.set(botId, this.#diagnostics.report(error, { reuse: true, publicError: connectionError(error, t('QQ 连接仍未就绪,请稍后重试。')) , stage: 'connection.start' }).publicError);
|
|
251
261
|
throw error;
|
|
252
262
|
} finally {
|
|
253
263
|
this.#touch();
|
|
@@ -288,28 +298,32 @@ export class QqController {
|
|
|
288
298
|
}
|
|
289
299
|
|
|
290
300
|
async deleteBot(botId) {
|
|
301
|
+
const warnings = [];
|
|
291
302
|
const config = this.#configStore.get(botId);
|
|
292
303
|
if (!config) throw new Error('Unknown QQ bot');
|
|
293
304
|
await this.#withBotTransition(botId, async () => {
|
|
294
|
-
const previous = await this.#credentials.resolve(config.secretRef)
|
|
305
|
+
const previous = await atConnectionStage('credential.read', () => this.#credentials.resolve(config.secretRef), 'credential-store');
|
|
295
306
|
await this.#stopRuntime(botId);
|
|
296
307
|
try {
|
|
297
|
-
await this.#credentials.unset(config.secretRef);
|
|
298
|
-
await this.#configStore.remove(botId);
|
|
308
|
+
await atConnectionStage('credential.remove', () => this.#credentials.unset(config.secretRef), 'credential-store');
|
|
309
|
+
await atConnectionStage('account.remove', () => this.#configStore.remove(botId), 'account-config');
|
|
299
310
|
} catch (error) {
|
|
300
|
-
if (
|
|
301
|
-
|
|
302
|
-
|
|
311
|
+
if (!this.#configStore.get(botId)) {
|
|
312
|
+
warnings.push(this.#diagnostics.report(error, { operation: 'bot.delete', stage: 'workspace.cleanup', warning: true,
|
|
313
|
+
publicError: { code: 'workspace-cleanup-failed', message: '账号已移除,但本地状态清理失败。' } }).publicError);
|
|
314
|
+
} else {
|
|
315
|
+
if (previous?.value) {
|
|
316
|
+
await atConnectionStage('credential.save', () => this.#credentials.set(config.secretRef, previous.value), 'credential-store').catch(() => undefined);
|
|
317
|
+
await this.#startRuntime(config, previous.value).catch(() => undefined);
|
|
318
|
+
}
|
|
319
|
+
throw new Error('Unable to remove the QQ bot safely.', { cause: error });
|
|
303
320
|
}
|
|
304
|
-
throw new Error('Unable to remove the QQ bot safely.', { cause: error });
|
|
305
321
|
}
|
|
306
|
-
await this.#deleteState({ botId, config }).catch(
|
|
307
|
-
this.#logger.warn?.(`[dsh-im:qq] bot ${botId} state cleanup failed:`, error);
|
|
308
|
-
});
|
|
322
|
+
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); });
|
|
309
323
|
this.#errors.delete(botId);
|
|
310
324
|
this.#touch();
|
|
311
325
|
});
|
|
312
|
-
return this.status();
|
|
326
|
+
return { ...this.status(), ...(warnings.length ? { warnings } : {}) };
|
|
313
327
|
}
|
|
314
328
|
|
|
315
329
|
status() {
|
|
@@ -341,7 +355,7 @@ export class QqController {
|
|
|
341
355
|
messagesReplied: runtimeStatus?.messagesReplied ?? 0,
|
|
342
356
|
},
|
|
343
357
|
lastMessageError: publicMessageFailure(runtimeStatus?.lastMessageError),
|
|
344
|
-
error: structuredClone(this.#errors.get(config.botId) ?? null),
|
|
358
|
+
error: structuredClone(runtimeStatus?.error ?? this.#errors.get(config.botId) ?? null),
|
|
345
359
|
};
|
|
346
360
|
});
|
|
347
361
|
const connectedCount = bots.filter((bot) => bot.connected).length;
|
|
@@ -390,8 +404,8 @@ export class QqController {
|
|
|
390
404
|
record.error = safeError('cancelled', t('扫码绑定已取消。'));
|
|
391
405
|
} else {
|
|
392
406
|
record.state = 'failed';
|
|
393
|
-
record.error =
|
|
394
|
-
|
|
407
|
+
record.error = this.#failure(error, 'activation-failed', t('QQ 已授权,但无法安全保存接入配置。'));
|
|
408
|
+
|
|
395
409
|
}
|
|
396
410
|
} finally {
|
|
397
411
|
record.dispose?.();
|
|
@@ -403,7 +417,7 @@ export class QqController {
|
|
|
403
417
|
async #activateBot(record, { appId, appSecret, ownerUserOpenid }) {
|
|
404
418
|
const identity = deriveQqBotIdentity(appId);
|
|
405
419
|
const previousConfig = this.#configStore.getByAppId(appId);
|
|
406
|
-
const previousSecret = await this.#credentials.resolve(identity.secretRef)
|
|
420
|
+
const previousSecret = await atConnectionStage('credential.read', () => this.#credentials.resolve(identity.secretRef), 'credential-store');
|
|
407
421
|
const config = {
|
|
408
422
|
botId: identity.botId,
|
|
409
423
|
appId,
|
|
@@ -413,10 +427,10 @@ export class QqController {
|
|
|
413
427
|
connectedAt: new Date().toISOString(),
|
|
414
428
|
};
|
|
415
429
|
return this.#withBotTransition(identity.botId, async () => {
|
|
416
|
-
await this.#credentials.set(identity.secretRef, appSecret);
|
|
430
|
+
await atConnectionStage('credential.save', () => this.#credentials.set(identity.secretRef, appSecret), 'credential-store');
|
|
417
431
|
try {
|
|
418
432
|
if (record.controller.signal.aborted) throw new DOMException('Cancelled', 'AbortError');
|
|
419
|
-
await this.#configStore.save(config);
|
|
433
|
+
await atConnectionStage('account.save', () => this.#configStore.save(config), 'account-config');
|
|
420
434
|
} catch (error) {
|
|
421
435
|
await this.#restoreCredential(identity.secretRef, previousSecret);
|
|
422
436
|
throw error;
|
|
@@ -428,20 +442,20 @@ export class QqController {
|
|
|
428
442
|
} catch (error) {
|
|
429
443
|
if (record.controller.signal.aborted) {
|
|
430
444
|
await this.#stopRuntime(identity.botId);
|
|
431
|
-
if (previousConfig) await this.#configStore.save(previousConfig).catch(() => undefined);
|
|
445
|
+
if (previousConfig) await atConnectionStage('account.save', () => this.#configStore.save(previousConfig), 'account-config').catch(() => undefined);
|
|
432
446
|
else {
|
|
433
|
-
const removed = await this.#configStore.remove(identity.botId).catch(() => null);
|
|
447
|
+
const removed = await atConnectionStage('account.remove', () => this.#configStore.remove(identity.botId), 'account-config').catch(() => null);
|
|
434
448
|
if (removed) {
|
|
435
449
|
await this.#deleteState({ botId: identity.botId, config }).catch((cleanupError) => {
|
|
436
|
-
this.#logger.warn?.('[dsh-im:qq] cancelled bot state cleanup failed:', cleanupError);
|
|
450
|
+
this.#logger.warn?.('[dsh-im:qq] cancelled bot state cleanup failed:', extractConnectionEvidence(cleanupError).details);
|
|
437
451
|
});
|
|
438
452
|
}
|
|
439
453
|
}
|
|
440
454
|
await this.#restoreCredential(identity.secretRef, previousSecret);
|
|
441
455
|
throw error;
|
|
442
456
|
}
|
|
443
|
-
this.#errors.set(identity.botId, connectionError(error, t('QQ 机器人已绑定,消息连接暂未就绪。')));
|
|
444
|
-
|
|
457
|
+
this.#errors.set(identity.botId, this.#diagnostics.report(error, { reuse: true, publicError: connectionError(error, t('QQ 机器人已绑定,消息连接暂未就绪。')) , stage: 'connection.start' }).publicError);
|
|
458
|
+
|
|
445
459
|
}
|
|
446
460
|
this.#touch();
|
|
447
461
|
return identity.botId;
|
|
@@ -452,7 +466,7 @@ export class QqController {
|
|
|
452
466
|
if (this.#closed) throw new Error('QQ controller is closed');
|
|
453
467
|
await this.#stopRuntime(config.botId);
|
|
454
468
|
if (this.#closed) throw new Error('QQ controller is closed');
|
|
455
|
-
const runtime = await this.#createRuntime({ botId: config.botId, config, appSecret });
|
|
469
|
+
const runtime = await atConnectionStage('runtime.prepare', () => this.#createRuntime({ botId: config.botId, config, appSecret }));
|
|
456
470
|
if (!runtime || typeof runtime.start !== 'function' || typeof runtime.stop !== 'function') {
|
|
457
471
|
throw new TypeError('createRuntime returned an invalid QQ runtime');
|
|
458
472
|
}
|
|
@@ -470,18 +484,18 @@ export class QqController {
|
|
|
470
484
|
const runtime = this.#runtimes.get(botId);
|
|
471
485
|
this.#runtimes.delete(botId);
|
|
472
486
|
await runtime?.stop().catch((error) => {
|
|
473
|
-
this.#logger.warn?.(`[dsh-im:qq] bot ${botId} failed to stop cleanly:`, error);
|
|
487
|
+
this.#logger.warn?.(`[dsh-im:qq] bot ${botId} failed to stop cleanly:`, extractConnectionEvidence(error).details);
|
|
474
488
|
});
|
|
475
489
|
}
|
|
476
490
|
|
|
477
491
|
async #resolveSecret(ref) {
|
|
478
|
-
const result = await this.#credentials.resolve(ref)
|
|
492
|
+
const result = await atConnectionStage('credential.read', () => this.#credentials.resolve(ref), 'credential-store');
|
|
479
493
|
return cleanString(result?.value);
|
|
480
494
|
}
|
|
481
495
|
|
|
482
496
|
async #restoreCredential(ref, previous) {
|
|
483
|
-
if (previous?.value) await this.#credentials.set(ref, previous.value).catch(() => undefined);
|
|
484
|
-
else await this.#credentials.unset(ref).catch(() => undefined);
|
|
497
|
+
if (previous?.value) await atConnectionStage('credential.save', () => this.#credentials.set(ref, previous.value), 'credential-store').catch(() => undefined);
|
|
498
|
+
else await atConnectionStage('credential.remove', () => this.#credentials.unset(ref), 'credential-store').catch(() => undefined);
|
|
485
499
|
}
|
|
486
500
|
|
|
487
501
|
#withBotTransition(botId, operation) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { extractConnectionEvidence, createConnectionDiagnostics, atConnectionStage } from '../shared/connection-error.mjs';
|
|
1
2
|
import { QQBot, contentSanitizer, typingIndicator } from '@tencent-connect/qqbot-nodejs';
|
|
2
3
|
|
|
3
4
|
import {
|
|
@@ -35,6 +36,7 @@ export class QqRuntime {
|
|
|
35
36
|
#contextEnhancement;
|
|
36
37
|
#accessPolicy;
|
|
37
38
|
#logger;
|
|
39
|
+
#diagnostics;
|
|
38
40
|
#replyTimeoutMs;
|
|
39
41
|
#connectTimeoutMs;
|
|
40
42
|
#createBot;
|
|
@@ -68,7 +70,7 @@ export class QqRuntime {
|
|
|
68
70
|
this.#state = state;
|
|
69
71
|
this.#contextEnhancement = contextEnhancement;
|
|
70
72
|
this.#accessPolicy = accessPolicy;
|
|
71
|
-
this.#logger = logger;
|
|
73
|
+
this.#logger = logger; this.#diagnostics = createConnectionDiagnostics({ channel: 'qq', logger });
|
|
72
74
|
this.#replyTimeoutMs = replyTimeoutMs;
|
|
73
75
|
this.#connectTimeoutMs = connectTimeoutMs;
|
|
74
76
|
this.#createBot = createBot;
|
|
@@ -137,8 +139,8 @@ export class QqRuntime {
|
|
|
137
139
|
await this.stop();
|
|
138
140
|
this.#status.startedAt = new Date().toISOString();
|
|
139
141
|
this.#status.qqConnectionState = 'connecting';
|
|
140
|
-
this.#status.lastError = null;
|
|
141
|
-
await this.#harness.ensureRunning();
|
|
142
|
+
this.#status.lastError = null; this.#status.error = null; this.#diagnostics.clear();
|
|
143
|
+
await atConnectionStage('harness.check', () => this.#harness.ensureRunning());
|
|
142
144
|
this.#status.harnessReachable = true;
|
|
143
145
|
|
|
144
146
|
const sdkLogger = {
|
|
@@ -211,14 +213,15 @@ export class QqRuntime {
|
|
|
211
213
|
this.#status.qqConnectionState = 'connected';
|
|
212
214
|
this.#status.lastCheckedAt = now;
|
|
213
215
|
this.#status.lastConnectedAt = now;
|
|
214
|
-
this.#status.lastError = null;
|
|
216
|
+
this.#status.lastError = null; this.#status.error = null; this.#diagnostics.clear();
|
|
215
217
|
readyResolve();
|
|
216
218
|
};
|
|
217
219
|
const onError = (error) => {
|
|
218
220
|
if (!this.#status.ready) readyReject(error);
|
|
219
221
|
else {
|
|
220
|
-
this.#status.
|
|
221
|
-
this.#
|
|
222
|
+
this.#status.error = this.#diagnostics.report(error, { operation: 'connection.monitor', botId: this.#config?.botId, automatic: true }).publicError;
|
|
223
|
+
this.#status.lastError = this.#status.error.message;
|
|
224
|
+
this.#logger.warn?.(`[dsh-im:qq] bot ${this.#config.botId} connection error:`, extractConnectionEvidence(error).details);
|
|
222
225
|
}
|
|
223
226
|
};
|
|
224
227
|
const onMessage = (_ctx, message) => {
|
|
@@ -228,7 +231,7 @@ export class QqRuntime {
|
|
|
228
231
|
if (controller.signal.aborted) return;
|
|
229
232
|
this.#logger.error?.(
|
|
230
233
|
`[dsh-im:qq] bot ${this.#config.botId} message handling failed:`,
|
|
231
|
-
error,
|
|
234
|
+
extractConnectionEvidence(error).details,
|
|
232
235
|
);
|
|
233
236
|
});
|
|
234
237
|
};
|
|
@@ -244,8 +247,9 @@ export class QqRuntime {
|
|
|
244
247
|
readyReject(error);
|
|
245
248
|
this.#status.ready = false;
|
|
246
249
|
this.#status.qqConnectionState = 'failed';
|
|
247
|
-
this.#status.
|
|
248
|
-
this.#
|
|
250
|
+
this.#status.error = this.#diagnostics.report(error, { operation: 'connection.monitor', botId: this.#config?.botId, automatic: true }).publicError;
|
|
251
|
+
this.#status.lastError = this.#status.error.message;
|
|
252
|
+
this.#logger.error?.(`[dsh-im:qq] bot ${this.#config.botId} connection stopped:`, extractConnectionEvidence(error).details);
|
|
249
253
|
});
|
|
250
254
|
|
|
251
255
|
let timer;
|
|
@@ -264,7 +268,8 @@ export class QqRuntime {
|
|
|
264
268
|
} catch (error) {
|
|
265
269
|
this.#status.ready = false;
|
|
266
270
|
this.#status.qqConnectionState = 'failed';
|
|
267
|
-
this.#status.
|
|
271
|
+
this.#status.error = this.#diagnostics.report(error, { operation: 'connection.monitor', botId: this.#config?.botId, automatic: true }).publicError;
|
|
272
|
+
this.#status.lastError = this.#status.error.message;
|
|
268
273
|
await this.stop();
|
|
269
274
|
throw error;
|
|
270
275
|
} finally {
|
|
@@ -284,7 +289,7 @@ export class QqRuntime {
|
|
|
284
289
|
try {
|
|
285
290
|
bot?.stop();
|
|
286
291
|
} catch (error) {
|
|
287
|
-
this.#logger.warn?.(`[dsh-im:qq] bot ${this.#config.botId} failed to stop cleanly:`, error);
|
|
292
|
+
this.#logger.warn?.(`[dsh-im:qq] bot ${this.#config.botId} failed to stop cleanly:`, extractConnectionEvidence(error).details);
|
|
288
293
|
}
|
|
289
294
|
await Promise.race([
|
|
290
295
|
runTask?.catch(() => undefined) ?? Promise.resolve(),
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { t } from './i18n.mjs';
|
|
2
|
+
import { atConnectionStage, connectionStageError, extractConnectionEvidence } from './connection-error.mjs';
|
|
1
3
|
import { validateBotAlias, withBotAlias } from './bot-alias.mjs';
|
|
2
4
|
import { defaultImWorkspace, sameWorkspacePath } from './default-workspace.mjs';
|
|
3
5
|
import {
|
|
@@ -1475,7 +1477,10 @@ export function observeBotWorkspaceRemovals(
|
|
|
1475
1477
|
return async (...args) => {
|
|
1476
1478
|
const removed = await value.apply(target, args);
|
|
1477
1479
|
const botId = removed ? botIdFromRemoved(removed, args) : null;
|
|
1478
|
-
if (botId)
|
|
1480
|
+
if (botId) {
|
|
1481
|
+
const cleanup = await atConnectionStage('workspace.cleanup', () => workspaces.retireAfterConfigCommit(botId), 'workspace-config');
|
|
1482
|
+
if (cleanup?.error) throw connectionStageError(cleanup.error, 'workspace.cleanup', 'workspace-config');
|
|
1483
|
+
}
|
|
1479
1484
|
return removed;
|
|
1480
1485
|
};
|
|
1481
1486
|
}
|
|
@@ -2279,6 +2284,17 @@ export function createWorkspaceAwareController(controller, {
|
|
|
2279
2284
|
});
|
|
2280
2285
|
};
|
|
2281
2286
|
const deleteWithWorkspace = (botId, invokeDelete) => withBotTransition(botId, async () => {
|
|
2287
|
+
const warnings = [];
|
|
2288
|
+
const cleanupWarning = (error, stage = 'workspace.cleanup') => {
|
|
2289
|
+
if (!controller.diagnostics) return;
|
|
2290
|
+
warnings.push(controller.diagnostics.report(error, { operation: 'bot.delete', stage, botId, warning: true,
|
|
2291
|
+
publicError: { code: 'workspace-cleanup-failed', message: stage === 'state.cleanup'
|
|
2292
|
+
? t('本地会话状态清理失败,请查看诊断详情。') : t('账号已移除,但本地状态清理失败。') } }).publicError);
|
|
2293
|
+
};
|
|
2294
|
+
const finishRemoval = async () => {
|
|
2295
|
+
const outcome = await workspaces.finishRemoval(removal);
|
|
2296
|
+
if (outcome?.error) cleanupWarning(outcome.error);
|
|
2297
|
+
};
|
|
2282
2298
|
// Fence the old runtime without changing the durable mapping. A crash
|
|
2283
2299
|
// before the controller removes its config therefore keeps the bot's
|
|
2284
2300
|
// workspace, while a crash after that commit is healed by startup
|
|
@@ -2292,24 +2308,27 @@ export function createWorkspaceAwareController(controller, {
|
|
|
2292
2308
|
}
|
|
2293
2309
|
await state.clearSessions();
|
|
2294
2310
|
} catch (error) {
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
botId,
|
|
2298
|
-
error?.message ?? error,
|
|
2299
|
-
);
|
|
2311
|
+
if (controller.diagnostics) cleanupWarning(error, 'state.cleanup');
|
|
2312
|
+
else console.warn('[dsh-im] ignored session cleanup failure while deleting bot:', extractConnectionEvidence(error).details);
|
|
2300
2313
|
}
|
|
2301
2314
|
},
|
|
2302
2315
|
});
|
|
2303
2316
|
try {
|
|
2304
2317
|
const result = await invokeDelete();
|
|
2305
|
-
await
|
|
2306
|
-
return decorate(result);
|
|
2318
|
+
await finishRemoval();
|
|
2319
|
+
return decorate({ ...result, ...(warnings.length ? { warnings: [...(result?.warnings ?? []), ...warnings] } : {}) });
|
|
2307
2320
|
} catch (error) {
|
|
2308
2321
|
const after = await targetStatus(controller).catch(() => null);
|
|
2309
2322
|
const knownAbsent = Array.isArray(after?.bots)
|
|
2310
2323
|
&& !after.bots.some((bot) => bot?.botId === botId);
|
|
2311
|
-
if (knownAbsent)
|
|
2312
|
-
|
|
2324
|
+
if (knownAbsent) {
|
|
2325
|
+
if (controller.diagnostics) {
|
|
2326
|
+
cleanupWarning(error);
|
|
2327
|
+
try { await finishRemoval(); } catch (cleanupError) { cleanupWarning(cleanupError); }
|
|
2328
|
+
return decorate({ ...after, warnings });
|
|
2329
|
+
}
|
|
2330
|
+
await workspaces.finishRemoval(removal);
|
|
2331
|
+
} else await workspaces.abortRemoval(removal);
|
|
2313
2332
|
throw error;
|
|
2314
2333
|
}
|
|
2315
2334
|
});
|