@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
@@ -15,6 +15,8 @@ import {
15
15
  imageFileSourcesFromContent,
16
16
  isModelImageRejection,
17
17
  } from './image-prompt.mjs';
18
+ import { imageInputLimits } from './image-input-policy.mjs';
19
+ import { inboundImagesAsFiles, imageContentFromStaged, isImageAdmissionRejection, IMAGE_HOST_LIMIT_FALLBACK_PROMPT } from './image-input.mjs';
18
20
  import { imSourceGuidance } from './im-source-guidance.mjs';
19
21
  import { outboundArtifactRegistry } from './semantic/artifact.mjs';
20
22
  import { t } from './i18n.mjs';
@@ -354,10 +356,23 @@ export function textFromHarnessContent(content) {
354
356
  .trim();
355
357
  }
356
358
 
359
+ /** Join only reasoning blocks from one Harness message payload. */
360
+ export function reasoningFromHarnessContent(content) {
361
+ return (Array.isArray(content) ? content : [])
362
+ .filter((part) => part?.type === 'reasoning' && typeof part.text === 'string')
363
+ .map((part) => part.text)
364
+ .join('\n')
365
+ .trim();
366
+ }
367
+
357
368
  function assistantMessageText(event) {
358
369
  return textFromHarnessContent(event?.data?.message?.content);
359
370
  }
360
371
 
372
+ function assistantReasoningText(event) {
373
+ return reasoningFromHarnessContent(event?.data?.message?.content);
374
+ }
375
+
361
376
  /** Aggregate assistant text in stable step/index order for one Harness Turn. */
