@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 { 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);
@@ -1,3 +1,4 @@
1
+ import { extractConnectionEvidence, atConnectionStage, createConnectionDiagnostics } from '../shared/connection-error.mjs';
1
2
  import {
2
3
  deriveWecomAppIdentity,
3
4
  generateCallbackSecret,
@@ -42,6 +43,7 @@ export class WecomAppController {
42
43
  #createRuntime;
43
44
  #deleteState;
44
45
  #logger;
46
+ #diagnostics;
45
47
  #buildCallbackUrl;
46
48
  #runtimes = new Map();
47
49
  #errors = new Map();
@@ -71,25 +73,33 @@ export class WecomAppController {
71
73
  this.#createRuntime = createRuntime;
72
74
  this.#deleteState = deleteState;
73
75
  this.#logger = logger;
76
+ this.#diagnostics = createConnectionDiagnostics({ channel: 'wecom-app', logger });
74
77
  this.#buildCallbackUrl = typeof buildCallbackUrl === 'function' ? buildCallbackUrl : null;
75
78
  }
76
79
 
80
+ get diagnostics() { return this.#diagnostics; }
81
+
82
+ #failure(error, code, message) {
83
+ const stage = code.startsWith('qr-') ? 'qr.begin' : code === 'activation-failed' ? 'activation' : 'connection.start';
84
+ return this.#diagnostics.report(error, { reuse: true, stage, publicError: { code, message } }).publicError;
85
+ }
86
+
77
87
  async initialize() {
78
88
  if (this.#closed) return this.status();
79
89
  for (const config of this.#configStore.list()) {
80
90
  await this.#withBotTransition(config.botId, async () => {
81
91
  if (this.#runtimes.has(config.botId)) return;
82
- const secrets = await this.#resolveSecrets(config);
83
- if (!secrets) {
84
- this.#errors.set(config.botId, safeError('missing-credentials', t('企业微信应用凭据缺失,请重新绑定。')));
85
- return;
86
- }
87
92
  try {
93
+ const secrets = await this.#resolveSecrets(config);
94
+ if (!secrets) {
95
+ this.#errors.set(config.botId, safeError('missing-credentials', t('企业微信应用凭据缺失,请重新绑定。')));
96
+ return;
97
+ }
88
98
  await this.#startRuntime(config, secrets);
89
99
  this.#errors.delete(config.botId);
90
100
  } catch (error) {
91
- this.#errors.set(config.botId, safeError('connection-failed', t('企业微信应用连接未就绪,插件会自动重试。')));
92
- this.#logger.warn?.(`[dsh-im:wecom-app] bot ${config.botId} failed to initialize`, error);
101
+ this.#errors.set(config.botId, this.#failure(error, 'connection-failed', t('企业微信应用连接未就绪,插件会自动重试。')));
102
+
93
103
  } finally {
94
104
  this.#touch();
95
105
  }
@@ -147,7 +157,7 @@ export class WecomAppController {
147
157
  encodingAESKey: normalizedAesKey,
148
158
  });
149
159
  try {
150
- await this.#configStore.save(config);
160
+ await atConnectionStage('account.save', () => this.#configStore.save(config), 'account-config');
151
161
  } catch (error) {
152
162
  await this.#restoreSecrets(identity, previous);
153
163
  throw error;
@@ -160,8 +170,8 @@ export class WecomAppController {
160
170
  });
161
171
  this.#errors.delete(botId);
162
172
  } catch (error) {
163
- this.#errors.set(botId, safeError('connection-failed', t('企业微信应用已绑定,但尚未就绪。')));
164
- this.#logger.warn?.(`[dsh-im:wecom-app] bot ${botId} credential connection failed`, error);
173
+ this.#errors.set(botId, this.#failure(error, 'connection-failed', t('企业微信应用已绑定,但尚未就绪。')));
174
+
165
175
  }
166
176
  this.#touch();
167
177
  });
@@ -202,8 +212,8 @@ export class WecomAppController {
202
212
  try {
203
213
  await this.#startRuntime(updated, secrets);
204
214
  } catch (error) {
205
- this.#errors.set(botId, safeError('connection-failed', t('企业微信应用已更新,但尚未就绪。')));
206
- this.#logger.warn?.(`[dsh-im:wecom-app] bot ${botId} restart after settings update failed`, error);
215
+ this.#errors.set(botId, this.#failure(error, 'connection-failed', t('企业微信应用已更新,但尚未就绪。')));
216
+
207
217
  }
208
218
  }
209
219
  }
@@ -224,8 +234,7 @@ export class WecomAppController {
224
234
  try {
225
235
  await this.#startRuntime(updated, secrets);
226
236
  } catch (error) {
227
- this.#errors.set(botId, safeError('connection-failed', t('回调密钥已重置,但机器人重启失败,请手动重连。')));
228
- this.#logger.warn?.(`[dsh-im:wecom-app] bot ${botId} restart after secret reset failed`, error);
237
+ this.#errors.set(botId, this.#failure(error, 'connection-failed', t('回调密钥已重置,但机器人重启失败,请手动重连。')));
229
238
  }
230
239
  }
231
240
  this.#touch();
@@ -243,7 +252,7 @@ export class WecomAppController {
243
252
  await this.#startRuntime(config, secrets);
244
253
  this.#errors.delete(botId);
245
254
  } catch (error) {
246
- this.#errors.set(botId, safeError('connection-failed', t('企业微信应用仍未就绪,请稍后重试。')));
255
+ this.#errors.set(botId, this.#failure(error, 'connection-failed', t('企业微信应用仍未就绪,请稍后重试。')));
247
256
  throw error;
248
257
  } finally {
249
258
  this.#touch();
@@ -282,30 +291,34 @@ export class WecomAppController {
282
291
  }
283
292
 
284
293
  async deleteBot(botId) {
294
+ const warnings = [];
285
295
  const config = this.#configStore.get(botId);
286
296
  if (!config) throw new Error('Unknown Enterprise WeChat app bot');
287
297
  await this.#withBotTransition(botId, async () => {
288
298
  const previous = await this.#resolveSecrets(config);
289
299
  await this.#stopRuntime(botId);
290
300
  try {
291
- await this.#credentials.unset(config.secretRef);
292
- await this.#credentials.unset(config.callbackTokenRef);
293
- await this.#credentials.unset(config.callbackKeyRef);
294
- await this.#configStore.remove(botId);
301
+ await atConnectionStage('credential.remove', () => this.#credentials.unset(config.secretRef), 'credential-store');
302
+ await atConnectionStage('credential.remove', () => this.#credentials.unset(config.callbackTokenRef), 'credential-store');
303
+ await atConnectionStage('credential.remove', () => this.#credentials.unset(config.callbackKeyRef), 'credential-store');
304
+ await atConnectionStage('account.remove', () => this.#configStore.remove(botId), 'account-config');
295
305
  } catch (error) {
296
- if (previous) {
297
- await this.#setSecrets(config, previous).catch(() => undefined);
298
- await this.#startRuntime(config, previous).catch(() => undefined);
306
+ if (!this.#configStore.get(botId)) {
307
+ warnings.push(this.#diagnostics.report(error, { operation: 'bot.delete', stage: 'workspace.cleanup', warning: true,
308
+ publicError: { code: 'workspace-cleanup-failed', message: '账号已移除,但本地状态清理失败。' } }).publicError);
309
+ } else {
310
+ if (previous) {
311
+ await this.#setSecrets(config, previous).catch(() => undefined);
312
+ await this.#startRuntime(config, previous).catch(() => undefined);
313
+ }
314
+ throw new Error('Unable to remove the Enterprise WeChat app bot safely.', { cause: error });
299
315
  }
300
- throw new Error('Unable to remove the Enterprise WeChat app bot safely.', { cause: error });
301
316
  }
302
- await this.#deleteState({ botId, config }).catch((error) => {
303
- this.#logger.warn?.(`[dsh-im:wecom-app] bot ${botId} state cleanup failed:`, error);
304
- });
317
+ 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); });
305
318
  this.#errors.delete(botId);
306
319
  this.#touch();
307
320
  });
