@xmanrui/dsh-im 4.21.2 → 4.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/README.en.md +10 -5
  2. package/README.md +10 -5
  3. package/docs/client-integration.md +81 -0
  4. package/lib/client.js +2565 -1134
  5. package/lib/index.js +292 -301
  6. package/package.json +9 -1
  7. package/plugin-src/client/channel-card-meta.js +2 -0
  8. package/plugin-src/client/channel-logos.js +11 -0
  9. package/plugin-src/client/channels/dingtalk/api.js +5 -1
  10. package/plugin-src/client/channels/dingtalk/index.js +15 -13
  11. package/plugin-src/client/channels/email/api.js +52 -0
  12. package/plugin-src/client/channels/email/index.js +728 -0
  13. package/plugin-src/client/channels/email/styles.js +51 -0
  14. package/plugin-src/client/channels/feishu/api.js +4 -2
  15. package/plugin-src/client/channels/feishu/index.js +16 -3
  16. package/plugin-src/client/channels/imessage/index.js +2 -1
  17. package/plugin-src/client/channels/office/api.js +2 -0
  18. package/plugin-src/client/channels/office/index.js +6 -5
  19. package/plugin-src/client/channels/qq/api.js +7 -0
  20. package/plugin-src/client/channels/qq/index.js +16 -3
  21. package/plugin-src/client/channels/shared/token-api.js +5 -1
  22. package/plugin-src/client/channels/shared/token-channel.js +28 -3
  23. package/plugin-src/client/channels/slack/index.js +2 -1
  24. package/plugin-src/client/channels/wecom/api.js +7 -1
  25. package/plugin-src/client/channels/wecom/index.js +16 -3
  26. package/plugin-src/client/channels/wecom-app/api.js +6 -1
  27. package/plugin-src/client/channels/wecom-app/index.js +16 -3
  28. package/plugin-src/client/channels/weixin/api.js +2 -1
  29. package/plugin-src/client/channels/weixin/connection-error.js +1 -71
  30. package/plugin-src/client/channels/whatsapp/api.js +5 -0
  31. package/plugin-src/client/channels/whatsapp/index.js +16 -3
  32. package/plugin-src/client/connection-error.js +87 -0
  33. package/plugin-src/client/global-settings.js +81 -1
  34. package/plugin-src/client/i18n.js +62 -0
  35. package/plugin-src/client/index.js +122 -28
  36. package/plugin-src/client/last-message-error.js +2 -1
  37. package/plugin-src/client/panel-error-boundary.js +21 -0
  38. package/plugin-src/client/styles.js +6 -0
  39. package/plugin-src/host/build.mjs +4 -0
  40. package/plugin-src/host/channels/dingtalk/production.mjs +5 -1
  41. package/plugin-src/host/channels/dingtalk/rpc.mjs +7 -4
  42. package/plugin-src/host/channels/email/availability.mjs +60 -0
  43. package/plugin-src/host/channels/email/index.mjs +36 -0
  44. package/plugin-src/host/channels/email/production.mjs +58 -0
  45. package/plugin-src/host/channels/email/rpc.mjs +163 -0
  46. package/plugin-src/host/channels/feishu/production.mjs +6 -1
  47. package/plugin-src/host/channels/feishu/rpc.mjs +7 -4
  48. package/plugin-src/host/channels/imessage/rpc.mjs +13 -6
  49. package/plugin-src/host/channels/office/rpc.mjs +3 -1
  50. package/plugin-src/host/channels/qq/production.mjs +5 -1
  51. package/plugin-src/host/channels/qq/rpc.mjs +5 -3
  52. package/plugin-src/host/channels/shared/production.mjs +55 -6
  53. package/plugin-src/host/channels/shared/rpc.mjs +5 -3
  54. package/plugin-src/host/channels/shared/startup-error.mjs +4 -3
  55. package/plugin-src/host/channels/shared/startup.mjs +11 -6
  56. package/plugin-src/host/channels/slack/production.mjs +5 -1
  57. package/plugin-src/host/channels/slack/rpc.mjs +5 -3
  58. package/plugin-src/host/channels/wecom/production.mjs +5 -1
  59. package/plugin-src/host/channels/wecom/rpc.mjs +5 -3
  60. package/plugin-src/host/channels/wecom-app/production.mjs +5 -1
  61. package/plugin-src/host/channels/wecom-app/rpc.mjs +5 -3
  62. package/plugin-src/host/channels/weixin/production.mjs +5 -1
  63. package/plugin-src/host/channels/whatsapp/production.mjs +5 -1
  64. package/plugin-src/host/channels/whatsapp/rpc.mjs +5 -3
  65. package/plugin-src/host/delivery-adapter.mjs +11 -0
  66. package/plugin-src/host/image-input-rpc.mjs +24 -0
  67. package/plugin-src/host/inbound-ttl-rpc.mjs +6 -1
  68. package/plugin-src/host/index.mjs +3 -0
  69. package/plugin-src/management-rpc.mjs +12 -2
  70. package/scripts/verify-package.mjs +3 -1
  71. package/src/channels/dingtalk/connection-error.mjs +5 -12
  72. package/src/channels/dingtalk/device-auth.mjs +4 -1
  73. package/src/channels/dingtalk/dingtalk-bridge.mjs +6 -4
  74. package/src/channels/dingtalk/dingtalk-controller.mjs +60 -45
  75. package/src/channels/dingtalk/dingtalk-runtime.mjs +20 -4
  76. package/src/channels/discord/discord-api.mjs +3 -3
  77. package/src/channels/discord/discord-runtime.mjs +18 -12
  78. package/src/channels/email/config-store.mjs +194 -0
  79. package/src/channels/email/email-api.mjs +20 -0
  80. package/src/channels/email/email-bridge.mjs +17 -0
  81. package/src/channels/email/email-controller.mjs +824 -0
  82. package/src/channels/email/email-runtime.mjs +692 -0
  83. package/src/channels/email/harness-client.mjs +7 -0
  84. package/src/channels/email/mail-format.mjs +91 -0
  85. package/src/channels/email/state-store.mjs +176 -0
  86. package/src/channels/email/transport.mjs +64 -0
  87. package/src/channels/email/transports/agent-mail.mjs +593 -0
  88. package/src/channels/email/transports/agently-cli.mjs +288 -0
  89. package/src/channels/email/transports/imap-smtp.mjs +165 -0
  90. package/src/channels/feishu/bridge.mjs +8 -7
  91. package/src/channels/feishu/feishu-runtime.mjs +15 -8
  92. package/src/channels/feishu/group-message-permission-manager.mjs +2 -1
  93. package/src/channels/feishu/message-utils.mjs +3 -2
  94. package/src/channels/feishu/multi-bot-controller.mjs +56 -47
  95. package/src/channels/feishu/registration-manager.mjs +8 -4
  96. package/src/channels/feishu/repair-manager.mjs +2 -1
  97. package/src/channels/imessage/runtime.mjs +9 -5
  98. package/src/channels/office/office-controller.mjs +23 -12
  99. package/src/channels/office/office-runtime.mjs +6 -3
  100. package/src/channels/office/office-transport.mjs +1 -0
  101. package/src/channels/qq/qq-bridge.mjs +7 -5
  102. package/src/channels/qq/qq-controller.mjs +56 -42
  103. package/src/channels/qq/qq-runtime.mjs +16 -11
  104. package/src/channels/shared/bot-workspace-store.mjs +37 -21
  105. package/src/channels/shared/connection-error.mjs +191 -0
  106. package/src/channels/shared/connection-test.mjs +3 -1
  107. package/src/channels/shared/conversation-state-store.mjs +46 -1
  108. package/src/channels/shared/default-workspace.mjs +28 -0
  109. package/src/channels/shared/diagnostic-details.mjs +95 -0
  110. package/src/channels/shared/harness-client.mjs +111 -20
  111. package/src/channels/shared/harness-session-binding.mjs +13 -8
  112. package/src/channels/shared/i18n-en/diagnostics.mjs +55 -0
  113. package/src/channels/shared/i18n-en/email.mjs +25 -0
  114. package/src/channels/shared/i18n-en/image-input.mjs +17 -0
  115. package/src/channels/shared/i18n-en/shared-a.mjs +1 -0
  116. package/src/channels/shared/i18n-en/shared-c.mjs +1 -1
  117. package/src/channels/shared/i18n-en.mjs +6 -0
  118. package/src/channels/shared/image-input-policy.mjs +35 -0
  119. package/src/channels/shared/image-input-settings-store.mjs +60 -0
  120. package/src/channels/shared/image-input.mjs +124 -0
  121. package/src/channels/shared/image-prompt.mjs +15 -7
  122. package/src/channels/shared/message-failure.mjs +12 -0
  123. package/src/channels/shared/semantic/reply-reference.mjs +3 -3
  124. package/src/channels/shared/text-harness-bridge.mjs +90 -11
  125. package/src/channels/shared/token-bot-controller.mjs +52 -47
  126. package/src/channels/slack/slack-api.mjs +3 -3
  127. package/src/channels/slack/slack-controller.mjs +59 -51
  128. package/src/channels/slack/slack-runtime.mjs +18 -12
  129. package/src/channels/telegram/telegram-api.mjs +2 -2
  130. package/src/channels/telegram/telegram-runtime.mjs +17 -13
  131. package/src/channels/wecom/wecom-bridge.mjs +15 -6
  132. package/src/channels/wecom/wecom-controller.mjs +51 -37
  133. package/src/channels/wecom/wecom-runtime.mjs +10 -6
  134. package/src/channels/wecom-app/wecom-app-bridge.mjs +7 -5
  135. package/src/channels/wecom-app/wecom-app-controller.mjs +50 -37
  136. package/src/channels/wecom-app/wecom-app-runtime.mjs +2 -1
  137. package/src/channels/weixin/connection-error.mjs +26 -92
  138. package/src/channels/weixin/diagnostic-details.mjs +1 -63
  139. package/src/channels/weixin/weixin-bridge.mjs +7 -5
  140. package/src/channels/whatsapp/whatsapp-controller.mjs +34 -17
  141. package/src/channels/whatsapp/whatsapp-runtime.mjs +35 -8
