@xmanrui/dsh-im 2.3.0 → 2.5.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 (57) hide show
  1. package/README.en.md +9 -4
  2. package/README.md +9 -4
  3. package/lib/client.js +411 -156
  4. package/lib/index.js +218 -210
  5. package/package.json +1 -1
  6. package/plugin-src/client/channel-card-meta.js +36 -1
  7. package/plugin-src/client/channels/dingtalk/api.js +2 -0
  8. package/plugin-src/client/channels/dingtalk/index.js +9 -1
  9. package/plugin-src/client/channels/feishu/api.js +3 -0
  10. package/plugin-src/client/channels/feishu/index.js +46 -28
  11. package/plugin-src/client/channels/feishu/styles.js +26 -1
  12. package/plugin-src/client/channels/qq/api.js +2 -0
  13. package/plugin-src/client/channels/qq/index.js +9 -1
  14. package/plugin-src/client/channels/shared/token-api.js +2 -0
  15. package/plugin-src/client/channels/shared/token-channel.js +9 -1
  16. package/plugin-src/client/channels/wecom/api.js +2 -0
  17. package/plugin-src/client/channels/wecom/index.js +9 -1
  18. package/plugin-src/client/channels/weixin/api.js +2 -10
  19. package/plugin-src/client/channels/weixin/index.js +9 -5
  20. package/plugin-src/client/channels/whatsapp/api.js +2 -0
  21. package/plugin-src/client/channels/whatsapp/index.js +9 -1
  22. package/plugin-src/client/i18n.js +31 -25
  23. package/plugin-src/client/index.js +16 -2
  24. package/plugin-src/client/last-message-error.js +17 -0
  25. package/plugin-src/client/styles.js +5 -1
  26. package/plugin-src/host/channels/feishu/rpc.mjs +3 -0
  27. package/src/channels/dingtalk/dingtalk-api.mjs +1 -1
  28. package/src/channels/dingtalk/dingtalk-bridge.mjs +50 -16
  29. package/src/channels/dingtalk/dingtalk-card-stream.mjs +2 -2
  30. package/src/channels/dingtalk/dingtalk-controller.mjs +2 -0
  31. package/src/channels/feishu/bridge.mjs +148 -70
  32. package/src/channels/feishu/feishu-cards.mjs +10 -6
  33. package/src/channels/feishu/feishu-runtime.mjs +0 -1
  34. package/src/channels/feishu/message-utils.mjs +1 -1
  35. package/src/channels/feishu/multi-bot-controller.mjs +3 -17
  36. package/src/channels/feishu/repair-manager.mjs +7 -2
  37. package/src/channels/qq/qq-bridge.mjs +84 -28
  38. package/src/channels/qq/qq-controller.mjs +2 -0
  39. package/src/channels/shared/control-command.mjs +11 -1
  40. package/src/channels/shared/harness-client.mjs +40 -4
  41. package/src/channels/shared/i18n-en/feishu.mjs +27 -25
  42. package/src/channels/shared/i18n-en/shared-a.mjs +78 -0
  43. package/src/channels/shared/i18n-en/shared-b.mjs +1 -0
  44. package/src/channels/shared/i18n-en/telegram.mjs +1 -0
  45. package/src/channels/shared/message-failure.mjs +244 -0
  46. package/src/channels/shared/semantic/artifact-delivery.mjs +9 -2
  47. package/src/channels/shared/text-harness-bridge.mjs +66 -60
  48. package/src/channels/shared/token-bot-controller.mjs +2 -0
  49. package/src/channels/slack/slack-controller.mjs +2 -0
  50. package/src/channels/telegram/telegram-runtime.mjs +1 -0
  51. package/src/channels/wecom/wecom-bridge.mjs +50 -15
  52. package/src/channels/wecom/wecom-controller.mjs +2 -0
  53. package/src/channels/weixin/weixin-api.mjs +47 -0
  54. package/src/channels/weixin/weixin-bridge.mjs +262 -43
  55. package/src/channels/weixin/weixin-controller.mjs +2 -15
  56. package/src/channels/weixin/weixin-runtime.mjs +1 -0
  57. package/src/channels/whatsapp/whatsapp-controller.mjs +2 -0