362
377
  export class AssistantTextAccumulator {
363
378
  #steps = new Map();
@@ -413,6 +428,29 @@ function toolResultErrorText(error) {
413
428
  return null;
414
429
  }
415
430
 
431
+ /** Join the visible text carried by a Harness tool result. */
432
+ function toolResultText(data) {
433
+ const blocks = Array.isArray(data?.message?.content) ? data.message.content : [];
434
+ const nested = blocks.flatMap((block) => (
435
+ Array.isArray(block?.content) ? block.content : [block]
436
+ ));
437
+ const text = nested
438
+ .filter((block) => block?.type === 'text' && typeof block.text === 'string')
439
+ .map((block) => block.text)
440
+ .join('');
441
+ return text || nonEmptyText(data?.text) || toolResultErrorText(data?.error) || '';
442
+ }
443
+
444
+ function toolResultCallId(data) {
445
+ const blocks = Array.isArray(data?.message?.content) ? data.message.content : [];
446
+ return blocks
447
+ .map((block) => nonEmptyText(block?.toolCallId))
448
+ .find(Boolean)
449
+ ?? nonEmptyText(data?.message?.source?.callId)
450
+ ?? nonEmptyText(data?.callId)
451
+ ?? nonEmptyText(data?.subCallId);
452
+ }
453
+
416
454
  function consumeInteractionOwnership(ownership, entries) {
417
455
  const ordered = [...entries]
418
456
  .map((entry) => entry?.event ?? entry)
@@ -503,10 +541,18 @@ export class HarnessReplyTracker {
503
541
  #reason = null;
504
542
  #toolNames = new Map();
505
543
  #lastToolName = null;
544
+ #transientSeqs = new Set();
545
+ #pendingReasoning = new Map();
546
+ #pendingReasoningChars = 0;
547
+ #reasoning = false;
506
548
 
507
- constructor({ promptRpcId, afterSeq = -1 }) {
549
+ constructor({ promptRpcId, afterSeq = -1, reasoning = false }) {
508
550
  this.#promptRpcId = promptRpcId;
509
551
  this.#lastSeq = afterSeq;
552
+ // Reasoning updates are opt-in per consumer: only channels that surface
553
+ // thinking traces (Telegram thinking mode) subscribe; every other channel
554
+ // keeps its pre-thinking-traces update stream untouched.
555
+ this.#reasoning = reasoning === true;
510
556
  }
511
557
 
512
558
  get finished() {
@@ -540,7 +586,35 @@ export class HarnessReplyTracker {
540
586
  pushUpdate({ type: 'text', text });
541
587
  }
542
588
 
543
- consumeAll(entries) {
589
+ #bufferReasoning(event) {
590
+ const text = event.data?.chunk?.text;
591
+ if (typeof text !== 'string' || !text || text.length > 65536
592
+ || this.#pendingReasoning.has(event.seq)) return;
593
+ this.#pendingReasoning.set(event.seq, event);
594
+ this.#pendingReasoningChars += text.length;
595
+ while (this.#pendingReasoning.size > 256 || this.#pendingReasoningChars > 65536) {
596
+ const [seq, oldest] = this.#pendingReasoning.entries().next().value;
597
+ this.#pendingReasoning.delete(seq);
598
+ this.#pendingReasoningChars -= oldest.data.chunk.text.length;
599
+ }
600
+ }
601
+
602
+ #takePendingReasoning() {
603
+ const events = [...this.#pendingReasoning.values()];
604
+ this.#pendingReasoning.clear();
605
+ this.#pendingReasoningChars = 0;
606
+ return events;
607
+ }
608
+
609
+ #emitReasoning(event, pushUpdate) {
610
+ const text = event.data?.chunk?.text;
611
+ if (event.data?.turn !== this.#targetTurn || this.#transientSeqs.has(event.seq)
612
+ || typeof text !== 'string' || !text) return;
613
+ this.#transientSeqs.add(event.seq);
614
+ pushUpdate({ type: 'reasoning', turn: this.#targetTurn, text });
615
+ }
616
+
617
+ consumeAll(entries, { live = false, fromMux = false } = {}) {
544
618
  const updates = [];
545
619
  // 同一批轮询内的 text 帧只保留最新累积,其余事件逐帧透出,
546
620
  // 让消费方能按顺序看到每个工具调用与结果。
@@ -554,20 +628,53 @@ export class HarnessReplyTracker {
554
628
  }
555
629
  updates.push(update);
556
630
  };
557
- const ordered = [...entries]
631
+ const ordered = [...(fromMux ? [] : this.#takePendingReasoning()), ...entries]
558
632
  .map((entry) => entry?.event ?? entry)
559
633
  .filter(Boolean)
560
634
  .sort((left, right) => (left.seq ?? -1) - (right.seq ?? -1));
561
635
 
562
636
  for (const event of ordered) {
637
+ if (this.#finished) break;
563
638
  const seq = event.seq ?? -1;
639
+ const isReasoning = event.type === 'assistant/chunk'
640
+ && event.data?.chunk?.type === 'reasoning-delta';
641
+ // The mux is lossy across reconnects. Only history may advance the
642
+ // durable cursor or finish the reply; the mux supplements reasoning.
643
+ if (fromMux && !isReasoning) continue;
644
+ if (isReasoning) {
645
+ if (!live || !Number.isFinite(seq)) continue;
646
+ if (this.#targetTurn === null) this.#bufferReasoning(event);
647
+ else this.#emitReasoning(event, pushUpdate);
648
+ continue;
649
+ }
650
+ if (this.#targetTurn === null) {
651
+ if (seq <= this.#lastSeq) continue;
652
+ if (event.type === 'turn/start') {
653
+ this.#openTurn = event.data?.turn ?? null;
654
+ continue;
655
+ }
656
+ if (event.type === 'user/message' && event.data?.source?.rpcId === this.#promptRpcId) {
657
+ this.#lastSeq = seq;
658
+ this.#targetTurn = event.data?.turn ?? this.#openTurn;
659
+ if (live && this.#targetTurn !== null) {
660
+ pushUpdate({ type: 'turn-start', turn: this.#targetTurn });
661
+ // Only earlier fragments flush here. Later steps remain in the
662
+ // sorted batch, interleaved with their tool calls and results.
663
+ for (const pending of this.#takePendingReasoning()) {
664
+ this.#emitReasoning(pending, pushUpdate);
665
+ }
666
+ }
667
+ }
668
+ continue;
669
+ }
564
670
  if (seq <= this.#lastSeq) continue;
565
671
  this.#lastSeq = seq;
566
672
 
567
673
  if (event.type === 'turn/start') this.#openTurn = event.data?.turn ?? null;
568
674
 
569
675
  if (event.type === 'user/message' && event.data?.source?.rpcId === this.#promptRpcId) {
570
- this.#targetTurn = this.#openTurn;
676
+ this.#targetTurn = event.data?.turn ?? this.#openTurn;
677
+ if (live) pushUpdate({ type: 'turn-start', turn: this.#targetTurn });
571
678
  continue;
572
679
  }
573
680
  if (this.#targetTurn === null) continue;
@@ -577,6 +684,13 @@ export class HarnessReplyTracker {
577
684
  this.#finished = true;
578
685
  this.#reason = event.data?.reason ?? null;
579
686
  this.#openTurn = null;
687
+ if (live) {
688
+ pushUpdate({
689
+ type: 'turn-end',
690
+ turn: this.#targetTurn,
691
+ reason: this.#reason,
692
+ });
693
+ }
580
694
  continue;
581
695
  }
582
696
  if (event.data?.turn !== this.#targetTurn) continue;
@@ -595,7 +709,22 @@ export class HarnessReplyTracker {
595
709
  this.#assistantText.setCanonical(step, text);
596
710
  // canonical 定稿且非空时按 step 透出,供分步推送消费方使用;
597
711
  // 先于 commitText 透出,保持 text 更新作为批次末尾的既有语义。
598
- if (text) pushUpdate({ type: 'assistant-message', step, text });
712
+ if (text) {
713
+ pushUpdate({
714
+ type: 'assistant-message',
715
+ step,
716
+ text,
717
+ ...(live ? { turn: this.#targetTurn } : {}),
718
+ });
719
+ }
720
+ // Thinking-trace channels consume this as the 💭 line that precedes the
721
+ // tool call it explains; only consumers that explicitly subscribed
722
+ // (reasoning: true) see these updates, so default-mode channels keep
723
+ // their pre-thinking-traces progress stream.
724
+ if (this.#reasoning) {
725
+ const reasoning = assistantReasoningText(event);
726
+ if (reasoning) pushUpdate({ type: 'reasoning', step, text: reasoning });
727
+ }
599
728
  this.#commitText(this.#assistantText.text, pushUpdate);
600
729
  continue;
601
730
  }
@@ -618,20 +747,38 @@ export class HarnessReplyTracker {
618
747
  }
619
748
  }
620
749
  }
621
- pushUpdate({ type: 'tool', name, ...(argsText ? { arguments: argsText } : {}), ...(callId ? { callId } : {}) });
750
+ pushUpdate({
751
+ type: 'tool',
752
+ name,
753
+ ...(argsText ? { arguments: argsText } : {}),
754
+ ...(callId ? { callId } : {}),
755
+ ...(live ? { turn: this.#targetTurn } : {}),
756
+ });
622
757
  } else if (event.type === 'tool/result') {
623
- const callId = nonEmptyText(event.data?.message?.source?.callId)
624
- ?? nonEmptyText(event.data?.callId)
625
- ?? nonEmptyText(event.data?.subCallId);
758
+ const callId = toolResultCallId(event.data);
626
759
  const toolName = (callId ? this.#toolNames.get(callId) : null)
627
760
  ?? this.#lastToolName;
628
761
  const error = toolResultErrorText(event.data?.error);
629
- pushUpdate({
630
- type: 'status',
631
- text: t('正在整理结果…'),
632
- ...(toolName ? { toolName } : {}),
633
- ...(error ? { error } : {}),
634
- });
762
+ if (live) {
763
+ const providerErrorCode = event.data?.error?.code;
764
+ pushUpdate({
765
+ type: 'tool-result',
766
+ turn: this.#targetTurn,
767
+ ...(callId ? { callId } : {}),
768
+ ...(toolName ? { toolName } : {}),
769
+ text: toolResultText(event.data),
770
+ ...(providerErrorCode !== undefined && providerErrorCode !== null
771
+ ? { errorCode: String(providerErrorCode) }
772
+ : {}),
773
+ });
774
+ } else {
775
+ pushUpdate({
776
+ type: 'status',
777
+ text: t('正在整理结果…'),
778
+ ...(toolName ? { toolName } : {}),
779
+ ...(error ? { error } : {}),
780
+ });
781
+ }
635
782
  }
636
783
  }
637
784
  return updates;
@@ -642,6 +789,19 @@ export class HarnessReplyTracker {
642
789
  }
643
790
  }
644
791
 
792
+ /**
793
+ * Progress updates an ask() consumer actually receives. latest 模式只投递一条
794
+ * 最新进展;assistant-message 是分步推送专用更新,且 canonical 去重后可能成为
795
+ * 批次唯一变化,绝不能冒充进度投给全部渠道。reasoning 同为思考留痕渠道专用,
796
+ * 默认模式下不得冒充进度(钉钉、企微等会展示其 text)。
797
+ */
798
+ export function visibleProgressUpdates(updates, progressMode) {
799
+ if (progressMode === 'all') return updates;
800
+ return updates
801
+ .filter((update) => update.type !== 'assistant-message' && update.type !== 'reasoning')
802
+ .slice(-1);
803
+ }
804
+
645
805
  export class HarnessRpcError extends Error {
646
806
  constructor(method, error) {
647
807
  super(`${method}: ${error?.message ?? 'unknown Harness RPC error'}`);
@@ -706,7 +866,6 @@ function harnessTurnError(reason) {
706
866
  return new HarnessTurnError('turn-interrupted', { reason });
707
867
  }
708
868
  if (kind === 'aborted') return new HarnessTurnError('turn-aborted', { reason });
709
- if (kind === 'completed') return new HarnessTurnError('model-empty-response', { reason });
710
869
  return new HarnessTurnError('harness-turn-failed', { reason });
711
870
  }
712
871
 
@@ -732,6 +891,7 @@ export class HarnessClient {
732
891
  #controlExecutor;
733
892
  #sessionMaintenanceExecutor;
734
893
  #fileIngressExecutor;
894
+ #imageInputPolicy;
735
895
  #managedProcess = null;
736
896
  #interactionRegistry;
737
897
  #interactionOwnerships;
@@ -756,6 +916,7 @@ export class HarnessClient {
756
916
  controlExecutor,
757
917
  sessionMaintenanceExecutor,
758
918
  fileIngressExecutor,
919
+ imageInputPolicy = () => undefined,
759
920
  }) {
760
921
  if (typeof createWebSocket !== 'function') {
761
922
  throw new TypeError('createWebSocket must be a function');
@@ -807,6 +968,7 @@ export class HarnessClient {
807
968
  this.#controlExecutor = controlExecutor;
808
969
  this.#sessionMaintenanceExecutor = sessionMaintenanceExecutor;
809
970
  this.#fileIngressExecutor = fileIngressExecutor;
971
+ this.#imageInputPolicy = imageInputPolicy;
810
972
  this.#interactionRegistry = interactionRegistry(this.#baseUrl?.origin ?? interactionScope);
811
973
  this.#interactionOwnerships = this.#interactionRegistry.ownerships;
812
974
  this.#interactionClaims = this.#interactionRegistry.claims;
@@ -1142,6 +1304,7 @@ export class HarnessClient {
1142
1304
  signal,
1143
1305
  onInteraction,
1144
1306
  onResolved,
1307
+ onSessionEvent,
1145
1308
  onOpen,
1146
1309
  ownership,
1147
1310
  } = {}) {
@@ -1155,6 +1318,9 @@ export class HarnessClient {
1155
1318
  if (onResolved !== undefined && typeof onResolved !== 'function') {
1156
1319
  throw new TypeError('onResolved must be a function');
1157
1320
  }
1321
+ if (onSessionEvent !== undefined && typeof onSessionEvent !== 'function') {
1322
+ throw new TypeError('onSessionEvent must be a function');
1323
+ }
1158
1324
  if (onOpen !== undefined && typeof onOpen !== 'function') {
1159
1325
  throw new TypeError('onOpen must be a function');
1160
1326
  }
@@ -1165,6 +1331,7 @@ export class HarnessClient {
1165
1331
  signal,
1166
1332
  onInteraction,
1167
1333
  onResolved,
1334
+ onSessionEvent,
1168
1335
  onOpen,
1169
1336
  ownership,
1170
1337
  });
@@ -1436,12 +1603,21 @@ export class HarnessClient {
1436
1603
  });
1437
1604
  }
1438
1605
 
1606
+ async getImageInputLimits() {
1607
+ return imageInputLimits(await this.#imageInputPolicy());
1608
+ }
1609
+
1439
1610
  async ask(sessionId, prompt, options = {}) {
1440
1611
  if (typeof options === 'number') options = { timeoutMs: options };
1441
1612
  const timeoutMs = options.timeoutMs ?? 600_000;
1442
1613
  const signal = options.signal;
1443
1614
  const onUpdate = typeof options.onUpdate === 'function' ? options.onUpdate : null;
1444
- const progressMode = options.progressMode === 'all' ? 'all' : 'latest';
1615
+ const progressMode = options.progressMode === 'live'
1616
+ ? 'live'
1617
+ : options.progressMode === 'all' ? 'all' : 'latest';
1618
+ // Reasoning updates only exist for consumers that opt in (thinking-trace
1619
+ // mode); default ask() consumers keep the pre-thinking-traces stream.
1620
+ const reasoning = options.reasoning === true;
1445
1621
  const onArtifact = typeof options.onArtifact === 'function' ? options.onArtifact : null;
1446
1622
  const onInteraction = typeof options.onInteraction === 'function'
1447
1623
  ? options.onInteraction
@@ -1451,6 +1627,7 @@ export class HarnessClient {
1451
1627
  : undefined;
1452
1628
  const control = normalizeControl(options.control);
1453
1629
  const inboundFiles = Array.isArray(options.files) ? options.files.filter(Boolean) : [];
1630
+ const inboundImages = Array.isArray(options.images) ? options.images.filter(Boolean) : [];
1454
1631
  await this.ensureRunning({ signal });
1455
1632
  const before = await this.rpc(
1456
1633
  'session.history',
@@ -1461,8 +1638,39 @@ export class HarnessClient {
1461
1638
  const baselineSeq = Math.max(-1, ...(before.events ?? []).map(({ event }) => event.seq ?? -1));
1462
1639
  const promptRpcId = `${this.#rpcIdPrefix}-${randomUUID()}`;
1463
1640
  const releasePromptInputOrigin = registerImInputOrigin(this.#interactionRegistry, promptRpcId);
1464
- const tracker = new HarnessReplyTracker({ promptRpcId, afterSeq: baselineSeq });
1641
+ const tracker = new HarnessReplyTracker({ promptRpcId, afterSeq: baselineSeq, reasoning });
1642
+ let lastProgressAt = Date.now();
1643
+ let lastPollSeq = tracker.lastSeq;
1644
+ let progressTail = Promise.resolve();
1645
+ const consumeProgress = (entries, { fromMux = false } = {}) => {
1646
+ const updates = tracker.consumeAll(entries, {
1647
+ live: progressMode === 'live',
1648
+ fromMux,
1649
+ });
1650
+ const seqAdvanced = tracker.lastSeq > lastPollSeq;
1651
+ lastPollSeq = tracker.lastSeq;
1652
+ if (seqAdvanced) lastProgressAt = Date.now();
1653
+ if (!onUpdate) return progressTail;
1654
+ const visibleUpdates = progressMode === 'all' || progressMode === 'live'
1655
+ ? updates
1656
+ : updates
1657
+ .filter((update) => update.type !== 'assistant-message' && update.type !== 'reasoning')
1658
+ .slice(-1);
1659
+ for (const update of visibleUpdates) {
1660
+ progressTail = progressTail
1661
+ .then(() => onUpdate(update))
1662
+ .catch((error) => {
1663
+ console.warn(
1664
+ '[dsh-im] ignored a progress update failure:',
1665
+ this.#logPrefix,
1666
+ error.message,
1667
+ );
1668
+ });
1669
+ }
1670
+ return progressTail;
1671
+ };
1465
1672
  const interactionController = onInteraction || onInteractionResolved
1673
+ || (progressMode === 'live' && onUpdate)
1466
1674
  ? new AbortController()
1467
1675
  : null;
1468
1676
  const interactionSignal = interactionController
@@ -1493,6 +1701,7 @@ export class HarnessClient {
1493
1701
  let interactionTask = null;
1494
1702
  let artifactsDelivered = false;
1495
1703
  let deliveredArtifactCount = 0;
1704
+ let artifactHandoffError = null;
1496
1705
  const stagedBatches = [];
1497
1706
  let promptAccepted = false;
1498
1707
  let turnFinished = false;
@@ -1508,6 +1717,7 @@ export class HarnessClient {
1508
1717
  await onArtifact(artifact);
1509
1718
  deliveredArtifactCount += 1;
1510
1719
  } catch (error) {
1720
+ artifactHandoffError ??= error;
1511
1721
  outboundArtifactRegistry.release(artifact);
1512
1722
  console.warn('[dsh-im] ignored an artifact handoff failure:', this.#logPrefix, error.message);
1513
1723
  }
@@ -1530,6 +1740,26 @@ export class HarnessClient {
1530
1740
  stagedBatches.push(staged);
1531
1741
  prompt = appendInboundFilesToPrompt(prompt, staged);
1532
1742
  }
1743
+ if (inboundImages.length > 0) {
1744
+ const limits = await this.getImageInputLimits();
1745
+ const sources = inboundImagesAsFiles(inboundImages, limits);
1746
+ let staged;
1747
+ try {
1748
+ staged = await this.#stageWorkspaceFiles(sessionId, sources, signal);
1749
+ } catch (error) {
1750
+ // Ingress wraps loader errors; retain the original image diagnostic.
1751
+ if (signal?.aborted) throw signal.reason ?? error;
1752
+ if (error?.cause?.name === 'ImagePromptError') throw error.cause;
1753
+ throw error;
1754
+ }
1755
+ stagedBatches.push(staged);
1756
+ const images = await imageContentFromStaged(staged, limits, { signal });
1757
+ const originalContent = typeof basePrompt === 'string'
1758
+ ? [{ type: 'text', text: basePrompt }] : basePrompt;
1759
+ prompt = appendInboundFilesToPrompt([...originalContent, ...images], {
1760
+ files: stagedBatches.flatMap((batch) => batch.files),
1761
+ });
1762
+ }
1533
1763
  if (interactionSignal) {
1534
1764
  let markOpen;
1535
1765
  const opened = new Promise((resolve) => { markOpen = resolve; });
@@ -1537,6 +1767,9 @@ export class HarnessClient {
1537
1767
  signal: interactionSignal,
1538
1768
  onInteraction,
1539
1769
  onResolved: onInteractionResolved,
1770
+ onSessionEvent: progressMode === 'live'
1771
+ ? (event) => { consumeProgress([event], { fromMux: true }); }
1772
+ : undefined,
1540
1773
  onOpen: markOpen,
1541
1774
  ownership,
1542
1775
  });
@@ -1577,29 +1810,32 @@ export class HarnessClient {
1577
1810
  // Session workspace and named in a text manifest — then retry once
1578
1811
  // with a text-only prompt. The retry reuses promptRpcId so reply
1579
1812
  // tracking, control and interaction ownership stay bound to this ask.
1580
- const imageSources = isModelImageRejection(error)
1581
- ? imageFileSourcesFromContent(content)
1582
- : [];
1583
- if (imageSources.length === 0) throw error;
1584
- let stagedImages;
1585
- try {
1586
- stagedImages = await this.#stageWorkspaceFiles(sessionId, imageSources, signal);
1587
- } catch (stagingError) {
1588
- if (signal?.aborted) throw signal.reason ?? stagingError;
1589
- console.warn(
1590
- '[dsh-im] unable to restage rejected images as workspace files:',
1591
- this.#logPrefix,
1592
- stagingError?.message ?? String(stagingError),
1593
- );
1594
- throw error;
1813
+ const originalsStaged = inboundImages.length > 0 && isImageAdmissionRejection(error)
1814
+ && content.some((part) => part.type === 'image');
1815
+ const imageSources = !originalsStaged && isModelImageRejection(error)
1816
+ ? imageFileSourcesFromContent(content) : [];
1817
+ if (!originalsStaged && imageSources.length === 0) throw error;
1818
+ if (!originalsStaged) {
1819
+ let stagedImages;
1820
+ try {
1821
+ stagedImages = await this.#stageWorkspaceFiles(sessionId, imageSources, signal);
1822
+ } catch (stagingError) {
1823
+ if (signal?.aborted) throw signal.reason ?? stagingError;
1824
+ console.warn(
1825
+ '[dsh-im] unable to restage rejected images as workspace files:',
1826
+ this.#logPrefix,
1827
+ stagingError?.message ?? String(stagingError),
1828
+ );
1829
+ throw error;
1830
+ }
1831
+ stagedBatches.push(stagedImages);
1595
1832
  }
1596
- stagedBatches.push(stagedImages);
1597
1833
  const baseContent = typeof basePrompt === 'string'
1598
1834
  ? [{ type: 'text', text: basePrompt }]
1599
1835
  : basePrompt;
1600
1836
  const fallbackPrompt = appendInboundFilesToPrompt([
1601
1837
  ...contentWithoutImages(baseContent),
1602
- { type: 'text', text: t(IMAGE_FILE_FALLBACK_PROMPT) },
1838
+ { type: 'text', text: t(isModelImageRejection(error) ? IMAGE_FILE_FALLBACK_PROMPT : IMAGE_HOST_LIMIT_FALLBACK_PROMPT) },
1603
1839
  ], { files: stagedBatches.flatMap((batch) => batch?.files ?? []) });
1604
1840
  await sendPrompt(fallbackPrompt);
1605
1841
  }
@@ -1611,8 +1847,6 @@ export class HarnessClient {
1611
1847
  // confirm the Session is still running before renewing the wait.
1612
1848
  // Interaction ownership is intentionally not a liveness signal: it stays
1613
1849
  // active until turn/end and can therefore outlive a stalled turn.
1614
- let lastProgressAt = Date.now();
1615
- let lastPollSeq = tracker.lastSeq;
1616
1850
  while (true) {
1617
1851
  await sleep(300, signal);
1618
1852
  const history = await this.rpc(
@@ -1626,24 +1860,7 @@ export class HarnessClient {
1626
1860
  this.#consumeInteractionOwnerships(sessionId, history.events ?? []);
1627
1861
  if (!wasActive && ownership.active) ownership.reconnect?.();
1628
1862
  }
1629
- const updates = tracker.consumeAll(history.events ?? []);
1630
- const seqAdvanced = tracker.lastSeq > lastPollSeq;
1631
- lastPollSeq = tracker.lastSeq;
1632
- if (seqAdvanced) lastProgressAt = Date.now();
1633
- if (onUpdate) {
1634
- // latest 模式只投递一条最新进展;assistant-message 是分步推送专用更新,
1635
- // 且 canonical 去重后可能成为批次唯一变化,绝不能冒充进度投给全部渠道。
1636
- const visibleUpdates = progressMode === 'all'
1637
- ? updates
1638
- : updates.filter((update) => update.type !== 'assistant-message').slice(-1);
1639
- for (const update of visibleUpdates) {
1640
- try {
1641
- await onUpdate(update);
1642
- } catch (error) {
1643
- console.warn('[dsh-im] ignored a progress update failure:', this.#logPrefix, error.message);
1644
- }
1645
- }
1646
- }
1863
+ await consumeProgress(history.events ?? []);
1647
1864
  if (tracker.finished) {
1648
1865
  turnFinished = true;
1649
1866
  if (!ownership?.stopRequested && !harnessTurnSucceeded(tracker.reason)) {
@@ -1659,6 +1876,12 @@ export class HarnessClient {
1659
1876
  }
1660
1877
  if (artifactCount > 0) return '';
1661
1878
  if (ownership?.stopRequested) throw turnStoppedError();
1879
+ if (artifactHandoffError) throw artifactHandoffError;
1880
+ // A completed turn can do all its work through tools without text.
1881
+ // Missing end reasons retain the existing empty-reply failure.
1882
+ if (tracker.reason != null && harnessTurnSucceeded(tracker.reason)) {
1883
+ return t('本轮处理已结束,没有文本回复。');
1884
+ }
1662
1885
  throw harnessTurnError(tracker.reason);
1663
1886
  }
1664
1887
 
@@ -1681,6 +1904,11 @@ export class HarnessClient {
1681
1904
  throw timeoutError;
1682
1905
  }
1683
1906
  } catch (error) {
1907
+ if (error instanceof HarnessTurnError) {
1908
+ error.details ??= {
1909
+ sessionId, promptRpcId, baselineSeq, turn: tracker.turn, lastSeq: tracker.lastSeq,
1910
+ };
1911
+ }
1684
1912
  // Once cancellation was accepted, transport/poll failures and timeouts
1685
1913
  // describe the convergence of that stop, not an unrelated ask failure.
1686
1914
  if (!ownership?.stopRequested) throw error;
@@ -1716,6 +1944,7 @@ export class HarnessClient {
1716
1944
  signal,
1717
1945
  onInteraction,
1718
1946
  onResolved,
1947
+ onSessionEvent,
1719
1948
  onOpen,
1720
1949
  ownership,
1721
1950
  }) {
@@ -1755,8 +1984,9 @@ export class HarnessClient {
1755
1984
  };
1756
1985
  const processEnvelope = (envelope) => {
1757
1986
  const payload = envelope.payload;
1758
- if (ownership && payload.type === 'session/event') {
1759
- this.#consumeInteractionOwnerships(sessionId, [payload.event]);
1987
+ if (payload.type === 'session/event') {
1988
+ if (ownership) this.#consumeInteractionOwnerships(sessionId, [payload.event]);
1989
+ dispatch(onSessionEvent, payload.event);
1760
1990
  return;
1761
1991
  }
1762
1992
  if (payload.type === 'question/requested' || payload.type === 'approval/requested') {
@@ -0,0 +1,55 @@
1
+ export default Object.freeze({
2
+ "DSH 版本": "DSH version",
3
+ "依赖版本": "Dependency versions",
4
+ "代理已配置": "Proxy configured",
5
+ "原因链已截断": "Cause chain truncated",
6
+ "请在 macOS 系统设置中授予 DSH 完全磁盘访问权限,以读取 Messages 数据库。": "Grant DSH Full Disk Access in macOS System Settings to read the Messages database.",
7
+ "请在 macOS 系统设置中允许 DSH 自动控制 Messages。": "Allow DSH to control Messages in macOS System Settings.",
8
+ "先在 macOS 系统设置中授予 Messages 权限。": "Grant Messages permissions in macOS System Settings first.",
9
+
10
+ "加载渠道配置": "Load channel configuration",
11
+ "激活账号": "Activate account",
12
+ "启动连接": "Start connection",
13
+ "监测连接与消息": "Monitor connection and messages",
14
+ "停止连接": "Stop connection",
15
+ "服务返回码": "Provider code",
16
+ "验证凭据": "Verify credentials",
17
+ "检查权限": "Check permissions",
18
+ "加载渠道组件": "Load channel component",
19
+ "执行渠道操作": "Perform channel operation",
20
+ "接收消息": "Receive message",
21
+ "发送消息": "Send message",
22
+ "发送测试消息": "Send test message",
23
+ "暂未识别": "Unknown",
24
+ "操作已取消。": "The operation was cancelled.",
25
+ "本地会话状态清理失败,请查看诊断详情。": "Local session state cleanup failed. See the diagnostic details.",
26
+ "账号已移除,但本地状态清理失败。": "The account was removed, but local state cleanup failed.",
27
+ "账号已移除,但登录凭据清理失败。": "The account was removed, but credential cleanup failed.",
28
+ "连接已移除,但登录凭据清理失败。": "The connection was removed, but credential cleanup failed.",
29
+
30
+ "操作失败,请查看原因和诊断详情。": "The operation failed. See the cause and diagnostic details.",
31
+ "检测到多个底层原因,请展开诊断详情逐项检查。": "Multiple underlying causes were detected. Expand the diagnostic details to inspect them.",
32
+ "服务域名解析失败,请检查运行 DSH 的机器的 DNS 和网络设置。": "Service DNS resolution failed. Check DNS and network settings on the machine running DSH.",
33
+ "目标拒绝连接,请检查服务地址、端口和访问限制。": "The connection was refused. Check the service address, port, and access restrictions.",
34
+ "连接已中断,请检查网络和目标服务状态。": "The connection was interrupted. Check the network and target service.",
35
+ "目标网络不可达,请检查运行 DSH 的机器的网络出口和访问限制。": "The target network is unreachable. Check network routing and access restrictions on the machine running DSH.",
36
+ "TLS 或证书校验失败,请检查系统时间、证书链和网络设置。": "TLS or certificate validation failed. Check the system clock, certificate chain, and network settings.",
37
+ "请求超时,请检查目标服务和网络;诊断详情中可查看已知的耗时。": "The request timed out. Check the service and network; available timings are in the diagnostic details.",
38
+ "代理连接或认证失败,请检查 DSH 进程使用的代理配置。": "Proxy connection or authentication failed. Check the proxy configuration used by the DSH process.",
39
+ "服务认证失败,请检查账号和授权信息。": "Service authentication failed. Check the account and credentials.",
40
+ "操作权限不足,请检查对应资源的访问权限。": "Permission was denied. Check access to the affected resource.",
41
+ "所需文件或本地工具不存在,请检查安装和配置。": "A required file or local tool is missing. Check installation and configuration.",
42
+ "服务正在限流,请稍后重试。": "The service is rate limiting requests. Try again later.",
43
+ "服务拒绝了请求,请检查凭据、权限或访问限制;HTTP 状态本身不能确认登录失效。": "The service rejected the request. Check credentials, permissions, or access restrictions; the HTTP status alone does not prove that the login expired.",
44
+ "目标服务暂时异常,请稍后重试。": "The target service is temporarily unavailable. Try again later.",
45
+ "目标服务返回了 HTTP 错误,请查看诊断详情。": "The target service returned an HTTP error. See the diagnostic details.",
46
+ "数据格式无效,请按失败阶段检查服务响应或本地配置。": "The data format is invalid. Check the service response or local configuration indicated by the failing stage.",
47
+ "暂未识别具体原因,请复制诊断信息,并查看相同参考号的 DSH 日志。": "The underlying cause is unknown. Copy the diagnostics and find the matching reference in the DSH logs.",
48
+ "多个原因": "Multiple causes",
49
+ "服务域名": "Service host",
50
+ "请求耗时(毫秒)": "Request duration (ms)",
51
+ "超时阈值(毫秒)": "Timeout (ms)",
52
+ "Node 版本": "Node version",
53
+ "运行平台": "Platform",
54
+ "无法访问 DSH 管理接口,请检查 DSH 连接或重新登录后重试。": "Cannot access the DSH management API. Check the DSH connection or sign in again."
55
+ });
@@ -0,0 +1,25 @@
1
+ /** English projections for host-side Email channel text. */
2
+ export default Object.freeze({
3
+ '邮箱': 'Email',
4
+ '邮箱地址格式不正确': 'The email address format is invalid',
5
+ '邮箱地址与应用密码均为必填': 'Both the email address and the app password are required',
6
+ '请选择邮箱服务商或填写 IMAP/SMTP 服务器地址':
7
+ 'Choose a mail provider or fill in the IMAP/SMTP server addresses',
8
+ '必须至少配置一个允许发件人':
9
+ 'At least one allowed sender must be configured',
10
+ '邮箱连接失败,请检查地址、应用密码与服务器设置':
11
+ 'Could not connect to the mailbox; check the address, app password, and server settings',
12
+ '未找到该邮箱配置': 'No mailbox configuration was found',
13
+ '邮箱凭据已丢失,请重新绑定':
14
+ 'The mailbox credential is missing; please bind the mailbox again',
15
+ '邮箱尚未连接': 'The mailbox is not connected yet',
16
+ '邮箱通道连接正常。': 'The Email channel is connected.',
17
+ '邮箱通道运行正常': 'The Email channel is running normally',
18
+ '邮箱通道尚未连接': 'The Email channel is not connected yet',
19
+ '邮箱访问策略同步失败,请重试': 'Failed to sync the mailbox access policy; please retry',
20
+ '当前环境不支持会话绑定': 'Session binding is not supported in this environment',
21
+ '该接入方式不需要扫码授权': 'This access method does not use QR authorization',
22
+ '授权信息缺失,请重新扫码授权': 'The authorization is missing; please scan again',
23
+ '扫码授权尚未开始': 'QR authorization has not been started',
24
+ '扫码授权已超时,请重新发起': 'QR authorization timed out; please start it again',
25
+ });