@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
@@ -1,3 +1,4 @@
1
+ import { extractConnectionEvidence, atConnectionStage, createConnectionDiagnostics } from '../shared/connection-error.mjs';
1
2
  import { connectionTestMessage } from '../shared/connection-test.mjs';
2
3
  import { publicMessageFailure } from '../shared/message-failure.mjs';
3
4
  import { t } from '../shared/i18n.mjs';
@@ -20,6 +21,7 @@ export class SlackController {
20
21
  #createRuntime;
21
22
  #deleteState;
22
23
  #logger;
24
+ #diagnostics;
23
25
  #runtimes = new Map();
24
26
  #errors = new Map();
25
27
  #transitions = new Map();
@@ -51,6 +53,14 @@ export class SlackController {
51
53
  this.#createRuntime = createRuntime;
52
54
  this.#deleteState = deleteState;
53
55
  this.#logger = logger;
56
+ this.#diagnostics = createConnectionDiagnostics({ channel: 'slack', logger });
57
+ }
58
+
59
+ get diagnostics() { return this.#diagnostics; }
60
+
61
+ #failure(error, code, message) {
62
+ const stage = code.startsWith('qr-') ? 'qr.begin' : code === 'activation-failed' ? 'activation' : 'connection.start';
63
+ return this.#diagnostics.report(error, { reuse: true, stage, publicError: { code, message } }).publicError;
54
64
  }
55
65
 
56
66
  async initialize() {
@@ -58,26 +68,23 @@ export class SlackController {
58
68
  for (const config of this.#configStore.list()) {
59
69
  await this.#withBotTransition(config.botId, async () => {
60
70
  if (this.#closed || this.#runtimes.get(config.botId)?.status?.ready) return;
61
- const resolved = await this.#resolveCredentials(config);
62
- if (!resolved) {
63
- this.#errors.set(config.botId, safeError(
64
- 'missing-token',
65
- t('Slack机器人凭据缺失,请移除后重新接入。'),
66
- ));
67
- return;
68
- }
69
71
  try {
72
+ const resolved = await this.#resolveCredentials(config);
73
+ if (!resolved) {
74
+ this.#errors.set(config.botId, safeError(
75
+ 'missing-token',
76
+ t('Slack机器人凭据缺失,请移除后重新接入。'),
77
+ ));
78
+ return;
79
+ }
70
80
  await this.#startRuntime(config, resolved);
71
81
  this.#errors.delete(config.botId);
72
82
  } catch (error) {
73
- this.#errors.set(config.botId, safeError(
83
+ this.#errors.set(config.botId, this.#failure(error,
74
84
  'connection-failed',
75
85
  t('Slack Socket Mode 连接未就绪,插件会自动重试。'),
76
86
  ));
77
- this.#logger.warn?.(
78
- `[dsh-im:slack] bot ${config.botId} failed to initialize:`,
79
- error,
80
- );
87
+
81
88
  } finally {
82
89
  this.#touch();
83
90
  }