@@ -29,6 +29,7 @@ import { askInWorkspaceSession } from '../shared/workspace-session.mjs';
29
29
  import {
30
30
  hasInboundImages,
31
31
  ImagePromptError,
32
+ imagePromptDiagnostic,
32
33
  imagePromptUserMessage,
33
34
  promptContentForMessage,
34
35
  } from '../shared/image-prompt.mjs';
@@ -42,6 +43,12 @@ import { deliverOutboundArtifacts } from '../shared/semantic/artifact-delivery.m
42
43
  import {
43
44
  createDeliveryReceipt,
44
45
  } from '../shared/semantic/delivery.mjs';
46
+ import {
47
+ channelDeliveryFailure,
48
+ clearLastMessageFailure,
49
+ messageFailureText,
50
+ setLastMessageFailure,
51
+ } from '../shared/message-failure.mjs';
45
52
  import { t } from '../shared/i18n.mjs';
46
53
 
47
54
  const DEFAULT_FILE_UPLOAD_TIMEOUT_MS = 120_000;
@@ -73,6 +80,7 @@ function helpText() {
73
80
  t('/send 提交当前批次'),
74
81
  t('/cancel 取消当前批次'),
75
82
  t('/status 检查连接状态'),
83
+ t('/version 查看插件版本'),
76
84
  t('/help 显示本帮助'),
77
85
  ].join('\n');
78
86
  }
@@ -459,6 +467,7 @@ export function createWecomBridgeStatus() {
459
467
  lastReplyAt: null,
460
468
  lastRejectedAt: null,
461
469
  lastError: null,
470
+ lastMessageError: null,
462
471
  };
463
472
  }
464
473
 
