@xmanrui/dsh-im 4.22.0 → 4.24.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 (148) hide show
  1. package/README.en.md +5 -4
  2. package/README.md +5 -4
  3. package/lib/client.js +2584 -1153
  4. package/lib/index.js +290 -298
  5. package/package.json +14 -2
  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 +23 -7
  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 +8 -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/telegram/index.js +3 -0
  24. package/plugin-src/client/channels/telegram/styles.js +12 -0
  25. package/plugin-src/client/channels/telegram/thinking-traces.js +25 -0
  26. package/plugin-src/client/channels/wecom/api.js +7 -1
  27. package/plugin-src/client/channels/wecom/index.js +16 -3
  28. package/plugin-src/client/channels/wecom-app/api.js +6 -1
  29. package/plugin-src/client/channels/wecom-app/index.js +16 -3
  30. package/plugin-src/client/channels/weixin/api.js +2 -1
  31. package/plugin-src/client/channels/weixin/connection-error.js +1 -71
  32. package/plugin-src/client/channels/whatsapp/api.js +5 -0
  33. package/plugin-src/client/channels/whatsapp/index.js +16 -3
  34. package/plugin-src/client/connection-error.js +87 -0
  35. package/plugin-src/client/global-settings.js +81 -1
  36. package/plugin-src/client/i18n.js +69 -1
  37. package/plugin-src/client/index.js +40 -4
  38. package/plugin-src/client/last-message-error.js +2 -1
  39. package/plugin-src/client/styles.js +8 -2
  40. package/plugin-src/host/build.mjs +4 -0
  41. package/plugin-src/host/channels/dingtalk/production.mjs +2 -0
  42. package/plugin-src/host/channels/dingtalk/rpc.mjs +7 -4
  43. package/plugin-src/host/channels/email/availability.mjs +60 -0
  44. package/plugin-src/host/channels/email/index.mjs +36 -0
  45. package/plugin-src/host/channels/email/production.mjs +58 -0
  46. package/plugin-src/host/channels/email/rpc.mjs +163 -0
  47. package/plugin-src/host/channels/feishu/production.mjs +3 -0
  48. package/plugin-src/host/channels/feishu/rpc.mjs +7 -4
  49. package/plugin-src/host/channels/imessage/rpc.mjs +13 -6
  50. package/plugin-src/host/channels/office/rpc.mjs +3 -1
  51. package/plugin-src/host/channels/qq/production.mjs +2 -0
  52. package/plugin-src/host/channels/qq/rpc.mjs +5 -3
  53. package/plugin-src/host/channels/shared/production.mjs +52 -5
  54. package/plugin-src/host/channels/shared/rpc.mjs +14 -3
  55. package/plugin-src/host/channels/shared/startup-error.mjs +4 -3
  56. package/plugin-src/host/channels/shared/startup.mjs +11 -6
  57. package/plugin-src/host/channels/shared/thinking-traces-rpc.mjs +11 -0
  58. package/plugin-src/host/channels/slack/production.mjs +2 -0
  59. package/plugin-src/host/channels/slack/rpc.mjs +5 -3
  60. package/plugin-src/host/channels/wecom/production.mjs +2 -0
  61. package/plugin-src/host/channels/wecom/rpc.mjs +5 -3
  62. package/plugin-src/host/channels/wecom-app/production.mjs +2 -0
  63. package/plugin-src/host/channels/wecom-app/rpc.mjs +5 -3
  64. package/plugin-src/host/channels/weixin/production.mjs +2 -0
  65. package/plugin-src/host/channels/whatsapp/production.mjs +2 -0
  66. package/plugin-src/host/channels/whatsapp/rpc.mjs +5 -3
  67. package/plugin-src/host/delivery-adapter.mjs +11 -0
  68. package/plugin-src/host/image-input-rpc.mjs +24 -0
  69. package/plugin-src/host/inbound-ttl-rpc.mjs +6 -1
  70. package/plugin-src/host/index.mjs +3 -0
  71. package/plugin-src/host/modern-harness-api.mjs +7 -2
  72. package/plugin-src/management-rpc.mjs +12 -2
  73. package/scripts/verify-package.mjs +13 -5
  74. package/src/channels/dingtalk/connection-error.mjs +5 -12
  75. package/src/channels/dingtalk/device-auth.mjs +4 -1
  76. package/src/channels/dingtalk/dingtalk-bridge.mjs +6 -4
  77. package/src/channels/dingtalk/dingtalk-controller.mjs +60 -45
  78. package/src/channels/dingtalk/dingtalk-runtime.mjs +20 -4
  79. package/src/channels/discord/discord-api.mjs +3 -3
  80. package/src/channels/discord/discord-runtime.mjs +18 -12
  81. package/src/channels/email/config-store.mjs +194 -0
  82. package/src/channels/email/email-api.mjs +20 -0
  83. package/src/channels/email/email-bridge.mjs +17 -0
  84. package/src/channels/email/email-controller.mjs +824 -0
  85. package/src/channels/email/email-runtime.mjs +699 -0
  86. package/src/channels/email/harness-client.mjs +7 -0
  87. package/src/channels/email/mail-format.mjs +91 -0
  88. package/src/channels/email/state-store.mjs +176 -0
  89. package/src/channels/email/transport.mjs +64 -0
  90. package/src/channels/email/transports/agent-mail.mjs +604 -0
  91. package/src/channels/email/transports/agently-cli.mjs +288 -0
  92. package/src/channels/email/transports/imap-smtp.mjs +165 -0
  93. package/src/channels/feishu/bridge.mjs +39 -11
  94. package/src/channels/feishu/feishu-channel.mjs +35 -0
  95. package/src/channels/feishu/feishu-runtime.mjs +15 -8
  96. package/src/channels/feishu/group-message-permission-manager.mjs +2 -1
  97. package/src/channels/feishu/live-cot.mjs +260 -0
  98. package/src/channels/feishu/message-utils.mjs +3 -2
  99. package/src/channels/feishu/multi-bot-controller.mjs +56 -47
  100. package/src/channels/feishu/registration-manager.mjs +8 -4
  101. package/src/channels/feishu/repair-manager.mjs +2 -1
  102. package/src/channels/feishu/slash-command-registry.mjs +17 -0
  103. package/src/channels/feishu/step-push-mode.mjs +10 -4
  104. package/src/channels/imessage/runtime.mjs +9 -5
  105. package/src/channels/office/office-controller.mjs +23 -12
  106. package/src/channels/office/office-runtime.mjs +6 -3
  107. package/src/channels/office/office-transport.mjs +1 -0
  108. package/src/channels/qq/qq-bridge.mjs +7 -5
  109. package/src/channels/qq/qq-controller.mjs +56 -42
  110. package/src/channels/qq/qq-runtime.mjs +16 -11
  111. package/src/channels/shared/bot-workspace-store.mjs +29 -10
  112. package/src/channels/shared/connection-error.mjs +191 -0
  113. package/src/channels/shared/connection-test.mjs +3 -1
  114. package/src/channels/shared/conversation-state-store.mjs +46 -1
  115. package/src/channels/shared/diagnostic-details.mjs +95 -0
  116. package/src/channels/shared/harness-client.mjs +287 -57
  117. package/src/channels/shared/i18n-en/diagnostics.mjs +55 -0
  118. package/src/channels/shared/i18n-en/email.mjs +25 -0
  119. package/src/channels/shared/i18n-en/image-input.mjs +17 -0
  120. package/src/channels/shared/i18n-en/shared-a.mjs +1 -0
  121. package/src/channels/shared/i18n-en/shared-c.mjs +1 -1
  122. package/src/channels/shared/i18n-en.mjs +6 -0
  123. package/src/channels/shared/image-input-policy.mjs +35 -0
  124. package/src/channels/shared/image-input-settings-store.mjs +60 -0
  125. package/src/channels/shared/image-input.mjs +124 -0
  126. package/src/channels/shared/image-prompt.mjs +15 -7
  127. package/src/channels/shared/message-failure.mjs +12 -0
  128. package/src/channels/shared/semantic/reply-reference.mjs +3 -3
  129. package/src/channels/shared/text-harness-bridge.mjs +150 -25
  130. package/src/channels/shared/token-bot-controller.mjs +52 -47
  131. package/src/channels/slack/slack-api.mjs +3 -3
  132. package/src/channels/slack/slack-controller.mjs +59 -51
  133. package/src/channels/slack/slack-runtime.mjs +18 -12
  134. package/src/channels/telegram/config-store.mjs +4 -1
  135. package/src/channels/telegram/telegram-api.mjs +2 -2
  136. package/src/channels/telegram/telegram-controller.mjs +13 -1
  137. package/src/channels/telegram/telegram-runtime.mjs +215 -14
  138. package/src/channels/wecom/wecom-bridge.mjs +15 -6
  139. package/src/channels/wecom/wecom-controller.mjs +51 -37
  140. package/src/channels/wecom/wecom-runtime.mjs +10 -6
  141. package/src/channels/wecom-app/wecom-app-bridge.mjs +7 -5
  142. package/src/channels/wecom-app/wecom-app-controller.mjs +50 -37
  143. package/src/channels/wecom-app/wecom-app-runtime.mjs +2 -1
  144. package/src/channels/weixin/connection-error.mjs +26 -92
  145. package/src/channels/weixin/diagnostic-details.mjs +1 -63
  146. package/src/channels/weixin/weixin-bridge.mjs +7 -5
  147. package/src/channels/whatsapp/whatsapp-controller.mjs +34 -17
  148. package/src/channels/whatsapp/whatsapp-runtime.mjs +35 -8