@@ -1,3 +1,4 @@
1
+ import { extractConnectionEvidence, createConnectionDiagnostics, atConnectionStage } from '../shared/connection-error.mjs';
1
2
  import { randomInt } from 'node:crypto';
2
3
 
3
4
  import { createEditableMessageStream, splitMessageText } from '../shared/editable-message-stream.mjs';
@@ -451,7 +452,7 @@ class TelegramDeliveryStream {
451
452
  try {
452
453
  return await this.#update(block);
453
454
  } catch (error) {
454
- this.#logger.warn?.('[dsh-im:telegram] rich stream update failed:', error);
455
+ this.#logger.warn?.('[dsh-im:telegram] rich stream update failed:', extractConnectionEvidence(error).details);
455
456
  return undefined;
456
457
  }
457
458
  });
@@ -467,7 +468,7 @@ class TelegramDeliveryStream {
467
468
  try {
468
469
  return await this.#update(this.#lastBlock);
469
470
  } catch (error) {
470
- this.#logger.warn?.('[dsh-im:telegram] rich stream refresh failed:', error);
471
+ this.#logger.warn?.('[dsh-im:telegram] rich stream refresh failed:', extractConnectionEvidence(error).details);
471
472
  return undefined;
472
473
  }
473
474
  });
@@ -926,6 +927,7 @@ export class TelegramRuntime {
926
927
  #contextEnhancement;
927
928
  #accessPolicy;
928
929
  #logger;
930
+ #diagnostics;
929
931
  #replyTimeoutMs;
930
932
  #createApi;
931
933
  #createHttpTransport;
@@ -968,7 +970,7 @@ export class TelegramRuntime {
968
970
  this.#state = state;
969
971
  this.#contextEnhancement = contextEnhancement;
970
972
  this.#accessPolicy = accessPolicy;
971
- this.#logger = logger;
973
+ this.#logger = logger; this.#diagnostics = createConnectionDiagnostics({ channel: 'telegram', logger });
972
974
  this.#replyTimeoutMs = replyTimeoutMs;
973
975
  this.#createApi = createApi;
974
976
  this.#createHttpTransport = createHttpTransport;
@@ -1026,8 +1028,8 @@ export class TelegramRuntime {
1026
1028
  this.#connecting = true;
1027
1029
  this.#status.startedAt = new Date().toISOString();
1028
1030
  this.#status.connectionState = 'connecting';
1029
- this.#status.lastError = null;
1030
- await this.#harness.ensureRunning();
1031
+ this.#status.lastError = null; this.#status.error = null; this.#diagnostics.clear();
1032
+ await atConnectionStage('harness.check', () => this.#harness.ensureRunning());
1031
1033
  this.#status.harnessReachable = true;
1032
1034
 
1033
1035
  const controller = new AbortController();
@@ -1057,7 +1059,7 @@ export class TelegramRuntime {
1057
1059
  } catch (error) {
1058
1060
  this.#logger.warn?.(
1059
1061
  `[dsh-im:telegram] bot ${this.#config.botId} command menu setup failed:`,
1060
- error,
1062
+ extractConnectionEvidence(error).details,
1061
1063
  );
1062
1064
  }
1063
1065
  const client = new TelegramBotClient({
@@ -1099,7 +1101,7 @@ export class TelegramRuntime {
1099
1101
  } catch (error) {
1100
1102
  this.#logger.warn?.(
1101
1103
  `[dsh-im:telegram] bot ${this.#config.botId} command menu catch-up failed:`,
1102
- error,
1104
+ extractConnectionEvidence(error).details,
1103
1105
  );
1104
1106
  }
1105
1107
  }
@@ -1108,14 +1110,16 @@ export class TelegramRuntime {
1108
1110
  if (controller.signal.aborted) return;
1109
1111
  this.#status.ready = false;
1110
1112
  this.#status.connectionState = 'failed';
1111
- this.#status.lastError = error?.message ?? String(error);
1112
- this.#logger.error?.(`[dsh-im:telegram] bot ${this.#config.botId} polling stopped:`, error);
1113
+ this.#status.error = this.#diagnostics.report(error, { operation: 'connection.monitor', botId: this.#config?.botId, automatic: true }).publicError;
1114
+ this.#status.lastError = this.#status.error.message;
1115
+
1113
1116
  });
1114
1117
  return this.status;
1115
1118
  } catch (error) {
1116
1119
  this.#status.ready = false;
1117
1120
  this.#status.connectionState = 'failed';
1118
- this.#status.lastError = error?.message ?? String(error);
1121
+ this.#status.error = this.#diagnostics.report(error, { operation: 'connection.restore', reuse: true, botId: this.#config?.botId, automatic: true }).publicError;
1122
+ this.#status.lastError = this.#status.error.message;
1119
1123
  await this.stop();
1120
1124
  throw error;
1121
1125
  } finally {
@@ -1160,7 +1164,7 @@ export class TelegramRuntime {
1160
1164
  } catch (error) {
1161
1165
  this.#logger.warn?.(
1162
1166
  `[dsh-im:telegram] bot ${this.#config.botId} command menu refresh failed:`,
1163
- error,
1167
+ extractConnectionEvidence(error).details,
1164
1168
  );
1165
1169
  return false;
1166
1170
  }
@@ -1210,7 +1214,7 @@ export class TelegramRuntime {
1210
1214
  if (signal.aborted) return;
1211
1215
  this.#logger.error?.(
1212
1216
  `[dsh-im:telegram] bot ${this.#config.botId} callback handling failed:`,
1213
- error,
1217
+ extractConnectionEvidence(error).details,
1214
1218
  );
1215
1219
  });