@@ -105,8 +112,8 @@ export class SlackController {
105
112
  await this.#withBotTransition(identity.botId, async () => {
106
113
  if (this.#closed) throw new Error('Slack controller is closed');
107
114
  const previousConfig = this.#configStore.getByPlatformId(platformId);
108
- const previousBotToken = await this.#credentials.resolve(identity.botTokenRef).catch(() => undefined);
109
- const previousAppToken = await this.#credentials.resolve(identity.appTokenRef).catch(() => undefined);
115
+ const previousBotToken = await atConnectionStage('credential.read', () => this.#credentials.resolve(identity.botTokenRef), 'credential-store');
116
+ const previousAppToken = await atConnectionStage('credential.read', () => this.#credentials.resolve(identity.appTokenRef), 'credential-store');
110
117
  const config = {
111
118
  ...identity,
112
119
  platformId,
@@ -118,9 +125,9 @@ export class SlackController {
118
125
  connectedAt: new Date().toISOString(),
119
126
  };
120
127
  try {
121
- await this.#credentials.set(identity.botTokenRef, normalizedBotToken);
122
- await this.#credentials.set(identity.appTokenRef, normalizedAppToken);
123
- await this.#configStore.save(config);
128
+ await atConnectionStage('credential.save', () => this.#credentials.set(identity.botTokenRef, normalizedBotToken), 'credential-store');
129
+ await atConnectionStage('credential.save', () => this.#credentials.set(identity.appTokenRef, normalizedAppToken), 'credential-store');
130
+ await atConnectionStage('account.save', () => this.#configStore.save(config), 'account-config');
124
131
  } catch (error) {
125
132
  await Promise.all([
126
133
  this.#restoreCredential(identity.botTokenRef, previousBotToken),
@@ -135,14 +142,11 @@ export class SlackController {
135
142
  });
136
143
  this.#errors.delete(identity.botId);
137
144
  } catch (error) {
138
- this.#errors.set(identity.botId, safeError(
145
+ this.#errors.set(identity.botId, this.#failure(error,
139
146
  'connection-failed',
140
147
  t('Slack机器人已接入,Socket Mode 连接暂未就绪。'),
141
148
  ));
142
- this.#logger.warn?.(
143
- `[dsh-im:slack] bot ${identity.botId} credential connection failed:`,
144
- error,
145
- );
149
+
146
150
  }
147
151
  this.#touch();
148
152
  });
@@ -159,7 +163,7 @@ export class SlackController {
159
163
  await this.#startRuntime(config, resolved);
160
164
  this.#errors.delete(botId);
161
165
  } catch (error) {
162
- this.#errors.set(botId, safeError(
166
+ this.#errors.set(botId, this.#failure(error,
163
167
  'connection-failed',
164
168
  t('Slack Socket Mode 连接仍未就绪,请检查两个 Token。'),
165
169
  ));
@@ -204,36 +208,40 @@ export class SlackController {
204
208
  }
205
209
 
206
210
  async deleteBot(botId) {
211
+ const warnings = [];
207
212
  const config = this.#configStore.get(botId);
208
213
  if (!config) throw new Error('Unknown Slack bot');
209
214
  await this.#withBotTransition(botId, async () => {
210
- const previousBotToken = await this.#credentials.resolve(config.botTokenRef).catch(() => undefined);
211
- const previousAppToken = await this.#credentials.resolve(config.appTokenRef).catch(() => undefined);
215
+ const previousBotToken = await atConnectionStage('credential.read', () => this.#credentials.resolve(config.botTokenRef), 'credential-store');
216
+ const previousAppToken = await atConnectionStage('credential.read', () => this.#credentials.resolve(config.appTokenRef), 'credential-store');
212
217
  await this.#stopRuntime(botId);
213
218
  try {
214
- await this.#credentials.unset(config.botTokenRef);
215
- await this.#credentials.unset(config.appTokenRef);
216
- await this.#configStore.remove(botId);
219
+ await atConnectionStage('credential.remove', () => this.#credentials.unset(config.botTokenRef), 'credential-store');
220
+ await atConnectionStage('credential.remove', () => this.#credentials.unset(config.appTokenRef), 'credential-store');
221
+ await atConnectionStage('account.remove', () => this.#configStore.remove(botId), 'account-config');
217
222
  } catch (error) {
218
- await Promise.all([
219
- this.#restoreCredential(config.botTokenRef, previousBotToken),
220
- this.#restoreCredential(config.appTokenRef, previousAppToken),
221
- ]);
222
- if (previousBotToken?.value && previousAppToken?.value) {
223
- await this.#startRuntime(config, {
224
- botToken: previousBotToken.value,
225
- appToken: previousAppToken.value,
226
- }).catch(() => undefined);
223
+ if (!this.#configStore.get(botId)) {
224
+ warnings.push(this.#diagnostics.report(error, { operation: 'bot.delete', stage: 'workspace.cleanup', warning: true,
225
+ publicError: { code: 'workspace-cleanup-failed', message: '账号已移除,但本地状态清理失败。' } }).publicError);
226
+ } else {
227
+ await Promise.all([
228
+ this.#restoreCredential(config.botTokenRef, previousBotToken),
229
+ this.#restoreCredential(config.appTokenRef, previousAppToken),
230
+ ]);
231
+ if (previousBotToken?.value && previousAppToken?.value) {
232
+ await this.#startRuntime(config, {
233
+ botToken: previousBotToken.value,
234
+ appToken: previousAppToken.value,
235
+ }).catch(() => undefined);
236
+ }
237
+ throw new Error('Unable to remove the Slack bot safely.', { cause: error });
227
238
  }
228
- throw new Error('Unable to remove the Slack bot safely.', { cause: error });
229
239
  }
230
- await this.#deleteState({ botId, config }).catch((error) => {
231
- this.#logger.warn?.(`[dsh-im:slack] bot ${botId} state cleanup failed:`, error);
232
- });
240
+ 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); });
233
241
  this.#errors.delete(botId);
234
242
  this.#touch();
235
243
  });
236
- return this.status();
244
+ return { ...this.status(), ...(warnings.length ? { warnings } : {}) };
237
245
  }
238
246
 
239
247
  status() {
@@ -270,7 +278,7 @@ export class SlackController {
270
278
  messagesReplied: runtimeStatus?.messagesReplied ?? 0,
271
279
  },
272
280
  lastMessageError: publicMessageFailure(runtimeStatus?.lastMessageError),
273
- error: structuredClone(this.#errors.get(config.botId) ?? null),
281
+ error: structuredClone(runtimeStatus?.error ?? this.#errors.get(config.botId) ?? null),
274
282
  };
275
283
  });