@@ -1,3 +1,4 @@
1
+ import { atConnectionStage, createConnectionDiagnostics } from '../shared/connection-error.mjs';
1
2
  import { randomUUID } from 'node:crypto';
2
3
  import { connectionTestMessage } from '../shared/connection-test.mjs';
3
4
  import { publicMessageFailure } from '../shared/message-failure.mjs';
@@ -115,6 +116,7 @@ function optionalNonEmptyString(value) {
115
116
  * runtime lifecycles may proceed independently.
116
117
  */
117
118
  export class MultiBotDshFeishuController {
119
+ #diagnostics;
118
120
  #registerApp;
119
121
  #verifyApp;
120
122
  #credentials;
@@ -138,6 +140,7 @@ export class MultiBotDshFeishuController {
138
140
  constructor({
139
141
  registerApp,
140
142
  verifyApp,
143
+ logger = console,
141
144
  credentials,
142
145
  configStore,
143
146
  createRuntime,
@@ -159,6 +162,7 @@ export class MultiBotDshFeishuController {
159
162
  || callbackProbeTimeoutMs > MAX_CALLBACK_PROBE_TIMEOUT_MS) {
160
163
  throw new TypeError('callbackProbeTimeoutMs must be between 1 and 600000ms');
161
164
  }
165
+ this.#diagnostics = createConnectionDiagnostics({ channel: 'feishu', logger });
162
166
  this.#registerApp = registerApp;
163
167
  this.#verifyApp = verifyApp;
164
168
  this.#credentials = credentials;
@@ -191,12 +195,12 @@ export class MultiBotDshFeishuController {
191
195
  }
192
196
  let resolved;
193
197
  try {
194
- resolved = await this.#credentials.resolve(config.secretRef);
195
- } catch {
196
- this.#botErrors.set(config.id, {
198
+ resolved = await atConnectionStage('credential.read', () => this.#credentials.resolve(config.secretRef), 'credential-store');
199
+ } catch (error) {
200
+ this.#botErrors.set(config.id, this.#diagnostics.report(error, { reuse: true, botId: config.id, publicError: {
197
201
  code: 'missing_credentials',
198
202
  message: '无法读取机器人凭据,请检查凭据存储。',
199
- });
203
+ } }).publicError);
200
204
  return;
201
205
  }
