@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, atConnectionStage, createConnectionDiagnostics } from './connection-error.mjs';
1
2
  import { connectionTestMessage } from './connection-test.mjs';
2
3
  import { t } from './i18n.mjs';
3
4
  import { publicMessageFailure } from './message-failure.mjs';
@@ -20,6 +21,7 @@ export class TokenBotController {
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();
@@ -61,6 +63,14 @@ export class TokenBotController {
61
63
  this.#createRuntime = createRuntime;
62
64
  this.#deleteState = deleteState;
63
65
  this.#logger = logger;
66
+ this.#diagnostics = createConnectionDiagnostics({ channel: descriptor.key, logger });
67
+ }
68
+
69
+ get diagnostics() { return this.#diagnostics; }
70
+
71
+ #failure(error, code, message) {
72
+ const stage = code.startsWith('qr-') ? 'qr.begin' : code === 'activation-failed' ? 'activation' : 'connection.start';
73
+ return this.#diagnostics.report(error, { reuse: true, stage, publicError: { code, message } }).publicError;
64
74
  }
65
75
 
66
76
  async initialize() {
@@ -68,30 +78,27 @@ export class TokenBotController {
68
78
  for (const config of this.#configStore.list()) {
69
79
  await this.#withBotTransition(config.botId, async () => {
70
80
  if (this.#closed || this.#runtimes.get(config.botId)?.status?.ready) return;
71
- const token = await this.#resolveToken(config.tokenRef);
72
- if (!token) {
73
- this.#errors.set(config.botId, safeError(
74
- 'missing-token',
75
- t('{label}机器人凭据缺失,请移除后重新接入。', {
76
- label: this.#descriptor.label,
77
- }),
78
- ));
79
- return;
80
- }
81
81
  try {
82
+ const token = await this.#resolveToken(config.tokenRef);
83
+ if (!token) {
84
+ this.#errors.set(config.botId, safeError(
85
+ 'missing-token',
86
+ t('{label}机器人凭据缺失,请移除后重新接入。', {
87
+ label: this.#descriptor.label,
88
+ }),
89
+ ));
90
+ return;
91
+ }
82
92
  await this.#startRuntime(config, token);
83
93
  this.#errors.delete(config.botId);
84
94
  } catch (error) {
85
- this.#errors.set(config.botId, safeError(
95
+ this.#errors.set(config.botId, this.#failure(error,
86
96
  'connection-failed',
87
97
  t('{label}连接未就绪,插件会自动重试。', {
88
98
  label: this.#descriptor.label,
89
99
  }),
90
100
  ));
91
- this.#logger.warn?.(
92
- `[dsh-im:${this.#descriptor.key}] bot ${config.botId} failed to initialize:`,
93
- error,
94
- );
101
+
95
102
  } finally {
96
103
  this.#touch();
97
104
  }
