@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.
Files changed (137) hide show
  1. package/README.en.md +3 -3
  2. package/README.md +3 -3
  3. package/lib/client.js +2505 -1152
  4. package/lib/index.js +290 -299
  5. package/package.json +8 -1
  6. package/plugin-src/client/channel-card-meta.js +2 -0
  7. package/plugin-src/client/channel-logos.js +11 -0
  8. package/plugin-src/client/channels/dingtalk/api.js +5 -1
  9. package/plugin-src/client/channels/dingtalk/index.js +15 -13
  10. package/plugin-src/client/channels/email/api.js +52 -0
  11. package/plugin-src/client/channels/email/index.js +728 -0
  12. package/plugin-src/client/channels/email/styles.js +51 -0
  13. package/plugin-src/client/channels/feishu/api.js +4 -2
  14. package/plugin-src/client/channels/feishu/index.js +16 -3
  15. package/plugin-src/client/channels/imessage/index.js +2 -1
  16. package/plugin-src/client/channels/office/api.js +2 -0
  17. package/plugin-src/client/channels/office/index.js +6 -5
  18. package/plugin-src/client/channels/qq/api.js +7 -0
  19. package/plugin-src/client/channels/qq/index.js +16 -3
  20. package/plugin-src/client/channels/shared/token-api.js +5 -1
  21. package/plugin-src/client/channels/shared/token-channel.js +28 -3
  22. package/plugin-src/client/channels/slack/index.js +2 -1
  23. package/plugin-src/client/channels/wecom/api.js +7 -1
  24. package/plugin-src/client/channels/wecom/index.js +16 -3
  25. package/plugin-src/client/channels/wecom-app/api.js +6 -1
  26. package/plugin-src/client/channels/wecom-app/index.js +16 -3
  27. package/plugin-src/client/channels/weixin/api.js +2 -1
  28. package/plugin-src/client/channels/weixin/connection-error.js +1 -71
  29. package/plugin-src/client/channels/whatsapp/api.js +5 -0
  30. package/plugin-src/client/channels/whatsapp/index.js +16 -3
  31. package/plugin-src/client/connection-error.js +87 -0
  32. package/plugin-src/client/global-settings.js +81 -1
  33. package/plugin-src/client/i18n.js +61 -0
  34. package/plugin-src/client/index.js +30 -2
  35. package/plugin-src/client/last-message-error.js +2 -1
  36. package/plugin-src/client/styles.js +6 -0
  37. package/plugin-src/host/build.mjs +4 -0
  38. package/plugin-src/host/channels/dingtalk/production.mjs +2 -0
  39. package/plugin-src/host/channels/dingtalk/rpc.mjs +7 -4
  40. package/plugin-src/host/channels/email/availability.mjs +60 -0
  41. package/plugin-src/host/channels/email/index.mjs +36 -0
  42. package/plugin-src/host/channels/email/production.mjs +58 -0
  43. package/plugin-src/host/channels/email/rpc.mjs +163 -0
  44. package/plugin-src/host/channels/feishu/production.mjs +3 -0
  45. package/plugin-src/host/channels/feishu/rpc.mjs +7 -4
  46. package/plugin-src/host/channels/imessage/rpc.mjs +13 -6
  47. package/plugin-src/host/channels/office/rpc.mjs +3 -1
  48. package/plugin-src/host/channels/qq/production.mjs +2 -0
  49. package/plugin-src/host/channels/qq/rpc.mjs +5 -3
  50. package/plugin-src/host/channels/shared/production.mjs +52 -5
  51. package/plugin-src/host/channels/shared/rpc.mjs +5 -3
  52. package/plugin-src/host/channels/shared/startup-error.mjs +4 -3
  53. package/plugin-src/host/channels/shared/startup.mjs +11 -6
  54. package/plugin-src/host/channels/slack/production.mjs +2 -0
  55. package/plugin-src/host/channels/slack/rpc.mjs +5 -3
  56. package/plugin-src/host/channels/wecom/production.mjs +2 -0
  57. package/plugin-src/host/channels/wecom/rpc.mjs +5 -3
  58. package/plugin-src/host/channels/wecom-app/production.mjs +2 -0
  59. package/plugin-src/host/channels/wecom-app/rpc.mjs +5 -3
  60. package/plugin-src/host/channels/weixin/production.mjs +2 -0
  61. package/plugin-src/host/channels/whatsapp/production.mjs +2 -0
  62. package/plugin-src/host/channels/whatsapp/rpc.mjs +5 -3
  63. package/plugin-src/host/delivery-adapter.mjs +11 -0
  64. package/plugin-src/host/image-input-rpc.mjs +24 -0
  65. package/plugin-src/host/inbound-ttl-rpc.mjs +6 -1
  66. package/plugin-src/host/index.mjs +3 -0
  67. package/plugin-src/management-rpc.mjs +12 -2
  68. package/scripts/verify-package.mjs +3 -1
  69. package/src/channels/dingtalk/connection-error.mjs +5 -12
  70. package/src/channels/dingtalk/device-auth.mjs +4 -1
  71. package/src/channels/dingtalk/dingtalk-bridge.mjs +6 -4
  72. package/src/channels/dingtalk/dingtalk-controller.mjs +60 -45
  73. package/src/channels/dingtalk/dingtalk-runtime.mjs +20 -4
  74. package/src/channels/discord/discord-api.mjs +3 -3
  75. package/src/channels/discord/discord-runtime.mjs +18 -12
  76. package/src/channels/email/config-store.mjs +194 -0
  77. package/src/channels/email/email-api.mjs +20 -0
  78. package/src/channels/email/email-bridge.mjs +17 -0
  79. package/src/channels/email/email-controller.mjs +824 -0
  80. package/src/channels/email/email-runtime.mjs +692 -0
  81. package/src/channels/email/harness-client.mjs +7 -0
  82. package/src/channels/email/mail-format.mjs +91 -0
  83. package/src/channels/email/state-store.mjs +176 -0
  84. package/src/channels/email/transport.mjs +64 -0
  85. package/src/channels/email/transports/agent-mail.mjs +593 -0
  86. package/src/channels/email/transports/agently-cli.mjs +288 -0
  87. package/src/channels/email/transports/imap-smtp.mjs +165 -0
  88. package/src/channels/feishu/bridge.mjs +8 -7
  89. package/src/channels/feishu/feishu-runtime.mjs +15 -8
  90. package/src/channels/feishu/group-message-permission-manager.mjs +2 -1
  91. package/src/channels/feishu/message-utils.mjs +3 -2
  92. package/src/channels/feishu/multi-bot-controller.mjs +56 -47
  93. package/src/channels/feishu/registration-manager.mjs +8 -4
  94. package/src/channels/feishu/repair-manager.mjs +2 -1
  95. package/src/channels/imessage/runtime.mjs +9 -5
  96. package/src/channels/office/office-controller.mjs +23 -12
  97. package/src/channels/office/office-runtime.mjs +6 -3
  98. package/src/channels/office/office-transport.mjs +1 -0
  99. package/src/channels/qq/qq-bridge.mjs +7 -5
  100. package/src/channels/qq/qq-controller.mjs +56 -42
  101. package/src/channels/qq/qq-runtime.mjs +16 -11
  102. package/src/channels/shared/bot-workspace-store.mjs +29 -10
  103. package/src/channels/shared/connection-error.mjs +191 -0
  104. package/src/channels/shared/connection-test.mjs +3 -1
  105. package/src/channels/shared/conversation-state-store.mjs +46 -1
  106. package/src/channels/shared/diagnostic-details.mjs +95 -0
  107. package/src/channels/shared/harness-client.mjs +63 -18
  108. package/src/channels/shared/i18n-en/diagnostics.mjs +55 -0
  109. package/src/channels/shared/i18n-en/email.mjs +25 -0
  110. package/src/channels/shared/i18n-en/image-input.mjs +17 -0
  111. package/src/channels/shared/i18n-en/shared-a.mjs +1 -0
  112. package/src/channels/shared/i18n-en/shared-c.mjs +1 -1
  113. package/src/channels/shared/i18n-en.mjs +6 -0
  114. package/src/channels/shared/image-input-policy.mjs +35 -0
  115. package/src/channels/shared/image-input-settings-store.mjs +60 -0
  116. package/src/channels/shared/image-input.mjs +124 -0
  117. package/src/channels/shared/image-prompt.mjs +15 -7
  118. package/src/channels/shared/message-failure.mjs +12 -0
  119. package/src/channels/shared/semantic/reply-reference.mjs +3 -3
  120. package/src/channels/shared/text-harness-bridge.mjs +90 -11
  121. package/src/channels/shared/token-bot-controller.mjs +52 -47
  122. package/src/channels/slack/slack-api.mjs +3 -3
  123. package/src/channels/slack/slack-controller.mjs +59 -51
  124. package/src/channels/slack/slack-runtime.mjs +18 -12
  125. package/src/channels/telegram/telegram-api.mjs +2 -2
  126. package/src/channels/telegram/telegram-runtime.mjs +17 -13
  127. package/src/channels/wecom/wecom-bridge.mjs +15 -6
  128. package/src/channels/wecom/wecom-controller.mjs +51 -37
  129. package/src/channels/wecom/wecom-runtime.mjs +10 -6
  130. package/src/channels/wecom-app/wecom-app-bridge.mjs +7 -5
  131. package/src/channels/wecom-app/wecom-app-controller.mjs +50 -37
  132. package/src/channels/wecom-app/wecom-app-runtime.mjs +2 -1
  133. package/src/channels/weixin/connection-error.mjs +26 -92
  134. package/src/channels/weixin/diagnostic-details.mjs +1 -63
  135. package/src/channels/weixin/weixin-bridge.mjs +7 -5
  136. package/src/channels/whatsapp/whatsapp-controller.mjs +34 -17
  137. 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