202
206
  if (!resolved?.value) {
@@ -209,11 +213,11 @@ export class MultiBotDshFeishuController {
209
213
  try {
210
214
  await this.#startRuntime(config, resolved.value);
211
215
  this.#botErrors.delete(config.id);
212
- } catch {
213
- this.#botErrors.set(config.id, {
216
+ } catch (error) {
217
+ this.#botErrors.set(config.id, this.#diagnostics.report(error, { reuse: true, botId: config.id, stage: 'connection.start', publicError: {
214
218
  code: 'connection_failed',
215
219
  message: '机器人暂时无法连接飞书,请重试。',
216
- });
220
+ } }).publicError);
217
221
  }
218
222
  })));
219
223
  if (attempted) this.#touch();
@@ -228,6 +232,7 @@ export class MultiBotDshFeishuController {
228
232
  }
229
233
  const record = { id, manager: null, botId: null, createdNew: false, cancelled: false };
230
234
  record.manager = new RegistrationManager({
235
+ diagnostics: this.#diagnostics,
231
236
  registerApp: this.#registerApp,
232
237
  onCredentials: (result) => this.#serializeConfig(() => this.#acceptCredentials(record, result)),
233
238
  });
@@ -290,6 +295,7 @@ export class MultiBotDshFeishuController {
290
295
  },
291
296
  };
292
297
  record.manager = new CallbackRepairManager({
298
+ diagnostics: this.#diagnostics,
293
299
  registerApp: this.#registerApp,
294
300
  appId: target.appId,
295
301
  domain: target.domain,
@@ -349,6 +355,7 @@ export class MultiBotDshFeishuController {
349
355
  initiator: { actorOpenId: null, chatId: null },
350
356
  };
351
357
  record.manager = new GroupMessagePermissionManager({
358
+ diagnostics: this.#diagnostics,
352
359
  registerApp: this.#registerApp,
353
360
  appId: target.appId,
354
361
  domain: target.domain,
@@ -407,6 +414,8 @@ export class MultiBotDshFeishuController {
407
414
  return this.registrationStatus(attemptId) ?? this.status();
408
415
  }
409
416
 
417
+ get diagnostics() { return this.#diagnostics; }
418
+
410
419
  status(botId) {
411
420
  return this.#status({
412
421
  registration: this.#registrations.get(this.#latestRegistrationId) ?? null,
@@ -440,7 +449,7 @@ export class MultiBotDshFeishuController {
440
449
  throw new Error('Bot id generator returned an invalid or duplicate id');
441
450
  }
442
451
  const secretRef = existing?.secretRef ?? secretRefFor(botId);
443
- const previousSecret = await this.#credentials.resolve(secretRef).catch(() => undefined);
452
+ const previousSecret = await atConnectionStage('credential.read', () => this.#credentials.resolve(secretRef), 'credential-store').catch(() => undefined);
444
453
  const config = {
445
454
  ...existing,
446
455
  id: botId,
@@ -460,7 +469,7 @@ export class MultiBotDshFeishuController {
460
469
  createdAt: existing?.createdAt ?? new Date().toISOString(),
461
470
  };
462
471
 
463
- await this.#credentials.set(secretRef, normalizedSecret);
472
+ await atConnectionStage('credential.save', () => this.#credentials.set(secretRef, normalizedSecret), 'credential-store');
464
473
  let saved;
465
474
  try {
466
475
  saved = await this.#configStore.saveBot(config);
@@ -473,11 +482,11 @@ export class MultiBotDshFeishuController {
473
482
  try {
474
483
  await this.#startRuntime(saved, normalizedSecret);
475
484
  this.#botErrors.delete(botId);
476
- } catch {
477
- this.#botErrors.set(botId, {
485
+ } catch (error) {
486
+ this.#botErrors.set(botId, this.#diagnostics.report(error, { reuse: true, botId: botId, publicError: {
478
487
  code: 'connection_failed',
479
488
  message: '机器人已经绑定,但长连接未就绪,请点击重试。',
480
- });
489
+ } }).publicError);
481
490
  }
482
491
  });
483
492
  this.#touch();
@@ -501,8 +510,8 @@ export class MultiBotDshFeishuController {
501
510
  }
502
511
  let resolved;
503
512
  try {
504
- resolved = await this.#credentials.resolve(config.secretRef);
505
- } catch {
513
+ resolved = await atConnectionStage('credential.read', () => this.#credentials.resolve(config.secretRef), 'credential-store');
514
+ } catch (error) {
506
515
  resolved = null;
507
516
  }
508
517
  if (!resolved?.value) {
@@ -516,11 +525,11 @@ export class MultiBotDshFeishuController {
516
525
  try {
517
526
  await this.#startRuntime(config, resolved.value);
518
527
  this.#botErrors.delete(botId);
519
- } catch {
520
- this.#botErrors.set(botId, {
528
+ } catch (error) {
529
+ this.#botErrors.set(botId, this.#diagnostics.report(error, { reuse: true, botId: botId, stage: 'connection.start', publicError: {
521
530
  code: 'connection_failed',
522
531
  message: '机器人暂时无法连接飞书,请重试。',
523
- });
532
+ } }).publicError);
524
533
  }
525
534
  this.#touch();
526
535
  return this.status(botId);
