@xmanrui/dsh-im 2.4.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 (47) hide show
  1. package/README.en.md +1 -1
  2. package/README.md +1 -1
  3. package/lib/client.js +321 -109
  4. package/lib/index.js +214 -207
  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 +9 -1
  11. package/plugin-src/client/channels/qq/api.js +2 -0
  12. package/plugin-src/client/channels/qq/index.js +9 -1
  13. package/plugin-src/client/channels/shared/token-api.js +2 -0
  14. package/plugin-src/client/channels/shared/token-channel.js +9 -1
  15. package/plugin-src/client/channels/wecom/api.js +2 -0
  16. package/plugin-src/client/channels/wecom/index.js +9 -1
  17. package/plugin-src/client/channels/weixin/api.js +2 -10
  18. package/plugin-src/client/channels/weixin/index.js +9 -5
  19. package/plugin-src/client/channels/whatsapp/api.js +2 -0
  20. package/plugin-src/client/channels/whatsapp/index.js +9 -1
  21. package/plugin-src/client/i18n.js +4 -0
  22. package/plugin-src/client/index.js +16 -2
  23. package/plugin-src/client/last-message-error.js +17 -0
  24. package/plugin-src/client/styles.js +5 -1
  25. package/plugin-src/host/channels/feishu/rpc.mjs +3 -0
  26. package/src/channels/dingtalk/dingtalk-api.mjs +1 -1
  27. package/src/channels/dingtalk/dingtalk-bridge.mjs +49 -16
  28. package/src/channels/dingtalk/dingtalk-card-stream.mjs +2 -2
  29. package/src/channels/dingtalk/dingtalk-controller.mjs +2 -0
  30. package/src/channels/feishu/bridge.mjs +99 -43
  31. package/src/channels/feishu/multi-bot-controller.mjs +2 -0
  32. package/src/channels/qq/qq-bridge.mjs +83 -28
  33. package/src/channels/qq/qq-controller.mjs +2 -0
  34. package/src/channels/shared/harness-client.mjs +40 -4
  35. package/src/channels/shared/i18n-en/shared-a.mjs +77 -0
  36. package/src/channels/shared/message-failure.mjs +244 -0
  37. package/src/channels/shared/semantic/artifact-delivery.mjs +9 -2
  38. package/src/channels/shared/text-harness-bridge.mjs +65 -60
  39. package/src/channels/shared/token-bot-controller.mjs +2 -0
  40. package/src/channels/slack/slack-controller.mjs +2 -0
  41. package/src/channels/wecom/wecom-bridge.mjs +49 -15
  42. package/src/channels/wecom/wecom-controller.mjs +2 -0
  43. package/src/channels/weixin/weixin-api.mjs +47 -0
  44. package/src/channels/weixin/weixin-bridge.mjs +261 -43
  45. package/src/channels/weixin/weixin-controller.mjs +2 -15
  46. package/src/channels/weixin/weixin-runtime.mjs +1 -0
  47. package/src/channels/whatsapp/whatsapp-controller.mjs +2 -0
@@ -1,5 +1,6 @@
1
1
  import { connectionTestMessage } from './connection-test.mjs';
2
2
  import { t } from './i18n.mjs';
3
+ import { publicMessageFailure } from './message-failure.mjs';
3
4
 
4
5
  function cleanString(value) {
5
6
  return typeof value === 'string' && value.trim() ? value.trim() : null;
@@ -291,6 +292,7 @@ export class TokenBotController {
291
292
  messagesReceived: runtimeStatus?.messagesReceived ?? 0,
292
293
  messagesReplied: runtimeStatus?.messagesReplied ?? 0,
293
294
  },
295
+ lastMessageError: publicMessageFailure(runtimeStatus?.lastMessageError),
294
296
  error: structuredClone(this.#errors.get(config.botId) ?? null),
295
297
  };
296
298
  });
@@ -1,4 +1,5 @@
1
1
  import { connectionTestMessage } from '../shared/connection-test.mjs';
2
+ import { publicMessageFailure } from '../shared/message-failure.mjs';
2
3
  import { t } from '../shared/i18n.mjs';
3
4
  import { deriveSlackBotIdentity, maskSlackBotId } from './config-store.mjs';
4
5
  import { inspectSlackCredentials } from './slack-api.mjs';
@@ -254,6 +255,7 @@ export class SlackController {
254
255
  messagesReceived: runtimeStatus?.messagesReceived ?? 0,
255
256
  messagesReplied: runtimeStatus?.messagesReplied ?? 0,
256
257
  },
258
+ lastMessageError: publicMessageFailure(runtimeStatus?.lastMessageError),
257
259
  error: structuredClone(this.#errors.get(config.botId) ?? null),
258
260
  };
259
261
  });
@@ -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;
@@ -460,6 +467,7 @@ export function createWecomBridgeStatus() {
460
467
  lastReplyAt: null,
461
468
  lastRejectedAt: null,
462
469
  lastError: null,
470
+ lastMessageError: null,
463
471
  };
464
472
  }
465
473
 