@@ -588,8 +597,11 @@ export class WecomHarnessBridge {
588
597
  ).catch((error) => {
589
598
  if (error?.code === 'turn-stopped' || this.#signal?.aborted) return;
590
599
  this.#status.lastError = error?.message ?? String(error);
591
- this.#logger.error?.('[dsh-im:wecom] failed to process a command');
592
- return this.#sendImmediate(frame, chatId, t('消息处理失败,请稍后重试。'))
600
+ const failure = setLastMessageFailure(this.#status, error);
601
+ this.#logger.error?.(
602
+ `[dsh-im:wecom] failed to process a command [${failure.referenceId}]`,
603
+ );
604
+ return this.#sendImmediate(frame, chatId, messageFailureText(failure))
593
605
  .catch(() => undefined);
594
606
  }).finally(() => {
595
607
  this.#acceptedMessageIds.delete(messageId);
@@ -712,8 +724,11 @@ export class WecomHarnessBridge {
712
724
  }).catch(async (error) => {
713
725
  if (this.#signal?.aborted) return;
714
726
  this.#status.lastError = error?.message ?? String(error);
715
- this.#logger.error?.('[dsh-im:wecom] failed to process a batch input message');
716
- await this.#sendImmediate(frame, chatId, t('消息处理失败,请稍后重试。'))
727
+ const failure = setLastMessageFailure(this.#status, error);
728
+ this.#logger.error?.(
729
+ `[dsh-im:wecom] failed to process a batch input message [${failure.referenceId}]`,
730
+ );
731
+ await this.#sendImmediate(frame, chatId, messageFailureText(failure))
717
732
  .catch(() => undefined);
718
733
  }).finally(() => {
719
734
  this.#acceptedMessageIds.delete(messageId);
@@ -790,7 +805,7 @@ export class WecomHarnessBridge {
790
805
 
791
806
  async #deliverArtifacts(chatId, replyTo, artifacts = [], baseReceipt = null) {
792
807
  if (artifacts.length === 0) {
793
- return { receipt: baseReceipt, failureNoticeVisible: false };
808
+ return { receipt: baseReceipt, failureNoticeVisible: false, artifactSendErrors: 0 };
794
809
  }
795
810
  const delivery = await deliverOutboundArtifacts({
796
811
  artifacts,
@@ -808,9 +823,13 @@ export class WecomHarnessBridge {
808
823
  signal: this.#signal,
809
824
  timeoutMs: this.#fileUploadTimeoutMs,
810
825
  }),
811
- sendFailureNotice: (artifact, error) => this.#sendActive(
826
+ onFailure: (artifact, error) => setLastMessageFailure(this.#status, error, {
827
+ userMessage: artifactFailureText(artifact?.fileName, error),
828
+ reason: error?.code,
829
+ }),
830
+ sendFailureNotice: (_artifact, _error, failure) => this.#sendActive(
812
831
  chatId,
813
- artifactFailureText(artifact?.fileName, error),
832
+ messageFailureText(failure),
814
833
  ),
815
834
  logger: this.#logger,
816
835
  });
@@ -821,6 +840,7 @@ export class WecomHarnessBridge {
821
840
  return {
822
841
  receipt: delivery.receipt,
823
842
  failureNoticeVisible: delivery.failureNoticeVisible,
843
+ artifactSendErrors: delivery.artifactSendErrors,
824
844
  };
825
845
  }
826
846
 
@@ -848,6 +868,7 @@ export class WecomHarnessBridge {
848
868
  let streamId = null;
849
869
  let streamStarted = false;
850
870
  let batchSettled = batchSubmission === null;
871
+ let promptRecorded = false;
851
872
  try {
852
873
  if (!text && !hasImages && !hasFiles) {
853
874
  await this.#sendImmediate(frame, chatId, t('目前支持文字、图片、文件和语音转写消息。'));
@@ -908,6 +929,8 @@ export class WecomHarnessBridge {
908
929
  const content = hasImages
909
930
  ? await promptContentForMessage(message, { signal: this.#signal })
910
931
  : undefined;
932
+ await this.#state.markSeen(messageId);
933
+ promptRecorded = true;
911
934
  const { answer, artifacts = [] } = await askInWorkspaceSession({
912
935
  harness: this.#harness,
913
936
  state: this.#state,
@@ -981,7 +1004,7 @@ export class WecomHarnessBridge {
981
1004
  });
982
1005
  }
983
1006
  } catch (error) {
984
- textSendError = error;
1007
+ textSendError = channelDeliveryFailure(error);
985
1008
  this.#logger.warn?.(
986
1009
  '[dsh-im:wecom] final text delivery failed; continuing with result files:',
987
1010
  error,
@@ -994,10 +1017,16 @@ export class WecomHarnessBridge {
994
1017
  if (textSendError && !artifactDispatched && !delivery.failureNoticeVisible) {
995
1018
  throw textSendError;
996
1019
  }
997
- await this.#state.markSeen(messageId);
1020
+ if (textSendError && delivery.artifactSendErrors === 0) {
1021
+ setLastMessageFailure(this.#status, textSendError);
1022
+ }
1023
+ if (!promptRecorded) await this.#state.markSeen(messageId);
998
1024
  this.#status.messagesReplied += 1;
999
1025
  this.#status.lastReplyAt = new Date().toISOString();
1000
1026
  this.#status.lastError = null;
1027
+ if (!textSendError && delivery.artifactSendErrors === 0) {
1028
+ clearLastMessageFailure(this.#status);
1029
+ }
1001
1030
  return delivery.receipt;
1002
1031
  } catch (error) {
1003
1032
  let batchFailureMessage = null;
@@ -1014,15 +1043,21 @@ export class WecomHarnessBridge {
1014
1043
  await this.#client.replyStream(frame, streamId, t('已停止。'), true)
1015
1044
  .catch(() => undefined);
1016
1045
  }
1017
- await this.#state.markSeen(messageId);
1046
+ if (!promptRecorded) await this.#state.markSeen(messageId);
1018
1047
  return;
1019
1048
  }
1020
1049
  if (this.#signal?.aborted) return;
1021
1050
  this.#status.lastError = error?.message ?? String(error);
1022
- this.#logger.error?.('[dsh-im:wecom] failed to process an inbound message');
1023
- const errorText = inboundFileUserMessage(error)
1024
- ?? imagePromptUserMessage(error)
1025
- ?? t('消息处理失败,请稍后重试。');
1051
+ const userMessage = inboundFileUserMessage(error)
1052
+ ?? imagePromptUserMessage(error);
1053
+ const failure = setLastMessageFailure(this.#status, error, {
1054
+ userMessage,
1055
+ reason: imagePromptDiagnostic(error)?.reason,
1056
+ });
1057
+ this.#logger.error?.(
1058
+ `[dsh-im:wecom] failed to process an inbound message [${failure.referenceId}]`,
1059
+ );
1060
+ const errorText = messageFailureText(failure);
1026
1061
  const visibleError = batchFailureMessage
1027
1062
  ? `${errorText}\n\n${batchFailureMessage}`
1028
1063
  : errorText;
@@ -1032,7 +1067,7 @@ export class WecomHarnessBridge {
1032
1067
  } else {
1033
1068
  await this.#sendImmediate(frame, chatId, visibleError);
1034
1069
  }
1035
- await this.#state.markSeen(messageId);
1070
+ if (!promptRecorded) await this.#state.markSeen(messageId);
1036
1071
  } catch {
1037
1072
  this.#logger.error?.('[dsh-im:wecom] failed to send the safe error reply');
1038
1073
  }
@@ -6,6 +6,7 @@ import {
6
6
  connectionTestTargetUnavailable,
7
7
  } from '../shared/connection-test.mjs';
8
8
  import { t } from '../shared/i18n.mjs';
9
+ import { publicMessageFailure } from '../shared/message-failure.mjs';
9
10
 
10
11
  const ACTIVE_ATTEMPT_STATES = new Set(['pending', 'connecting']);
11
12
  const TERMINAL_ATTEMPT_STATES = new Set(['connected', 'failed', 'cancelled', 'expired']);
@@ -308,6 +309,7 @@ export class WecomController {
308
309
  messagesReceived: runtimeStatus?.messagesReceived ?? 0,
309
310
  messagesReplied: runtimeStatus?.messagesReplied ?? 0,
310
311
  },
312
+ lastMessageError: publicMessageFailure(runtimeStatus?.lastMessageError),
311
313
  error: structuredClone(this.#errors.get(config.botId) ?? null),
312
314
  };
313
315
  });
@@ -637,6 +637,53 @@ export function createWeixinApi({ fetchImpl = fetch } = {}) {
637
637
  }
638
638
  },
639
639
 
640
+ async getConfig({ baseUrl, token, toUserId, contextToken, signal }) {
641
+ const recipient = nonEmptyString(toUserId);
642
+ if (!recipient) throw new TypeError('toUserId is required');
643
+ const response = await requestJson(fetchImpl, {
644
+ method: 'POST',
645
+ baseUrl,
646
+ endpoint: 'ilink/bot/getconfig',
647
+ token,
648
+ signal,
649
+ timeoutMs: 10_000,
650
+ body: {
651
+ ilink_user_id: recipient,
652
+ ...(nonEmptyString(contextToken) ? { context_token: contextToken.trim() } : {}),
653
+ base_info: baseInfo(),
654
+ },
655
+ });
656
+ if (response?.ret !== undefined && response.ret !== 0) {
657
+ throw new WeixinApiError('config-rejected', '微信服务拒绝了机器人配置请求。');
658
+ }
659
+ return { typingTicket: nonEmptyString(response?.typing_ticket) };
660
+ },
661
+
662
+ async sendTyping({ baseUrl, token, toUserId, typingTicket, status, signal }) {
663
+ const recipient = nonEmptyString(toUserId);
664
+ const ticket = nonEmptyString(typingTicket);
665
+ if (!recipient || !ticket) throw new TypeError('toUserId and typingTicket are required');
666
+ if (status !== 1 && status !== 2) throw new TypeError('typing status must be 1 or 2');
667
+ const response = await requestJson(fetchImpl, {
668
+ method: 'POST',
669
+ baseUrl,
670
+ endpoint: 'ilink/bot/sendtyping',
671
+ token,
672
+ signal,
673
+ timeoutMs: 10_000,
674
+ body: {
675
+ ilink_user_id: recipient,
676
+ typing_ticket: ticket,
677
+ status,
678
+ base_info: baseInfo(),
679
+ },
680
+ });
681
+ if (response?.ret !== undefined && response.ret !== 0) {
682
+ throw new WeixinApiError('typing-rejected', '微信服务拒绝了输入状态请求。');
683
+ }
684
+ return true;
685
+ },
686
+
640
687
  async sendText({ baseUrl, token, toUserId, text, contextToken, runId, signal }) {
641
688
  const recipient = nonEmptyString(toUserId);
642
689
  const content = nonEmptyString(text);