@@ -691,7 +700,7 @@ export class MultiBotDshFeishuController {
691
700
  const bots = this.#configStore.list().map((config) => {
692
701
  const connection = connectionStatus(this.#runtimes.get(config.id));
693
702
  const connected = isConnected(connection);
694
- const error = this.#botErrors.get(config.id) ?? null;
703
+ const error = connectionStatus(this.#runtimes.get(config.id)).error ?? this.#botErrors.get(config.id) ?? null;
695
704
  return {
696
705
  botId: config.id,
697
706
  phase: botPhase({ connected, error, connection }),
@@ -832,7 +841,7 @@ export class MultiBotDshFeishuController {
832
841
 
833
842
  let previous;
834
843
  try {
835
- previous = await this.#credentials.resolve(current.secretRef);
844
+ previous = await atConnectionStage('credential.read', () => this.#credentials.resolve(current.secretRef), 'credential-store');
836
845
  } catch (error) {
837
846
  throw this.#callbackRepairError(
838
847
  record,
@@ -844,9 +853,9 @@ export class MultiBotDshFeishuController {
844
853
  if (previous?.value !== appSecret) {
845
854
  record.stage = 'persisting_secret';
846
855
  try {
847
- await this.#credentials.set(current.secretRef, appSecret);
856
+ await atConnectionStage('credential.save', () => this.#credentials.set(current.secretRef, appSecret), 'credential-store');
848
857
  } catch (writeError) {
849
- const observed = await this.#credentials.resolve(current.secretRef).catch(() => null);
858
+ const observed = await atConnectionStage('credential.read', () => this.#credentials.resolve(current.secretRef), 'credential-store').catch(() => null);
850
859
  if (observed?.value !== appSecret) {
851
860
  throw this.#callbackRepairError(
852
861
  record,
@@ -856,7 +865,7 @@ export class MultiBotDshFeishuController {
856
865
  );
857
866
  }
858
867
  }
859
- const persisted = await this.#credentials.resolve(current.secretRef).catch(() => null);
868
+ const persisted = await atConnectionStage('credential.read', () => this.#credentials.resolve(current.secretRef), 'credential-store').catch(() => null);
860
869
  if (persisted?.value !== appSecret) {
861
870
  throw this.#callbackRepairError(
862
871
  record,
@@ -887,10 +896,10 @@ export class MultiBotDshFeishuController {
887
896
  try {
888
897
  await this.#startRuntime(saved, appSecret);
889
898
  } catch (error) {
890
- this.#botErrors.set(record.botId, {
899
+ this.#botErrors.set(record.botId, this.#diagnostics.report(error, { reuse: true, botId: record.botId, stage: 'connection.start', publicError: {
891
900
  code: 'connection_failed',
892
901
  message: '群消息权限已开通,但机器人长连接未就绪,请点击重试。',
893
- });
902
+ } }).publicError);
894
903
  this.#touch();
895
904
  throw this.#callbackRepairError(
896
905
  record,
@@ -981,7 +990,7 @@ export class MultiBotDshFeishuController {
981
990
 
982
991
  let previous;
983
992
  try {
984
- previous = await this.#credentials.resolve(current.secretRef);
993
+ previous = await atConnectionStage('credential.read', () => this.#credentials.resolve(current.secretRef), 'credential-store');
985
994
  } catch (error) {
986
995
  throw this.#callbackRepairError(
987
996
  record,
@@ -994,9 +1003,9 @@ export class MultiBotDshFeishuController {
994
1003
  if (credentialChanged) {
995
1004
  record.stage = 'persisting_secret';
996
1005
  try {
997
- await this.#credentials.set(current.secretRef, appSecret);
1006
+ await atConnectionStage('credential.save', () => this.#credentials.set(current.secretRef, appSecret), 'credential-store');
998
1007
  } catch (writeError) {
999
- const observed = await this.#credentials.resolve(current.secretRef).catch(() => null);
1008
+ const observed = await atConnectionStage('credential.read', () => this.#credentials.resolve(current.secretRef), 'credential-store').catch(() => null);
1000
1009
  if (observed?.value !== appSecret) {
1001
1010
  throw this.#callbackRepairError(
1002
1011
  record,
@@ -1006,7 +1015,7 @@ export class MultiBotDshFeishuController {
1006
1015
  );
1007
1016
  }
1008
1017
  }
1009
- const persisted = await this.#credentials.resolve(current.secretRef).catch(() => null);
1018
+ const persisted = await atConnectionStage('credential.read', () => this.#credentials.resolve(current.secretRef), 'credential-store').catch(() => null);
1010
1019
  if (persisted?.value !== appSecret) {
1011
1020
  throw this.#callbackRepairError(
1012
1021
  record,
@@ -1029,10 +1038,10 @@ export class MultiBotDshFeishuController {
1029
1038
  // The returned credential was already verified and persisted. Do
1030
1039
  // not restore a potentially revoked old secret; reconnectBot can
1031
1040
  // safely retry this forward state later.
1032
- this.#botErrors.set(record.botId, {
1041
+ this.#botErrors.set(record.botId, this.#diagnostics.report(error, { reuse: true, botId: record.botId, publicError: {
1033
1042
  code: 'connection_failed',
1034
1043
  message: '机器人回调修复已保存,但长连接未就绪,请点击重试。',
1035
- });
1044
+ } }).publicError);
1036
1045
  this.#touch();
1037
1046
  throw this.#callbackRepairError(
1038
1047
  record,
@@ -1120,8 +1129,8 @@ export class MultiBotDshFeishuController {
1120
1129
  }
1121
1130
  const secretRef = existing?.secretRef ?? secretRefFor(botId);
1122
1131
  const previousOwnership = this.#botOwnership.get(botId);
1123
- const previousSecret = await this.#credentials.resolve(secretRef).catch(() => undefined);
1124
- await this.#credentials.set(secretRef, appSecret);
1132
+ const previousSecret = await atConnectionStage('credential.read', () => this.#credentials.resolve(secretRef), 'credential-store').catch(() => undefined);
1133
+ await atConnectionStage('credential.save', () => this.#credentials.set(secretRef, appSecret), 'credential-store');
1125
1134
  let config;
1126
1135
  try {
1127
1136
  config = await this.#configStore.saveBot({
@@ -1219,29 +1228,29 @@ export class MultiBotDshFeishuController {
1219
1228
  this.#botErrors.delete(botId);
1220
1229
  } else {
1221
1230
  await this.#withBotTransition(botId, () => this.#stopRuntime(botId));
1222
- this.#botErrors.set(botId, {
1231
+ this.#botErrors.set(botId, this.#diagnostics.report(error, { reuse: true, botId: botId, publicError: {
1223
1232
  code: 'deletion_pending',
1224
1233
  message: '机器人正在等待完成本地删除,请重试移除。',
1225
- });
1234
+ } }).publicError);
1226
1235
  }
1227
1236
  this.#touch();
1228
1237
  throw error;
1229
1238
  } catch (restoreError) {
1230
1239
  if (restoreError === error) throw error;
1231
- this.#botErrors.set(botId, {
1240
+ this.#botErrors.set(botId, this.#diagnostics.report(restoreError, { reuse: true, botId: botId, publicError: {
1232
1241
  code: 'connection_failed',
1233
1242
  message: '机器人连接更新失败,且原连接无法恢复,请重试。',
1234
- });
1243
+ } }).publicError);
1235
1244
  this.#touch();
1236
1245
  throw new Error('Unable to restore the previous Feishu bot connection.', {
1237
1246
  cause: restoreError,
1238
1247
  });
1239
1248
  }
1240
1249
  }
1241
- this.#botErrors.set(botId, {
1250
+ this.#botErrors.set(botId, this.#diagnostics.report(error, { reuse: true, botId: botId, publicError: {
1242
1251
  code: 'connection_failed',
1243
1252
  message: '机器人已经创建,但长连接未就绪,请点击重试。',
1244
- });
1253
+ } }).publicError);
1245
1254
  this.#touch();