276
284
  const connected = bots.filter((bot) => bot.connected).length;
@@ -296,12 +304,12 @@ export class SlackController {
296
304
  if (this.#closed) throw new Error('Slack controller is closed');
297
305
  await this.#stopRuntime(config.botId);
298
306
  if (this.#closed) throw new Error('Slack controller is closed');
299
- const runtime = await this.#createRuntime({
307
+ const runtime = await atConnectionStage('runtime.prepare', () => this.#createRuntime({
300
308
  botId: config.botId,
301
309
  config,
302
310
  botToken,
303
311
  appToken,
304
- });
312
+ }));
305
313
  if (!runtime || typeof runtime.start !== 'function' || typeof runtime.stop !== 'function') {
306
314
  throw new TypeError('createRuntime returned an invalid Slack runtime');
307
315
  }
@@ -319,14 +327,14 @@ export class SlackController {
319
327
  const runtime = this.#runtimes.get(botId);
320
328
  this.#runtimes.delete(botId);
321
329
  await runtime?.stop().catch((error) => {
322
- this.#logger.warn?.(`[dsh-im:slack] bot ${botId} failed to stop cleanly:`, error);
330
+ this.#logger.warn?.(`[dsh-im:slack] bot ${botId} failed to stop cleanly:`, extractConnectionEvidence(error).details);
323
331
  });
324
332
  }
325
333
 
326
334
  async #resolveCredentials(config) {