@@ -112,7 +119,7 @@ export class TokenBotController {
112
119
  await this.#withBotTransition(identity.botId, async () => {
113
120
  if (this.#closed) throw new Error(`${this.#descriptor.label} controller is closed`);
114
121
  const previousConfig = this.#configStore.getByPlatformId(platformId);
115
- const previousToken = await this.#credentials.resolve(identity.tokenRef).catch(() => undefined);
122
+ const previousToken = await atConnectionStage('credential.read', () => this.#credentials.resolve(identity.tokenRef), 'credential-store');
116
123
  const config = {
117
124
  botId: identity.botId,
118
125
  platformId,
@@ -122,9 +129,9 @@ export class TokenBotController {
122
129
  createdAt: previousConfig?.createdAt ?? new Date().toISOString(),
123
130
  connectedAt: new Date().toISOString(),
124
131
  };
125
- await this.#credentials.set(identity.tokenRef, normalizedToken);
132
+ await atConnectionStage('credential.save', () => this.#credentials.set(identity.tokenRef, normalizedToken), 'credential-store');
126
133
  try {
127
- await this.#configStore.save(config);
134
+ await atConnectionStage('account.save', () => this.#configStore.save(config), 'account-config');
128
135
  } catch (error) {
129
136
  await this.#restoreCredential(identity.tokenRef, previousToken);
130
137
  throw error;
@@ -133,16 +140,13 @@ export class TokenBotController {
133
140
  await this.#startRuntime(config, normalizedToken);
134
141
  this.#errors.delete(identity.botId);
135
142
  } catch (error) {
136
- this.#errors.set(identity.botId, safeError(
143
+ this.#errors.set(identity.botId, this.#failure(error,
137
144
  'connection-failed',
138
145
  t('{label}机器人已接入,消息连接暂未就绪。', {
139
146
  label: this.#descriptor.label,
140
147
  }),
141
148
  ));
142
- this.#logger.warn?.(
143
- `[dsh-im:${this.#descriptor.key}] bot ${identity.botId} credential connection failed:`,
144
- error,
145
- );
149
+
146
150
  }
147
151
  this.#touch();
148
152
  });
@@ -159,7 +163,7 @@ export class TokenBotController {
159
163
  await this.#startRuntime(config, token);
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('{label}连接仍未就绪,请稍后重试。', {
165
169
  label: this.#descriptor.label,
@@ -184,12 +188,12 @@ export class TokenBotController {
184
188
  if (!token) throw new Error(`${this.#descriptor.label} bot token is missing`);
185
189
  if (this.#closed) throw new Error(`${this.#descriptor.label} controller is closed`);
186
190
  const nextConfig = update(config);
187
- const savedConfig = await this.#configStore.save(nextConfig);
191
+ const savedConfig = await atConnectionStage('account.save', () => this.#configStore.save(nextConfig), 'account-config');
188
192
  try {
189
193
  await this.#startRuntime(savedConfig, token);
190
194
  this.#errors.delete(botId);
191
195
  } catch (error) {
192
- this.#errors.set(botId, safeError(
196
+ this.#errors.set(botId, this.#failure(error,
193
197
  'connection-failed',
194
198
  t('{label}连接仍未就绪,请稍后重试。', {
195
199
  label: this.#descriptor.label,
@@ -244,31 +248,32 @@ export class TokenBotController {
244
248
  }
245
249
 
246
250
  async deleteBot(botId) {
251
+ const warnings = [];
247
252
  const config = this.#configStore.get(botId);
248
253
  if (!config) throw new Error(`Unknown ${this.#descriptor.label} bot`);
249
254
  await this.#withBotTransition(botId, async () => {
250
- const previous = await this.#credentials.resolve(config.tokenRef).catch(() => undefined);
255
+ const previous = await atConnectionStage('credential.read', () => this.#credentials.resolve(config.tokenRef), 'credential-store');
251
256
  await this.#stopRuntime(botId);
252
257
  try {
253
- await this.#credentials.unset(config.tokenRef);
254
- await this.#configStore.remove(botId);
258
+ await atConnectionStage('credential.remove', () => this.#credentials.unset(config.tokenRef), 'credential-store');
259
+ await atConnectionStage('account.remove', () => this.#configStore.remove(botId), 'account-config');
255
260
  } catch (error) {
256
- if (previous?.value) {
257
- await this.#credentials.set(config.tokenRef, previous.value).catch(() => undefined);
258
- await this.#startRuntime(config, previous.value).catch(() => undefined);
261
+ if (!this.#configStore.get(botId)) {
262
+ warnings.push(this.#diagnostics.report(error, { operation: 'bot.delete', stage: 'workspace.cleanup', warning: true,
263
+ publicError: { code: 'workspace-cleanup-failed', message: '账号已移除,但本地状态清理失败。' } }).publicError);
264
+ } else {
265
+ if (previous?.value) {
266
+ await atConnectionStage('credential.save', () => this.#credentials.set(config.tokenRef, previous.value), 'credential-store').catch(() => undefined);
267
+ await this.#startRuntime(config, previous.value).catch(() => undefined);
268
+ }
269
+ throw new Error(`Unable to remove the ${this.#descriptor.label} bot safely.`, { cause: error });
259
270
  }
260
- throw new Error(`Unable to remove the ${this.#descriptor.label} bot safely.`, { cause: error });
261
271
  }
262
- await this.#deleteState({ botId, config }).catch((error) => {
263
- this.#logger.warn?.(
264
- `[dsh-im:${this.#descriptor.key}] bot ${botId} state cleanup failed:`,
265
- error,
266
- );
267
- });
272
+ 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); });
268
273
  this.#errors.delete(botId);
269
274
  this.#touch();
270
275
  });
271
- return this.status();
276
+ return { ...this.status(), ...(warnings.length ? { warnings } : {}) };
272
277
  }
273
278
 
274
279
  status() {
@@ -309,7 +314,7 @@ export class TokenBotController {
309
314
  messagesReplied: runtimeStatus?.messagesReplied ?? 0,
310
315
  },
311
316
  lastMessageError: publicMessageFailure(runtimeStatus?.lastMessageError),
312
- error: structuredClone(this.#errors.get(config.botId) ?? null),
317
+ error: structuredClone(runtimeStatus?.error ?? this.#errors.get(config.botId) ?? null),
313
318
  };
314
319
  });
315
320
  const connectedCount = bots.filter((bot) => bot.connected).length;
@@ -342,7 +347,7 @@ export class TokenBotController {
342
347
  } catch (error) {
343
348
  this.#logger.warn?.(
344
349
  `[dsh-im:${this.#descriptor.key}] bot ${botId} command menu refresh failed:`,
345
- error,
350
+ extractConnectionEvidence(error).details,
346
351
  );
347
352
  return false;
348
353
  }
@@ -361,7 +366,7 @@ export class TokenBotController {
361
366
  if (this.#closed) throw new Error(`${this.#descriptor.label} controller is closed`);
362
367
  await this.#stopRuntime(config.botId);
363
368
  if (this.#closed) throw new Error(`${this.#descriptor.label} controller is closed`);
364
- const runtime = await this.#createRuntime({ botId: config.botId, config, token });
369
+ const runtime = await atConnectionStage('runtime.prepare', () => this.#createRuntime({ botId: config.botId, config, token }));
365
370
  if (!runtime || typeof runtime.start !== 'function' || typeof runtime.stop !== 'function') {
366
371
  throw new TypeError(`createRuntime returned an invalid ${this.#descriptor.label} runtime`);
367
372
  }
@@ -381,19 +386,19 @@ export class TokenBotController {
381
386
  await runtime?.stop().catch((error) => {
382
387
  this.#logger.warn?.(
383
388
  `[dsh-im:${this.#descriptor.key}] bot ${botId} failed to stop cleanly:`,
384
- error,
389
+ extractConnectionEvidence(error).details,
385
390
  );
386
391
  });
387
392
  }
388
393
 
389
394
  async #resolveToken(ref) {
390
- const result = await this.#credentials.resolve(ref).catch(() => undefined);
395
+ const result = await atConnectionStage('credential.read', () => this.#credentials.resolve(ref), 'credential-store');
391
396
  return cleanString(result?.value);
392
397
  }
393
398
 
394
399
  async #restoreCredential(ref, previous) {
395
- if (previous?.value) await this.#credentials.set(ref, previous.value).catch(() => undefined);
396
- else await this.#credentials.unset(ref).catch(() => undefined);
400
+ if (previous?.value) await atConnectionStage('credential.save', () => this.#credentials.set(ref, previous.value), 'credential-store').catch(() => undefined);
401
+ else await atConnectionStage('credential.remove', () => this.#credentials.unset(ref), 'credential-store').catch(() => undefined);
397
402
  }
398
403
 
399
404
  #withBotTransition(botId, operation) {
@@ -522,7 +522,7 @@ export class SlackApi {
522
522
  });
523
523
  } catch (error) {
524
524
  if (error?.name === 'AbortError' || error?.name === 'TimeoutError') throw error;
525
- throw new Error(`Slack ${method} transport failed`);
525
+ throw new Error(`Slack ${method} transport failed`, { cause: error });
526
526
  }
527
527
 
528
528
  if (tokenKind === 'bot') {
@@ -533,8 +533,8 @@ export class SlackApi {
533
533
  let payload;
534
534
  try {
535
535
  payload = await response.json();
536
- } catch {
537
- throw new Error(`Slack ${method} returned invalid JSON`);
536
+ } catch (cause) {
537
+ throw Object.assign(new Error(`Slack ${method} returned invalid JSON`, { cause }), { status: response?.status });
538
538
  }
539
539
  if (response.status === 429 && retry) {
540
540
  const seconds = Number(response.headers.get('retry-after')) || 1;
@@ -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';