308
- return this.status();
321
+ return { ...this.status(), ...(warnings.length ? { warnings } : {}) };
309
322
  }
310
323
 
311
324
  status() {
@@ -347,7 +360,7 @@ export class WecomAppController {
347
360
  messagesReplied: runtimeStatus?.messagesReplied ?? 0,
348
361
  },
349
362
  lastMessageError: publicMessageFailure(runtimeStatus?.lastMessageError),
350
- error: structuredClone(this.#errors.get(config.botId) ?? null),
363
+ error: structuredClone(runtimeStatus?.error ?? this.#errors.get(config.botId) ?? null),
351
364
  };
352
365
  });
353
366
  const connectedCount = bots.filter((bot) => bot.connected).length;
@@ -373,7 +386,7 @@ export class WecomAppController {
373
386
  if (this.#closed) throw new Error('Enterprise WeChat app controller is closed');
374
387
  await this.#stopRuntime(config.botId);
375
388
  if (this.#closed) throw new Error('Enterprise WeChat app controller is closed');
376
- const runtime = await this.#createRuntime({ botId: config.botId, config, secrets });
389
+ const runtime = await atConnectionStage('runtime.prepare', () => this.#createRuntime({ botId: config.botId, config, secrets }));
377
390
  if (!runtime || typeof runtime.start !== 'function' || typeof runtime.stop !== 'function') {
378
391
  throw new TypeError('createRuntime returned an invalid Enterprise WeChat app runtime');
379
392
  }
@@ -391,7 +404,7 @@ export class WecomAppController {
391
404
  const runtime = this.#runtimes.get(botId);
392
405
  this.#runtimes.delete(botId);
393
406
  await runtime?.stop().catch((error) => {
394
- this.#logger.warn?.(`[dsh-im:wecom-app] bot ${botId} failed to stop cleanly:`, error);
407
+ this.#logger.warn?.(`[dsh-im:wecom-app] bot ${botId} failed to stop cleanly:`, extractConnectionEvidence(error).details);
395
408
  });
396
409
  }
397
410
 
@@ -404,13 +417,13 @@ export class WecomAppController {
404
417
  }
405
418
 
406
419
  async #setSecrets(identity, { corpSecret, token, encodingAESKey }) {
407
- await this.#credentials.set(identity.secretRef, corpSecret);
408
- await this.#credentials.set(identity.callbackTokenRef, token);
409
- await this.#credentials.set(identity.callbackKeyRef, encodingAESKey);
420
+ await atConnectionStage('credential.save', () => this.#credentials.set(identity.secretRef, corpSecret), 'credential-store');
421
+ await atConnectionStage('credential.save', () => this.#credentials.set(identity.callbackTokenRef, token), 'credential-store');
422
+ await atConnectionStage('credential.save', () => this.#credentials.set(identity.callbackKeyRef, encodingAESKey), 'credential-store');
410
423
  }
411
424
 
412
425
  async #resolveSecret(ref) {
413
- const result = await this.#credentials.resolve(ref).catch(() => undefined);
426
+ const result = await atConnectionStage('credential.read', () => this.#credentials.resolve(ref), 'credential-store');
414
427
  return cleanString(result?.value);
415
428
  }
416
429
 
@@ -419,9 +432,9 @@ export class WecomAppController {
419
432
  await this.#setSecrets(identity, previous).catch(() => undefined);
420
433
  return;
421
434
  }
422
- await this.#credentials.unset(identity.secretRef).catch(() => undefined);
423
- await this.#credentials.unset(identity.callbackTokenRef).catch(() => undefined);
424
- await this.#credentials.unset(identity.callbackKeyRef).catch(() => undefined);
435
+ await atConnectionStage('credential.remove', () => this.#credentials.unset(identity.secretRef), 'credential-store').catch(() => undefined);
436
+ await atConnectionStage('credential.remove', () => this.#credentials.unset(identity.callbackTokenRef), 'credential-store').catch(() => undefined);
437
+ await atConnectionStage('credential.remove', () => this.#credentials.unset(identity.callbackKeyRef), 'credential-store').catch(() => undefined);
425
438
  }
426
439
 
427
440
  #withBotTransition(botId, operation) {
@@ -1,3 +1,4 @@
1
+ import { atConnectionStage } from '../shared/connection-error.mjs';
1
2
  import {
2
3
  WecomAppApi,
3
4
  WecomAppError,
@@ -104,7 +105,7 @@ export class WecomAppRuntime {
104
105
  this.#status.startedAt = new Date().toISOString();
105
106
  this.#status.lastError = null;
106
107
  this.#status.wecomAppConnectionState = 'connecting';
107
- await this.#harness.ensureRunning({ signal: this.#signal() });
108
+ await atConnectionStage('harness.check', () => this.#harness.ensureRunning({ signal: this.#signal() }));
108
109
  this.#status.harnessReachable = true;
109
110
 
110
111
  this.#api = new WecomAppApi({