327
335
  const [bot, app] = await Promise.all([
328
- this.#credentials.resolve(config.botTokenRef).catch(() => undefined),
329
- this.#credentials.resolve(config.appTokenRef).catch(() => undefined),
336
+ atConnectionStage('credential.read', () => this.#credentials.resolve(config.botTokenRef), 'credential-store'),
337
+ atConnectionStage('credential.read', () => this.#credentials.resolve(config.appTokenRef), 'credential-store'),
330
338
  ]);
331
339
  const botToken = cleanString(bot?.value);
332
340
  const appToken = cleanString(app?.value);
@@ -334,8 +342,8 @@ export class SlackController {
334
342
  }
335
343
 
336
344
  async #restoreCredential(ref, previous) {
337
- if (previous?.value) await this.#credentials.set(ref, previous.value).catch(() => undefined);
338
- else await this.#credentials.unset(ref).catch(() => undefined);
345
+ if (previous?.value) await atConnectionStage('credential.save', () => this.#credentials.set(ref, previous.value), 'credential-store').catch(() => undefined);
346
+ else await atConnectionStage('credential.remove', () => this.#credentials.unset(ref), 'credential-store').catch(() => undefined);
339
347
  }
340
348
 
341
349
  #withBotTransition(botId, operation) {
@@ -1,3 +1,4 @@
1
+ import { extractConnectionEvidence, createConnectionDiagnostics, atConnectionStage } from '../shared/connection-error.mjs';
1
2
  import { splitMessageText } from '../shared/editable-message-stream.mjs';
2
3
  import { t } from '../shared/i18n.mjs';
3
4
  import { SlackApi } from './slack-api.mjs';
@@ -410,6 +411,7 @@ export class SlackRuntime {
410
411
  #contextEnhancement;
411
412
  #accessPolicy;
412
413
  #logger;
414
+ #diagnostics;
413
415
  #replyTimeoutMs;
414
416
  #connectTimeoutMs;
415
417
  #createApi;
@@ -451,7 +453,7 @@ export class SlackRuntime {
451
453
  this.#state = state;
452
454
  this.#contextEnhancement = contextEnhancement;
453
455
  this.#accessPolicy = accessPolicy;
454
- this.#logger = logger;
456
+ this.#logger = logger; this.#diagnostics = createConnectionDiagnostics({ channel: 'slack', logger });
455
457
  this.#replyTimeoutMs = replyTimeoutMs;
456
458
  this.#connectTimeoutMs = connectTimeoutMs;
457
459
  this.#createApi = createApi;
@@ -510,8 +512,8 @@ export class SlackRuntime {
510
512
  this.#reconnectAttempt = 0;
511
513
  this.#status.startedAt = new Date().toISOString();
512
514
  this.#status.connectionState = 'connecting';
513
- this.#status.lastError = null;
514
- await this.#harness.ensureRunning();
515
+ this.#status.lastError = null; this.#status.error = null; this.#diagnostics.clear();
516
+ await atConnectionStage('harness.check', () => this.#harness.ensureRunning());
515
517
  this.#status.harnessReachable = true;
516
518
  const controller = new AbortController();
517
519
  this.#abortController = controller;
@@ -540,7 +542,7 @@ export class SlackRuntime {
540
542
  this.#connect(),
541
543
  new Promise((_, reject) => {
542
544
  timer = setTimeout(
543
- () => reject(new Error('Slack Socket Mode did not become ready in time')),
545
+ () => reject(Object.assign(new Error('Slack Socket Mode did not become ready in time'), { code: 'ETIMEDOUT', timeoutMs: this.#connectTimeoutMs })),
544
546
  this.#connectTimeoutMs,
545
547
  );
546
548
  timer?.unref?.();
@@ -553,7 +555,8 @@ export class SlackRuntime {
553
555
  } catch (error) {
554
556
  this.#status.ready = false;
555
557
  this.#status.connectionState = 'failed';
556
- this.#status.lastError = error?.message ?? String(error);
558
+ this.#status.error = this.#diagnostics.report(error, { operation: 'connection.restore', reuse: true, botId: this.#config?.botId, automatic: true }).publicError;
559
+ this.#status.lastError = this.#status.error.message;
557
560
  await this.stop();
558
561
  throw error;
559
562
  }
@@ -582,7 +585,7 @@ export class SlackRuntime {
582
585
  this.#status.connectionState = 'connected';
583
586
  this.#status.lastCheckedAt = now;
584
587
  this.#status.lastConnectedAt = now;
585
- this.#status.lastError = null;
588
+ this.#status.lastError = null; this.#status.error = null; this.#diagnostics.clear();
586
589
  resolve();
587
590
  };
588
591
 
@@ -624,7 +627,7 @@ export class SlackRuntime {
624
627
  if (generation !== this.#generation || this.#stopped) return;
625
628
  this.#logger.error?.(
626
629
  `[dsh-im:slack] bot ${this.#config.botId} message handling failed:`,
627
- error,
630
+ extractConnectionEvidence(error).details,
628
631
  );
629
632
  });
630
633
  }
@@ -641,7 +644,8 @@ export class SlackRuntime {
641
644
  const error = new Error(`Slack Socket Mode closed (${code || 'unknown'})`);
642
645
  this.#status.ready = false;
643
646
  this.#status.connectionState = 'connecting';
644
- this.#status.lastError = error.message;
647
+ this.#status.error = this.#diagnostics.report(error, { operation: 'connection.monitor', botId: this.#config?.botId, automatic: true }).publicError;
648
+ this.#status.lastError = this.#status.error.message;
645
649
  if (!settled) {
646
650
  settled = true;
647
651
  reject(error);
@@ -649,9 +653,11 @@ export class SlackRuntime {
649
653
  this.#scheduleReconnect();
650
654
  });
651
655
 
652
- addSocketListener(socket, 'error', () => {
656
+ addSocketListener(socket, 'error', (event) => {
657
+ const error = event?.error ?? new Error('WebSocket error');
653
658
  if (generation !== this.#generation || this.#stopped) return;
654
- this.#status.lastError = 'Slack Socket Mode WebSocket error';
659
+ this.#status.error = this.#diagnostics.report(error, { operation: 'connection.monitor', botId: this.#config?.botId, automatic: true }).publicError;
660
+ this.#status.lastError = this.#status.error.message;
655
661
  });
656
662
  });
657
663
  }
@@ -664,7 +670,7 @@ export class SlackRuntime {
664
670
  this.#reconnectTimer = null;
665
671
  void this.#connect().catch((error) => {
666
672
  if (this.#stopped) return;
667
- this.#logger.warn?.('[dsh-im:slack] Socket Mode reconnect failed:', error);
673
+ this.#logger.warn?.('[dsh-im:slack] Socket Mode reconnect failed:', extractConnectionEvidence(error).details);
668
674
  this.#scheduleReconnect();
669
675
  });
670
676
  }, delay);
@@ -687,7 +693,7 @@ export class SlackRuntime {
687
693
  try {
688
694
  if (socket && socket.readyState < 2) socket.close(1000, 'Plugin stopped');
689
695
  } catch (error) {
690
- this.#logger.warn?.(`[dsh-im:slack] bot ${this.#config.botId} failed to close Socket Mode:`, error);
696
+ this.#logger.warn?.(`[dsh-im:slack] bot ${this.#config.botId} failed to close Socket Mode:`, extractConnectionEvidence(error).details);
691
697
  }
692
698
  await Promise.race([
693
699
  bridge?.waitForIdle() ?? Promise.resolve(),
@@ -450,8 +450,8 @@ export class TelegramApi {
450
450
  let body;
451
451
  try {
452
452
  body = await response.json();
453
- } catch {
454
- const error = new Error(`Telegram ${method} returned invalid JSON`);
453
+ } catch (cause) {
454
+ const error = new Error(`Telegram ${method} returned invalid JSON`, { cause });
455
455
  error.status = response?.status;
456
456
  error.code = 'telegram-response-invalid';
457
457
  error.deliveryOutcome = 'unknown';
@@ -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) {