1246
1255
  throw error;
1247
1256
  }
@@ -1249,12 +1258,12 @@ export class MultiBotDshFeishuController {
1249
1258
 
1250
1259
  async #startRuntime(config, appSecret) {
1251
1260
  await this.#stopRuntime(config.id);
1252
- const runtime = await this.#createRuntime({
1261
+ const runtime = await atConnectionStage('runtime.prepare', () => this.#createRuntime({
1253
1262
  botId: config.id,
1254
1263
  config,
1255
1264
  appSecret,
1256
1265
  repair: this.#runtimeRepairCapability(config.id),
1257
- });
1266
+ }));
1258
1267
  this.#runtimes.set(config.id, runtime);
1259
1268
  try {
1260
1269
  await runtime.start();
@@ -1300,21 +1309,21 @@ export class MultiBotDshFeishuController {
1300
1309
  }
1301
1310
  await this.#stopRuntime(botId);
1302
1311
  try {
1303
- await this.#credentials.unset(config.secretRef);
1312
+ await atConnectionStage('credential.remove', () => this.#credentials.unset(config.secretRef), 'credential-store');
1304
1313
  } catch (error) {
1305
- this.#botErrors.set(botId, {
1314
+ this.#botErrors.set(botId, this.#diagnostics.report(error, { reuse: true, botId: botId, publicError: {
1306
1315
  code: 'credential_removal_failed',
1307
1316
  message: '无法删除机器人凭据,请稍后重试。',
1308
- });
1317
+ } }).publicError);
1309
1318
  throw new Error('Unable to remove the Feishu credential.', { cause: error });
1310
1319
  }
1311
1320
  try {
1312
1321
  await this.#deleteState({ botId, config });
1313
1322
  } catch (error) {
1314
- this.#botErrors.set(botId, {
1323
+ this.#botErrors.set(botId, this.#diagnostics.report(error, { reuse: true, botId: botId, publicError: {
1315
1324
  code: 'state_cleanup_failed',
1316
1325
  message: '无法删除机器人的本地会话数据,请稍后重试。',
1317
- });
1326
+ } }).publicError);
1318
1327
  throw new Error('Unable to remove the Feishu bot session state.', { cause: error });
1319
1328
  }
1320
1329
  await this.#configStore.removeBot(botId);
@@ -1323,8 +1332,8 @@ export class MultiBotDshFeishuController {
1323
1332
  }
1324
1333
 
1325
1334
  async #restoreCredential(secretRef, previous) {
1326
- if (previous?.value) await this.#credentials.set(secretRef, previous.value);
1327
- else await this.#credentials.unset(secretRef);
1335
+ if (previous?.value) await atConnectionStage('credential.save', () => this.#credentials.set(secretRef, previous.value), 'credential-store');
1336
+ else await atConnectionStage('credential.remove', () => this.#credentials.unset(secretRef), 'credential-store');
1328
1337
  }
1329
1338
 
1330
1339
  #requireBot(botId) {
@@ -1,3 +1,4 @@
1
+ import { createConnectionDiagnostics } from '../shared/connection-error.mjs';
1
2
  const ACTIVE_STATES = new Set([
2
3
  'starting',
3
4
  'qr_ready',
@@ -74,6 +75,7 @@ function copyUserInfo(userInfo) {
74
75
  * `onCredentials` callback.
75
76
  */
76
77
  export class RegistrationManager {
78
+ #diagnostics;
77
79
  #registerApp;
78
80
  #onCredentials;
79
81
  #now;
@@ -86,6 +88,7 @@ export class RegistrationManager {
86
88
  constructor({
87
89
  registerApp,
88
90
  onCredentials,
91
+ diagnostics = createConnectionDiagnostics({ channel: 'feishu' }),
89
92
  now = Date.now,
90
93
  setTimeout: setTimeoutFn = globalThis.setTimeout,
91
94
  clearTimeout: clearTimeoutFn = globalThis.clearTimeout,
@@ -100,6 +103,7 @@ export class RegistrationManager {
100
103
  throw new TypeError('RegistrationManager clock dependencies must be functions');
101
104
  }
102
105
 
106
+ this.#diagnostics = diagnostics;
103
107
  this.#registerApp = registerApp;
104
108
  this.#onCredentials = onCredentials;
105
109
  this.#now = now;
@@ -263,13 +267,13 @@ export class RegistrationManager {
263
267
  client_secret: clientSecret,
264
268
  user_info: userInfo,
265
269
  });
266
- } catch {
270
+ } catch (error) {
267
271
  if (this.#isCurrent(run)) {
268
272
  this.#finishRun(run, REGISTRATION_STATES.ERROR, {
269
- error: {
273
+ error: this.#diagnostics.report(error, { operation: 'provision.poll', stage: 'activation', publicError: {
270
274
  code: 'credentials_callback_failed',
271
275
  message: 'Unable to store the Feishu credentials.',
272
- },
276
+ } }).publicError,
273
277
  });
274
278
  }
275
279
  return;
@@ -297,7 +301,7 @@ export class RegistrationManager {
297
301
  return;
298
302
  }
299
303
  this.#finishRun(run, REGISTRATION_STATES.ERROR, {
300
- error: publicError(error),
304
+ error: this.#diagnostics.report(error, { operation: run.qrCodeUrl ? 'provision.poll' : 'provision.begin', publicError: publicError(error) }).publicError,
301
305
  });