- this.#logger.warn?.(`[dsh-im:qq] bot ${config.botId} failed to initialize:`, error);
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 = safeError('qr-connect-failed', t('QQ 扫码服务暂时不可用,请重新生成二维码。'));
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 = safeError('qr-start-failed', t('无法生成 QQ 二维码,请稍后重试。'));
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).catch(() => undefined);
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
- this.#logger.warn?.(`[dsh-im:qq] bot ${identity.botId} credential connection failed:`, error);
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).catch(() => undefined);
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 (previous?.value) {
301
- await this.#credentials.set(config.secretRef, previous.value).catch(() => undefined);
302
- await this.#startRuntime(config, previous.value).catch(() => undefined);
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((error) => {
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 = safeError('activation-failed', t('QQ 已授权,但无法安全保存接入配置。'));
394
- this.#logger.error?.('[dsh-im:qq] provisioning failed:', error);
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).catch(() => undefined);
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
- this.#logger.warn?.(`[dsh-im:qq] bot ${identity.botId} activation connection failed:`, error);
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).catch(() => undefined);
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.lastError = error?.message ?? String(error);
221
- this.#logger.warn?.(`[dsh-im:qq] bot ${this.#config.botId} connection error:`, error);
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.lastError = error?.message ?? String(error);
248
- this.#logger.error?.(`[dsh-im:qq] bot ${this.#config.botId} connection stopped:`, error);
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.lastError = error?.message ?? String(error);
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) await workspaces.retireAfterConfigCommit(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
- console.warn(
2296
- '[dsh-im] ignored session cleanup failure while deleting bot:',
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 workspaces.finishRemoval(removal);
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) await workspaces.finishRemoval(removal);
2312
- else await workspaces.abortRemoval(removal);
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
  });