@@ -589,8 +597,11 @@ export class WecomHarnessBridge {
589
597
  ).catch((error) => {
590
598
  if (error?.code === 'turn-stopped' || this.#signal?.aborted) return;
591
599
  this.#status.lastError = error?.message ?? String(error);
592
- this.#logger.error?.('[dsh-im:wecom] failed to process a command');
593
- 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))
594
605
  .catch(() => undefined);
595
606
  }).finally(() => {
596
607
  this.#acceptedMessageIds.delete(messageId);
@@ -713,8 +724,11 @@ export class WecomHarnessBridge {
713
724
  }).catch(async (error) => {
714
725
  if (this.#signal?.aborted) return;
715
726
  this.#status.lastError = error?.message ?? String(error);
716
- this.#logger.error?.('[dsh-im:wecom] failed to process a batch input message');
717
- 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))
718
732
  .catch(() => undefined);
719
733
  }).finally(() => {
720
734
  this.#acceptedMessageIds.delete(messageId);
@@ -791,7 +805,7 @@ export class WecomHarnessBridge {
791
805
 
792
806
  async #deliverArtifacts(chatId, replyTo, artifacts = [], baseReceipt = null) {
793
807
  if (artifacts.length === 0) {
794
- return { receipt: baseReceipt, failureNoticeVisible: false };
808
+ return { receipt: baseReceipt, failureNoticeVisible: false, artifactSendErrors: 0 };
795
809
  }
796
810
  const delivery = await deliverOutboundArtifacts({
797
811
  artifacts,
@@ -809,9 +823,13 @@ export class WecomHarnessBridge {
809
823
  signal: this.#signal,
810
824
  timeoutMs: this.#fileUploadTimeoutMs,
811
825
  }),
812
- 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(
813
831
  chatId,
814
- artifactFailureText(artifact?.fileName, error),
832
+ messageFailureText(failure),
815
833
  ),
816
834
  logger: this.#logger,
817
835
  });
@@ -822,6 +840,7 @@ export class WecomHarnessBridge {
822
840
  return {
823
841
  receipt: delivery.receipt,
824
842
  failureNoticeVisible: delivery.failureNoticeVisible,
843
+ artifactSendErrors: delivery.artifactSendErrors,
825
844
  };
826
845
  }
827
846
 
@@ -849,6 +868,7 @@ export class WecomHarnessBridge {
849
868
  let streamId = null;
850
869
  let streamStarted = false;
851
870
  let batchSettled = batchSubmission === null;
871
+ let promptRecorded = false;
852
872
  try {
853
873
  if (!text && !hasImages && !hasFiles) {
854
874
  await this.#sendImmediate(frame, chatId, t('目前支持文字、图片、文件和语音转写消息。'));
@@ -909,6 +929,8 @@ export class WecomHarnessBridge {
909
929
  const content = hasImages
910
930
  ? await promptContentForMessage(message, { signal: this.#signal })
911
931
  : undefined;
932
+ await this.#state.markSeen(messageId);
933
+ promptRecorded = true;
912
934
  const { answer, artifacts = [] } = await askInWorkspaceSession({
913
935
  harness: this.#harness,
914
936
  state: this.#state,
@@ -982,7 +1004,7 @@ export class WecomHarnessBridge {
982
1004
  });
983
1005
  }
984
1006
  } catch (error) {
985
- textSendError = error;
1007
+ textSendError = channelDeliveryFailure(error);
986
1008
  this.#logger.warn?.(
987
1009
  '[dsh-im:wecom] final text delivery failed; continuing with result files:',
988
1010
  error,
@@ -995,10 +1017,16 @@ export class WecomHarnessBridge {
995
1017
  if (textSendError && !artifactDispatched && !delivery.failureNoticeVisible) {
996
1018
  throw textSendError;
997
1019
  }
998
- 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);
999
1024
  this.#status.messagesReplied += 1;
1000
1025
  this.#status.lastReplyAt = new Date().toISOString();
1001
1026
  this.#status.lastError = null;
1027
+ if (!textSendError && delivery.artifactSendErrors === 0) {
1028
+ clearLastMessageFailure(this.#status);
1029
+ }
1002
1030
  return delivery.receipt;
1003
1031
  } catch (error) {
1004
1032
  let batchFailureMessage = null;
@@ -1015,15 +1043,21 @@ export class WecomHarnessBridge {
1015
1043
  await this.#client.replyStream(frame, streamId, t('已停止。'), true)
1016
1044
  .catch(() => undefined);
1017
1045
  }
1018
- await this.#state.markSeen(messageId);
1046
+ if (!promptRecorded) await this.#state.markSeen(messageId);
1019
1047
  return;
1020
1048
  }
1021
1049
  if (this.#signal?.aborted) return;
1022
1050
  this.#status.lastError = error?.message ?? String(error);
1023
- this.#logger.error?.('[dsh-im:wecom] failed to process an inbound message');
1024
- const errorText = inboundFileUserMessage(error)
1025
- ?? imagePromptUserMessage(error)
1026
- ?? 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);
1027
1061
  const visibleError = batchFailureMessage
1028
1062
  ? `${errorText}\n\n${batchFailureMessage}`
1029
1063
  : errorText;
@@ -1033,7 +1067,7 @@ export class WecomHarnessBridge {
1033
1067
  } else {
1034
1068
  await this.#sendImmediate(frame, chatId, visibleError);
1035
1069
  }
1036
- await this.#state.markSeen(messageId);
1070
+ if (!promptRecorded) await this.#state.markSeen(messageId);
1037
1071
  } catch {
1038
1072
  this.#logger.error?.('[dsh-im:wecom] failed to send the safe error reply');
1039
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);