1216
1220
  }
@@ -1230,7 +1234,7 @@ export class TelegramRuntime {
1230
1234
  if (signal.aborted) return;
1231
1235
  this.#logger.error?.(
1232
1236
  `[dsh-im:telegram] bot ${this.#config.botId} message handling failed:`,
1233
- error,
1237
+ extractConnectionEvidence(error).details,
1234
1238
  );
1235
1239
  });
1236
1240
  }
@@ -1,3 +1,5 @@
1
+ import { imageInputLimits } from '../shared/image-input-policy.mjs';
2
+ import { imageDownloadLimitMessage } from '../shared/image-prompt.mjs';
1
3
  import { createDeferredDeliveryCoordinator, deferredOutcomeText } from '../shared/deferred-delivery-coordinator.mjs';
2
4
  import { generateReqId } from '@wecom/aibot-node-sdk';
3
5
  import { randomUUID } from 'node:crypto';
@@ -216,7 +218,7 @@ function imageSource(client, image) {
216
218
  throw new ImagePromptError(
217
219
  'image-too-large',
218
220
  `Enterprise WeChat image exceeds ${maxBytes} bytes`,
219
- t('图片超过 5 MB,请压缩后重试。'),
221
+ imageDownloadLimitMessage(maxBytes),
220
222
  );
221
223
  }