302
306
  }
303
307
 
@@ -85,7 +85,7 @@ export class CallbackRepairManager {
85
85
  #appId;
86
86
  #domain;
87
87
 
88
- constructor({ registerApp, onCredentials, appId, domain = 'feishu' } = {}) {
88
+ constructor({ registerApp, onCredentials, appId, domain = 'feishu', diagnostics } = {}) {
89
89
  if (typeof registerApp !== 'function') throw new TypeError('registerApp is required');
90
90
  if (typeof onCredentials !== 'function') throw new TypeError('onCredentials is required');
91
91
  if (typeof appId !== 'string' || !appId.trim()) throw new TypeError('appId is required');
@@ -94,6 +94,7 @@ export class CallbackRepairManager {
94
94
  this.#appId = appId.trim();
95
95
  this.#domain = domain;
96
96
  this.#manager = new RegistrationManager({
97
+ diagnostics,
97
98
  registerApp: (options) => registerApp({
98
99
  ...options,
99
100
  onQRCodeReady: (info) => {
@@ -50,6 +50,23 @@ export const SLASH_COMMAND_MANIFEST = Object.freeze([
50
50
  { command: 'unwatch', icon: 'clear_outlined', default: '取消关注会话', en_us: 'Unwatch a session' },
51
51
  { command: 'watchlist', icon: 'flag_outlined', default: '查看关注列表', en_us: 'List watched sessions' },
52
52
  { command: 'archived', icon: 'folder_outlined', default: '设置归档会话显隐(on/off)', en_us: 'Show or hide archived sessions (on/off)' },
53
+ { command: 'history', icon: 'chat-ai_outlined', default: '查看最近历史消息(仅私聊)', en_us: 'Show recent history (private chats only)' },
54
+ { command: 'workspace', icon: 'folder_outlined', default: '切换工作区', en_us: 'Switch workspace' },
55
+ { command: 'conv', icon: 'folder_outlined', default: '设置当前对话专属工作区', en_us: 'Set the workspace for this conversation' },
56
+ { command: 'session', icon: 'chat-ai_outlined', default: '绑定已有会话', en_us: 'Bind an existing session' },
57
+ { command: 'models', icon: 'ai-functions_outlined', default: '列出可用模型', en_us: 'List available models' },
58
+ { command: 'model', icon: 'ai-agent_outlined', default: '查看或切换当前模型', en_us: 'Show or switch the current model' },
59
+ { command: 'reasoninglist', icon: 'ai-deepthink_outlined', default: '列出可用推理等级', en_us: 'List available reasoning efforts' },
60
+ { command: 'reasoning', icon: 'ai-deepthink_outlined', default: '查看或切换推理等级', en_us: 'Show or switch the reasoning effort' },
61
+ { command: 'presetlist', icon: 'skill_outlined', default: '列出可用 Agent 预设', en_us: 'List available Agent Presets' },
62
+ { command: 'preset', icon: 'skill_outlined', default: '查看或切换 Agent 预设', en_us: 'Show or switch the Agent Preset' },
63
+ { command: 'stop', icon: 'clear_outlined', default: '停止当前任务', en_us: 'Stop the current task' },
64
+ { command: 'steer', icon: 'promptword_outlined', default: '给当前任务补充指令', en_us: 'Send additional instructions to the current task' },
65
+ { command: 'batch', icon: 'chat-ai_outlined', default: '开始批量输入(仅私聊)', en_us: 'Start batch input (private chats only)' },
66
+ { command: 'send', icon: 'chat-ai_outlined', default: '提交当前批次(仅私聊)', en_us: 'Submit the current batch (private chats only)' },
67
+ { command: 'cancel', icon: 'clear_outlined', default: '取消当前批次(仅私聊)', en_us: 'Cancel the current batch (private chats only)' },
68
+ { command: 'version', icon: 'ai-functions_outlined', default: '查看插件版本', en_us: 'Show the plugin version' },
69
+ { command: 'repair', icon: 'ai-functions_outlined', default: '补全飞书权限与卡片回调(仅私聊)', en_us: 'Complete Feishu permissions and card callbacks (private chats only)' },
53
70
  ]);
54
71
 
55
72
  // Commands that require a parameter are registered too, so the user can type
@@ -1,6 +1,7 @@
1
1
  export const FEISHU_STEP_PUSH_MODES = Object.freeze({
2
2
  POST: 'post',
3
3
  STREAMING_CARD: 'streaming_card',
4
+ LIVE_COT: 'live_cot',
4
5
  });
5
6
 
6
7
  /** New connections explicitly opt into the process-card presentation. */
@@ -8,12 +9,17 @@ export const DEFAULT_FEISHU_STEP_PUSH_MODE = FEISHU_STEP_PUSH_MODES.STREAMING_CA
8
9
 
9
10
  export function normalizeFeishuStepPushMode(value) {
10
11
  // Bots created before modes existed used posts when step push was enabled.
11
- return value === FEISHU_STEP_PUSH_MODES.STREAMING_CARD
12
- ? FEISHU_STEP_PUSH_MODES.STREAMING_CARD
13
- : FEISHU_STEP_PUSH_MODES.POST;
12
+ if (value === FEISHU_STEP_PUSH_MODES.STREAMING_CARD) {
13
+ return FEISHU_STEP_PUSH_MODES.STREAMING_CARD;
14
+ }
15
+ if (value === FEISHU_STEP_PUSH_MODES.LIVE_COT) {
16
+ return FEISHU_STEP_PUSH_MODES.LIVE_COT;
17
+ }
18
+ return FEISHU_STEP_PUSH_MODES.POST;
14
19
  }
15
20
 
16
21
  export function isFeishuStepPushMode(value) {
17
22
  return value === FEISHU_STEP_PUSH_MODES.POST
18
- || value === FEISHU_STEP_PUSH_MODES.STREAMING_CARD;
23
+ || value === FEISHU_STEP_PUSH_MODES.STREAMING_CARD
24
+ || value === FEISHU_STEP_PUSH_MODES.LIVE_COT;
19
25
  }
@@ -1,3 +1,4 @@
1
+ import { createConnectionDiagnostics, atConnectionStage } from '../shared/connection-error.mjs';
1
2
  import { sendRememberedConnectionTest } from '../shared/connection-test.mjs';
2
3
  import { MacOSMessagesApi, normalizeIMessage } from './imessage-api.mjs';
3
4
  import { createIMessageBridgeStatus, IMessageHarnessBridge } from './imessage-bridge.mjs';
@@ -26,6 +27,7 @@ export function createIMessageRuntimeStatus() {
26
27
 
27
28
  export class IMessageRuntime {
28
29
  #config; #token; #harness; #state; #contextEnhancement; #accessPolicy; #logger;
30
+ #diagnostics;
29
31
  #replyTimeoutMs; #pollIntervalMs; #createApi; #status = createIMessageRuntimeStatus();
30
32
  #api; #bridge; #abortController; #timer; #polling; #stopped = true;
31
33
  constructor({ config, token, harness, state, contextEnhancement, accessPolicy, logger = console,
@@ -33,7 +35,7 @@ export class IMessageRuntime {
33
35
  createApi = (options) => new MacOSMessagesApi(options) }) {
34
36
  if (!config || !token || !harness || !state) throw new TypeError('IMessageRuntime requires config, token, Harness, and state');
35
37
  this.#config = config; this.#token = token; this.#harness = harness; this.#state = state;
36
- this.#contextEnhancement = contextEnhancement; this.#accessPolicy = accessPolicy; this.#logger = logger;
38
+ this.#contextEnhancement = contextEnhancement; this.#accessPolicy = accessPolicy; this.#logger = logger; this.#diagnostics = createConnectionDiagnostics({ channel: 'imessage', logger });
37
39
  this.#replyTimeoutMs = replyTimeoutMs; this.#pollIntervalMs = pollIntervalMs; this.#createApi = createApi;
38
40
  }
39
41
  get status() { return structuredClone(this.#status); }
@@ -60,7 +62,7 @@ export class IMessageRuntime {
60
62
  await this.stop(); this.#stopped = false; this.#status.startedAt = new Date().toISOString(); this.#status.connectionState = 'connecting';
61
63
  this.#abortController = new AbortController();
62
64
  try {
63
- await this.#harness.ensureRunning(); this.#status.harnessReachable = true;
65
+ await atConnectionStage('harness.check', () => this.#harness.ensureRunning()); this.#status.harnessReachable = true;
64
66
  this.#api = this.#createApi({ signal: this.#abortController.signal });
65
67
  const permissions = await this.#api.getPermissions();
66
68
  if (permissions.database !== 'granted' || permissions.automation !== 'granted') {
@@ -78,7 +80,8 @@ export class IMessageRuntime {
78
80
  logger: this.#logger, replyTimeoutMs: this.#replyTimeoutMs, signal: this.#abortController.signal });
79
81
  this.#status.ready = true; this.#status.connectionState = 'connected'; this.#status.lastConnectedAt = Date.now();
80
82
  this.#schedulePoll(0); return this.status;
81
- } catch (error) { this.#status.ready = false; this.#status.connectionState = 'failed'; this.#status.lastError = error.message; await this.stop(); throw error; }
83
+ } catch (error) { this.#status.ready = false; this.#status.connectionState = 'failed'; this.#status.error = this.#diagnostics.report(error, { operation: 'connection.restore', reuse: true, botId: this.#config?.botId, automatic: true }).publicError;
84
+ this.#status.lastError = this.#status.error.message; await this.stop(); throw error; }
82
85
  }
83
86
  async stop() { this.#stopped = true; clearTimeout(this.#timer); this.#timer = null; this.#abortController?.abort(); await this.#polling?.catch(() => {}); this.#polling = null; this.#bridge = null; this.#api = null; this.#status.ready = false; }
84
87
  #schedulePoll(delay) { if (this.#stopped) return; this.#timer = setTimeout(() => { this.#polling = this.#poll().finally(() => this.#schedulePoll(this.#pollIntervalMs)); }, delay); this.#timer.unref?.(); }
@@ -92,7 +95,8 @@ export class IMessageRuntime {
92
95
  if (message) await this.#bridge.accept(message);
93
96
  if (Number.isSafeInteger(raw.rowid)) await this.#state.setCursor(raw.rowid);
94
97
  }
95
- this.#status.lastCheckedAt = Date.now(); this.#status.lastError = null;
96
- } catch (error) { if (!this.#stopped) { this.#status.lastError = error.message; this.#logger.warn?.('[dsh-im:imessage] polling failed', error); } }
98
+ this.#status.lastCheckedAt = Date.now(); this.#status.lastError = null; this.#status.error = null; this.#diagnostics.clear();
99
+ } catch (error) { if (!this.#stopped) { this.#status.error = this.#diagnostics.report(error, { operation: 'connection.monitor', botId: this.#config?.botId, automatic: true }).publicError;
100
+ this.#status.lastError = this.#status.error.message; } }
97
101
  }
98
102
  }
@@ -1,3 +1,4 @@
1
+ import { atConnectionStage, createConnectionDiagnostics } from '../shared/connection-error.mjs';
1
2
  import { createHash } from 'node:crypto';
2
3
 
3
4
  import { normalizeOfficeBaseUrl, officeHookUrls } from './protocol.mjs';
@@ -15,6 +16,7 @@ export class OfficeController {
15
16
  #credentials;
16
17
  #store;
17
18
  #logger;
19
+ #diagnostics;
18
20
  #createRuntime;
19
21
  #runtime = null;
20
22
  #transition = Promise.resolve();
@@ -29,13 +31,16 @@ export class OfficeController {
29
31
  this.#credentials = credentials;
30
32
  this.#store = configStore;
31
33
  this.#logger = logger;
34
+ this.#diagnostics = createConnectionDiagnostics({ channel: 'office', logger });
32
35
  this.#createRuntime = createRuntime ?? ((options) => new OfficeRuntime(options));
33
36
  }
34
37
 
38
+ get diagnostics() { return this.#diagnostics; }
39
+
35
40
  async initialize() {
36
41
  const config = this.#store.get();
37
42
  if (config) {
38
- const credential = await this.#credentials.resolve(config.deviceTokenRef).catch(() => undefined);
43
+ const credential = await atConnectionStage('credential.read', () => this.#credentials.resolve(config.deviceTokenRef), 'credential-store');
39
44
  if (credential?.value) await this.#start(config, credential.value);
40
45
  }
41
46
  return this.status();
@@ -50,7 +55,7 @@ export class OfficeController {
50
55
  const baseUrl = normalizeOfficeBaseUrl(requestedBaseUrl).origin;
51
56
  const tokenRef = officeTokenRef(baseUrl, deviceId);
52
57
  const suppliedToken = clean(input.deviceToken);
53
- const priorCredential = await this.#credentials.resolve(tokenRef).catch(() => undefined);
58
+ const priorCredential = await atConnectionStage('credential.read', () => this.#credentials.resolve(tokenRef), 'credential-store');
54
59
  const token = suppliedToken ?? priorCredential?.value;
55
60
  if (!token || token.length < 32) throw new TypeError('Device Token must contain at least 32 characters');
56
61
  const now = new Date().toISOString();
@@ -68,16 +73,16 @@ export class OfficeController {
68
73
  });
69
74
  if (!config) throw new TypeError('AI Office connector configuration is invalid');
70
75
 
71
- await this.#credentials.set(tokenRef, token);
76
+ await atConnectionStage('credential.save', () => this.#credentials.set(tokenRef, token), 'credential-store');
72
77
  try {
73
- await this.#store.save(config);
78
+ await atConnectionStage('account.save', () => this.#store.save(config), 'account-config');
74
79
  } catch (error) {
75
- if (priorCredential?.value) await this.#credentials.set(tokenRef, priorCredential.value).catch(() => undefined);
76
- else await this.#credentials.unset(tokenRef).catch(() => undefined);
80
+ if (priorCredential?.value) await atConnectionStage('credential.save', () => this.#credentials.set(tokenRef, priorCredential.value), 'credential-store').catch(() => undefined);
81
+ else await atConnectionStage('credential.remove', () => this.#credentials.unset(tokenRef), 'credential-store').catch(() => undefined);
77
82
  throw error;
78
83
  }
79
84
  if (previous?.deviceTokenRef && previous.deviceTokenRef !== tokenRef) {
80
- await this.#credentials.unset(previous.deviceTokenRef).catch(() => undefined);
85
+ await atConnectionStage('credential.remove', () => this.#credentials.unset(previous.deviceTokenRef), 'credential-store');
81
86
  }
82
87
  await this.#start(config, token);
83
88
  return this.status();
@@ -104,18 +109,24 @@ export class OfficeController {
104
109
 
105
110
  async remove() {
106
111
  return this.#serial(async () => {
112
+ const warnings = [];
107
113
  const config = this.#store.get();
108
114
  await this.#stop();
109
- if (config?.deviceTokenRef) await this.#credentials.unset(config.deviceTokenRef).catch(() => undefined);
110
- await this.#store.clear();
111
- return this.status();
115
+ await atConnectionStage('account.remove', () => this.#store.clear(), 'account-config');
116
+ if (config?.deviceTokenRef) {
117
+ await atConnectionStage('credential.remove', () => this.#credentials.unset(config.deviceTokenRef), 'credential-store').catch(error => {
118
+ warnings.push(this.#diagnostics.report(error, { reuse: true, operation: 'connector.remove', warning: true,
119
+ publicError: { code: 'cleanup-failed', message: '连接已移除,但登录凭据清理失败。' } }).publicError);
120
+ });
121
+ }
122
+ return { ...await this.status(), ...(warnings.length ? { warnings } : {}) };
112
123
  });
113
124
  }
114
125
 
115
126
  async status() {
116
127
  const config = this.#store.get();
117
128
  if (!config) return { schemaVersion: 1, configured: false, connected: false, state: 'unconfigured' };
118
- const credential = await this.#credentials.resolve(config.deviceTokenRef).catch(() => undefined);
129
+ const credential = await atConnectionStage('credential.read', () => this.#credentials.resolve(config.deviceTokenRef), 'credential-store');
119
130
  const runtime = this.#runtime?.status ?? null;
120
131
  return {
121
132
  schemaVersion: 1,
@@ -140,7 +151,7 @@ export class OfficeController {
140
151
  async close() { await this.#transition.catch(() => undefined); await this.#stop(); }
141
152
 
142
153
  async #resolveToken(config) {
143
- const credential = await this.#credentials.resolve(config.deviceTokenRef).catch(() => undefined);
154
+ const credential = await atConnectionStage('credential.read', () => this.#credentials.resolve(config.deviceTokenRef), 'credential-store');
144
155
  if (!credential?.value) throw new Error('AI Office Device Token is missing');
145
156
  return credential.value;
146
157
  }