222
224
  return { data, name: result?.filename };
@@ -280,12 +282,17 @@ function prefetchInboundFiles(message, signal) {
280
282
  };
281
283
  }
282
284
 
283
- function prefetchInboundImages(message, signal) {
285
+ function prefetchInboundImages(message, signal, limitsPromise) {
284
286
  if (!hasInboundImages(message)) return message;
285
287
  return {
286
288
  ...message,
287
289
  images: message.images.map((source) => {
288
- const download = source.load({ signal, maxBytes: MAX_IMAGE_BYTES });
290
+ const download = Promise.resolve(limitsPromise).then((limits) => {
291
+ if (message.images.length > limits.maxImages) throw new ImagePromptError(
292
+ 'too-many-images', 'Too many inbound images',
293
+ t('一次最多只能处理 {maxImages} 张图片。', { maxImages: limits.maxImages }));
294
+ return source.load({ signal, maxBytes: limits.maxDownloadBytes });
295
+ });
289
296
  // The conversation queue may not consume this promise immediately. Keep
290
297
  // an attached rejection handler while preserving the original outcome.
291
298
  download.catch(() => undefined);
@@ -301,7 +308,7 @@ function prefetchInboundImages(message, signal) {
301
308
  throw new ImagePromptError(
302
309
  'image-too-large',
303
310
  `Enterprise WeChat image exceeds ${maxBytes} bytes`,
304
- t('图片超过 5 MB,请压缩后重试。'),
311
+ imageDownloadLimitMessage(maxBytes),
305
312
  );
306
313
  }
307
314
  return result;
@@ -1060,7 +1067,8 @@ export class WecomHarnessBridge {
1060
1067
  if (this.#prefetchedImageCount + imageCount <= MAX_PREFETCHED_IMAGES) {
1061
1068
  reservedImages = imageCount;
1062
1069
  this.#prefetchedImageCount += reservedImages;
1063
- preparedMessage = prefetchInboundImages(inboundMessage, this.#signal);
1070
+ preparedMessage = prefetchInboundImages(inboundMessage, this.#signal,
1071
+ this.#harness.getImageInputLimits?.() ?? imageInputLimits());
1064
1072
  } else {
1065
1073
  preparedMessage = imageQueueFullMessage(inboundMessage);
1066
1074
  }
@@ -1356,7 +1364,7 @@ export class WecomHarnessBridge {
1356
1364
  }
1357
1365
 
1358
1366
  let content = hasImages || hasReply
1359
- ? await promptContentForInboundMessage(message, { signal: this.#signal })
1367
+ ? await promptContentForInboundMessage(message, { signal: this.#signal, deferImages: true })
1360
1368
  : undefined;
1361
1369
  const snapshot = this.#acceptedMessageIds.get(messageId);
1362
1370
  let contextEnhanced = false;
@@ -1408,6 +1416,7 @@ export class WecomHarnessBridge {
1408
1416
  }),
1409
1417
  onInteractionResolved: (resolution) => this.#handleInteractionResolved(resolution),
1410
1418
  files: message.files,
1419
+ images: message.images,
1411
1420
  },
1412
1421
  });
1413
1422
  if (batchSubmission) {
@@ -1,3 +1,4 @@
1
+ import { extractConnectionEvidence, atConnectionStage, createConnectionDiagnostics } from '../shared/connection-error.mjs';
1
2
  import { randomUUID } from 'node:crypto';
2
3
 
3
4
  import { deriveWecomBotIdentity, maskWecomBotId } from './config-store.mjs';
@@ -40,6 +41,7 @@ export class WecomController {
40
41
  #createRuntime;
41
42
  #deleteState;
42
43
  #logger;
44
+ #diagnostics;
43
45
  #runtimes = new Map();
44
46
  #errors = new Map();
45
47
  #attempts = new Map();
@@ -74,6 +76,14 @@ export class WecomController {
74
76
  this.#createRuntime = createRuntime;
75
77
  this.#deleteState = deleteState;
76
78
  this.#logger = logger;
79
+ this.#diagnostics = createConnectionDiagnostics({ channel: 'wecom', logger });
80
+ }
81
+
82
+ get diagnostics() { return this.#diagnostics; }
83
+
84
+ #failure(error, code, message) {
85
+ const stage = code.startsWith('qr-') ? 'qr.begin' : code === 'activation-failed' ? 'activation' : 'connection.start';
86
+ return this.#diagnostics.report(error, { reuse: true, stage, publicError: { code, message } }).publicError;
77
87
  }
78
88
 
79
89
  async initialize() {
@@ -82,16 +92,16 @@ export class WecomController {
82
92
  await this.#withBotTransition(config.botId, async () => {
83
93
  const existing = this.#runtimes.get(config.botId)?.status;
84
94
  if (this.#closed || existing?.ready || existing?.wecomConnectionState === 'connecting') return;
85
- const secret = await this.#resolveSecret(config.secretRef);
86
- if (!secret) {
87
- this.#errors.set(config.botId, safeError('missing-secret', t('企业微信机器人凭据缺失,请移除后重新扫码。')));
88
- return;
89
- }
90
95
  try {
96
+ const secret = await this.#resolveSecret(config.secretRef);
97
+ if (!secret) {
98
+ this.#errors.set(config.botId, safeError('missing-secret', t('企业微信机器人凭据缺失,请移除后重新扫码。')));
99
+ return;
100
+ }
91
101
  await this.#startRuntime(config, secret);
92
102
  this.#errors.delete(config.botId);
93
103
  } catch (error) {
94
- this.#errors.set(config.botId, safeError('connection-failed', t('企业微信连接未就绪,插件会自动重试。')));
104
+ this.#errors.set(config.botId, this.#failure(error, 'connection-failed', t('企业微信连接未就绪,插件会自动重试。')));
95
105
  this.#logger.warn?.(`[dsh-im:wecom] bot ${config.botId} failed to initialize`);
96
106
  } finally {
97
107
  this.#touch();
@@ -137,7 +147,7 @@ export class WecomController {
137
147
  record.state = record.controller.signal.aborted ? 'cancelled' : 'failed';
138
148
  record.error = record.controller.signal.aborted
139
149
  ? safeError('cancelled', t('扫码绑定已取消。'))
140
- : safeError('qr-start-failed', t('无法生成企业微信二维码,请稍后重试。'));
150
+ : this.#failure(error, 'qr-start-failed', t('无法生成企业微信二维码,请稍后重试。'));
141
151
  this.#finishAttempt(record);
142
152
  throw error;
143
153
  }
@@ -180,7 +190,7 @@ export class WecomController {
180
190
  await this.#withBotTransition(identity.botId, async () => {
181
191
  if (this.#closed) throw new Error('Enterprise WeChat controller is closed');
182
192
  const previousConfig = this.#configStore.getByRemoteBotId(remoteBotId);
183
- const previousSecret = await this.#credentials.resolve(identity.secretRef).catch(() => undefined);
193
+ const previousSecret = await atConnectionStage('credential.read', () => this.#credentials.resolve(identity.secretRef), 'credential-store');
184
194
  if (this.#closed) throw new Error('Enterprise WeChat controller is closed');
185
195
  const config = {
186
196
  botId: identity.botId,
@@ -189,9 +199,9 @@ export class WecomController {
189
199
  createdAt: previousConfig?.createdAt ?? new Date().toISOString(),
190
200
  connectedAt: new Date().toISOString(),
191
201
  };
192
- await this.#credentials.set(identity.secretRef, normalizedSecret);
202
+ await atConnectionStage('credential.save', () => this.#credentials.set(identity.secretRef, normalizedSecret), 'credential-store');
193
203
  try {
194
- await this.#configStore.save(config);
204
+ await atConnectionStage('account.save', () => this.#configStore.save(config), 'account-config');
195
205
  } catch (error) {
196
206
  await this.#restoreCredential(identity.secretRef, previousSecret);
197
207
  throw error;
@@ -234,7 +244,7 @@ export class WecomController {
234
244
  await this.#startRuntime(config, secret);
235
245
  this.#errors.delete(botId);
236
246
  } catch (error) {
237
- this.#errors.set(botId, safeError('connection-failed', t('企业微信连接仍未就绪,请稍后重试。')));
247
+ this.#errors.set(botId, this.#failure(error, 'connection-failed', t('企业微信连接仍未就绪,请稍后重试。')));
238
248
  throw error;
239
249
  } finally {
240
250
  this.#touch();
@@ -272,28 +282,32 @@ export class WecomController {
272
282
  }
273
283
 
274
284
  async deleteBot(botId) {
285
+ const warnings = [];
275
286
  const config = this.#configStore.get(botId);
276
287
  if (!config) throw new Error('Unknown Enterprise WeChat bot');
277
288
  await this.#withBotTransition(botId, async () => {
278
- const previous = await this.#credentials.resolve(config.secretRef).catch(() => undefined);
289
+ const previous = await atConnectionStage('credential.read', () => this.#credentials.resolve(config.secretRef), 'credential-store');
279
290
  await this.#stopRuntime(botId);
280
291
  try {
281
- await this.#credentials.unset(config.secretRef);
282
- await this.#configStore.remove(botId);
292
+ await atConnectionStage('credential.remove', () => this.#credentials.unset(config.secretRef), 'credential-store');
293
+ await atConnectionStage('account.remove', () => this.#configStore.remove(botId), 'account-config');
283
294
  } catch (error) {
284
- if (previous?.value) {
285
- await this.#credentials.set(config.secretRef, previous.value).catch(() => undefined);
286
- await this.#startRuntime(config, previous.value).catch(() => undefined);
295
+ if (!this.#configStore.get(botId)) {
296
+ warnings.push(this.#diagnostics.report(error, { operation: 'bot.delete', stage: 'workspace.cleanup', warning: true,
297
+ publicError: { code: 'workspace-cleanup-failed', message: '账号已移除,但本地状态清理失败。' } }).publicError);
298
+ } else {
299
+ if (previous?.value) {
300
+ await atConnectionStage('credential.save', () => this.#credentials.set(config.secretRef, previous.value), 'credential-store').catch(() => undefined);
301
+ await this.#startRuntime(config, previous.value).catch(() => undefined);
302
+ }
303
+ throw new Error('Unable to remove the Enterprise WeChat bot safely.', { cause: error });
287
304
  }
288
- throw new Error('Unable to remove the Enterprise WeChat bot safely.', { cause: error });
289
305
  }
290
- await this.#deleteState({ botId, config }).catch((error) => {
291
- this.#logger.warn?.(`[dsh-im:wecom] bot ${botId} state cleanup failed:`, error);
292
- });
306
+ await this.#deleteState({ botId, config }).catch(error => { warnings.push(this.#diagnostics.report(error, { reuse: true, operation: 'bot.delete', stage: 'state.cleanup', resource: 'account-state', warning: true, publicError: { code: 'cleanup-failed', message: '账号已移除,但本地状态清理失败。' } }).publicError); });
293
307
  this.#errors.delete(botId);
294
308
  this.#touch();
295
309
  });
296
- return this.status();
310
+ return { ...this.status(), ...(warnings.length ? { warnings } : {}) };
297
311
  }
298
312
 
299
313
  status() {
@@ -324,7 +338,7 @@ export class WecomController {
324
338
  messagesReplied: runtimeStatus?.messagesReplied ?? 0,
325
339
  },
326
340
  lastMessageError: publicMessageFailure(runtimeStatus?.lastMessageError),
327
- error: structuredClone(this.#errors.get(config.botId) ?? null),
341
+ error: structuredClone(runtimeStatus?.error ?? this.#errors.get(config.botId) ?? null),
328
342
  };
329
343
  });
330
344
  const connectedCount = bots.filter((bot) => bot.connected).length;
@@ -379,7 +393,7 @@ export class WecomController {
379
393
  } catch (error) {
380
394
  if (record.controller.signal.aborted) return;
381
395
  record.state = 'failed';
382
- record.error = safeError('qr-connect-failed', t('企业微信扫码服务暂时不可用,请重新生成二维码。'));
396
+ record.error = this.#failure(error, 'qr-connect-failed', t('企业微信扫码服务暂时不可用,请重新生成二维码。'));
383
397
  this.#logger.warn?.('[dsh-im:wecom] QR polling failed');
384
398
  this.#finishAttempt(record);
385
399
  }
@@ -399,7 +413,7 @@ export class WecomController {
399
413
  record.error = safeError('cancelled', t('扫码绑定已取消。'));
400
414
  } else {
401
415
  record.state = 'failed';
402
- record.error = safeError('activation-failed', t('企业微信已授权,但无法安全保存接入配置。'));
416
+ record.error = this.#failure(error, 'activation-failed', t('企业微信已授权,但无法安全保存接入配置。'));
403
417
  this.#logger.error?.('[dsh-im:wecom] provisioning failed');
404
418
  }
405
419
  } finally {
@@ -410,7 +424,7 @@ export class WecomController {
410
424
  async #activateBot(record, { remoteBotId, secret }) {
411
425
  const identity = deriveWecomBotIdentity(remoteBotId);
412
426
  const previousConfig = this.#configStore.getByRemoteBotId(remoteBotId);
413
- const previousSecret = await this.#credentials.resolve(identity.secretRef).catch(() => undefined);
427
+ const previousSecret = await atConnectionStage('credential.read', () => this.#credentials.resolve(identity.secretRef), 'credential-store');
414
428
  const config = {
415
429
  botId: identity.botId,
416
430
  remoteBotId,
@@ -419,10 +433,10 @@ export class WecomController {
419
433
  connectedAt: new Date().toISOString(),
420
434
  };
421
435
  return this.#withBotTransition(identity.botId, async () => {
422
- await this.#credentials.set(identity.secretRef, secret);
436
+ await atConnectionStage('credential.save', () => this.#credentials.set(identity.secretRef, secret), 'credential-store');
423
437
  try {
424
438
  if (record.controller.signal.aborted) throw new DOMException('Cancelled', 'AbortError');
425
- await this.#configStore.save(config);
439
+ await atConnectionStage('account.save', () => this.#configStore.save(config), 'account-config');
426
440
  } catch (error) {
427
441
  await this.#restoreCredential(identity.secretRef, previousSecret);
428
442
  throw error;
@@ -434,19 +448,19 @@ export class WecomController {
434
448
  } catch (error) {
435
449
  if (record.controller.signal.aborted) {
436
450
  await this.#stopRuntime(identity.botId);
437
- if (previousConfig) await this.#configStore.save(previousConfig).catch(() => undefined);
451
+ if (previousConfig) await atConnectionStage('account.save', () => this.#configStore.save(previousConfig), 'account-config').catch(() => undefined);
438
452
  else {
439
- const removed = await this.#configStore.remove(identity.botId).catch(() => null);
453
+ const removed = await atConnectionStage('account.remove', () => this.#configStore.remove(identity.botId), 'account-config').catch(() => null);
440
454
  if (removed) {
441
455
  await this.#deleteState({ botId: identity.botId, config }).catch((cleanupError) => {
442
- this.#logger.warn?.('[dsh-im:wecom] cancelled bot state cleanup failed:', cleanupError);
456
+ this.#logger.warn?.('[dsh-im:wecom] cancelled bot state cleanup failed:', extractConnectionEvidence(cleanupError).details);
443
457
  });
444
458
  }
445
459
  }
446
460
  await this.#restoreCredential(identity.secretRef, previousSecret);
447
461
  throw error;
448
462
  }
449
- this.#errors.set(identity.botId, safeError('connection-failed', t('企业微信机器人已绑定,消息连接暂未就绪。')));
463
+ this.#errors.set(identity.botId, this.#failure(error, 'connection-failed', t('企业微信机器人已绑定,消息连接暂未就绪。')));
450
464
  this.#logger.warn?.(`[dsh-im:wecom] bot ${identity.botId} activation connection failed`);
451
465
  }
452
466
  this.#touch();
@@ -458,7 +472,7 @@ export class WecomController {
458
472
  if (this.#closed) throw new Error('Enterprise WeChat controller is closed');
459
473
  await this.#stopRuntime(config.botId);
460
474
  if (this.#closed) throw new Error('Enterprise WeChat controller is closed');
461
- const runtime = await this.#createRuntime({ botId: config.botId, config, secret });
475
+ const runtime = await atConnectionStage('runtime.prepare', () => this.#createRuntime({ botId: config.botId, config, secret }));
462
476
  if (!runtime || typeof runtime.start !== 'function' || typeof runtime.stop !== 'function') {
463
477
  throw new TypeError('createRuntime returned an invalid Enterprise WeChat runtime');
464
478
  }
@@ -476,18 +490,18 @@ export class WecomController {
476
490
  const runtime = this.#runtimes.get(botId);
477
491
  this.#runtimes.delete(botId);
478
492
  await runtime?.stop().catch((error) => {
479
- this.#logger.warn?.(`[dsh-im:wecom] bot ${botId} failed to stop cleanly:`, error);
493
+ this.#logger.warn?.(`[dsh-im:wecom] bot ${botId} failed to stop cleanly:`, extractConnectionEvidence(error).details);
480
494
  });
481
495
  }
482
496
 
483
497
  async #resolveSecret(ref) {
484
- const result = await this.#credentials.resolve(ref).catch(() => undefined);
498
+ const result = await atConnectionStage('credential.read', () => this.#credentials.resolve(ref), 'credential-store');
485
499
  return cleanString(result?.value);
486
500
  }
487
501
 
488
502
  async #restoreCredential(ref, previous) {
489
- if (previous?.value) await this.#credentials.set(ref, previous.value).catch(() => undefined);
490
- else await this.#credentials.unset(ref).catch(() => undefined);
503
+ if (previous?.value) await atConnectionStage('credential.save', () => this.#credentials.set(ref, previous.value), 'credential-store').catch(() => undefined);
504
+ else await atConnectionStage('credential.remove', () => this.#credentials.unset(ref), 'credential-store').catch(() => undefined);
491
505
  }
492
506
 
493
507
  #withBotTransition(botId, operation) {
@@ -1,3 +1,4 @@
1
+ import { createConnectionDiagnostics, atConnectionStage } from '../shared/connection-error.mjs';
1
2
  import { WSAuthFailureError, WSClient, WSReconnectExhaustedError } from '@wecom/aibot-node-sdk';
2
3
 
3
4
  import { createWecomBridgeStatus, WecomHarnessBridge } from './wecom-bridge.mjs';
@@ -31,6 +32,7 @@ export class WecomRuntime {
31
32
  #contextEnhancement;
32
33
  #accessPolicy;
33
34
  #logger;
35
+ #diagnostics;
34
36
  #replyTimeoutMs;
35
37
  #connectTimeoutMs;
36
38
  #maxReconnectAttempts;
@@ -64,7 +66,7 @@ export class WecomRuntime {
64
66
  this.#state = state;
65
67
  this.#contextEnhancement = contextEnhancement;
66
68
  this.#accessPolicy = accessPolicy;
67
- this.#logger = logger;
69
+ this.#logger = logger; this.#diagnostics = createConnectionDiagnostics({ channel: 'wecom', logger });
68
70
  this.#replyTimeoutMs = replyTimeoutMs;
69
71
  this.#connectTimeoutMs = connectTimeoutMs;
70
72
  this.#maxReconnectAttempts = maxReconnectAttempts;
@@ -94,8 +96,8 @@ export class WecomRuntime {
94
96
  signal.throwIfAborted();
95
97
  this.#status.startedAt = new Date().toISOString();
96
98
  this.#status.wecomConnectionState = 'connecting';
97
- this.#status.lastError = null;
98
- await this.#harness.ensureRunning();
99
+ this.#status.lastError = null; this.#status.error = null; this.#diagnostics.clear();
100
+ await atConnectionStage('harness.check', () => this.#harness.ensureRunning());
99
101
  this.#status.harnessReachable = true;
100
102
 
101
103
  const silentSdkLogger = { debug() {}, info() {}, warn() {}, error() {} };
@@ -136,7 +138,7 @@ export class WecomRuntime {
136
138
  this.#status.wecomConnectionState = 'connected';
137
139
  this.#status.lastCheckedAt = now;
138
140
  this.#status.lastConnectedAt = now;
139
- this.#status.lastError = null;
141
+ this.#status.lastError = null; this.#status.error = null; this.#diagnostics.clear();
140
142
  readyResolve();
141
143
  };
142
144
  const onDisconnected = () => {
@@ -159,7 +161,8 @@ export class WecomRuntime {
159
161
  this.#status.ready = false;
160
162
  this.#status.wecomConnectionState = 'failed';
161
163
  }
162
- this.#status.lastError = terminal ? error.name : 'connection-error';
164
+ this.#status.error = this.#diagnostics.report(error, { operation: 'connection.monitor', botId: this.#config?.botId, automatic: true }).publicError;
165
+ this.#status.lastError = this.#status.error.message;
163
166
  this.#logger.warn?.(`[dsh-im:wecom] bot ${this.#config.botId} connection error`);
164
167
  };
165
168
  const onMessage = (frame) => this.#bridge?.accept(frame);
@@ -191,7 +194,8 @@ export class WecomRuntime {
191
194
  }
192
195
  this.#status.ready = false;
193
196
  this.#status.wecomConnectionState = 'failed';
194
- this.#status.lastError = error?.message ?? String(error);
197
+ this.#status.error = this.#diagnostics.report(error, { operation: 'connection.monitor', botId: this.#config?.botId, automatic: true }).publicError;
198
+ this.#status.lastError = this.#status.error.message;
195
199
  await this.#stopActive();
196
200
  throw error;
197
201
  } finally {
@@ -52,6 +52,7 @@ import {
52
52
  promptContentForInboundMessage,
53
53
  } from '../shared/semantic/reply-reference.mjs';
54
54
  import {
55
+ messageFailureDiagnostic,
55
56
  channelDeliveryFailure,
56
57
  clearLastMessageFailure,
57
58
  messageFailureText,
@@ -325,7 +326,7 @@ export class WecomAppBridge {
325
326
  const failure = setLastMessageFailure(this.#status, error);
326
327
  this.#logger.error?.(
327
328
  `[dsh-im:wecom-app] failed to process a command [${failure.referenceId}]:`,
328
- error,
329
+ messageFailureDiagnostic(error, failure),
329
330
  );
330
331
  return this.#send(sender, messageFailureText(failure)).catch(() => undefined);
331
332
  })
@@ -426,7 +427,7 @@ export class WecomAppBridge {
426
427
  const failure = setLastMessageFailure(this.#status, error);
427
428
  this.#logger.error?.(
428
429
  `[dsh-im:wecom-app] failed to process a batch input message [${failure.referenceId}]:`,
429
- error,
430
+ messageFailureDiagnostic(error, failure),
430
431
  );
431
432
  await this.#send(sender, messageFailureText(failure)).catch(() => undefined);
432
433
  }).finally(() => {
@@ -592,7 +593,7 @@ export class WecomAppBridge {
592
593
  let artifacts = [];
593
594
  try {
594
595
  let content = hasImages || hasReply
595
- ? await promptContentForInboundMessage(promptMessage, { signal: this.#signal })
596
+ ? await promptContentForInboundMessage(promptMessage, { signal: this.#signal, deferImages: true })
596
597
  : undefined;
597
598
  const snapshot = this.#acceptedMessageIds.get(messageId);
598
599
  let contextEnhanced = false;
@@ -638,6 +639,7 @@ export class WecomAppBridge {
638
639
  await this.#handleInteractionResolved(resolution);
639
640
  },
640
641
  files: promptMessage.files,
642
+ images: promptMessage.images,
641
643
  },
642
644
  }));
643
645
  if (batchSubmission) {
@@ -720,7 +722,7 @@ export class WecomAppBridge {
720
722
  });
721
723
  this.#logger.error?.(
722
724
  `[dsh-im:wecom-app] failed to process an inbound message [${failure.referenceId}]:`,
723
- error,
725
+ messageFailureDiagnostic(error, failure),
724
726
  );
725
727
  try {
726
728
  await this.#send(
@@ -1006,7 +1008,7 @@ export class WecomAppBridge {
1006
1008
  const failure = setLastMessageFailure(this.#status, error);
1007
1009
  this.#logger.error?.(
1008
1010
  `[dsh-im:wecom-app] failed to process an interaction reply [${failure.referenceId}]:`,
1009
- error,
1011
+ messageFailureDiagnostic(error, failure),
1010
1012
  );
1011
1013
  if (!this.#state.hasSeen(messageId)) {
1012
1014
  await this.#state.markSeen(messageId).catch(() => undefined);