@qwen-code/channel-dingtalk 0.22.2 → 0.22.3-nightly.20260831.3a0c4c6108

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.
@@ -36,6 +36,9 @@ export declare class DingtalkChannel extends ChannelBase {
36
36
  private readonly inboundCardOwners;
37
37
  private readonly cardRunBySession;
38
38
  private readonly cardRuns;
39
+ private readonly fileProjectors;
40
+ private readonly blockFileProjectors;
41
+ private readonly blockProjectionArmed;
39
42
  constructor(name: string, config: ChannelConfig, bridge: ChannelAgentBridge, options?: ChannelBaseOptions);
40
43
  private createClient;
41
44
  private installStructuredDownstreamHandler;
@@ -52,9 +55,11 @@ export declare class DingtalkChannel extends ChannelBase {
52
55
  * dropped on ingestion. Exposed for testing the drop rule.
53
56
  */
54
57
  static isUnroutableGroupMessage(isGroup: boolean, conversationId: string | undefined): boolean;
58
+ private projectOutgoingFileText;
55
59
  private prepareOutgoingText;
56
60
  private sendReply;
57
61
  sendMessage(chatId: string, text: string): Promise<void>;
62
+ protected sendThreadMessage(chatId: string, _threadId: string | undefined, text: string, sourceLabel?: string): Promise<void>;
58
63
  supportsProactiveSend(): boolean;
59
64
  protected supportsProactiveTarget(target: SessionTarget): boolean;
60
65
  protected supportsProactiveDeliveryTarget(target: SessionTarget): boolean;
@@ -63,7 +68,7 @@ export declare class DingtalkChannel extends ChannelBase {
63
68
  * Single-shot cold send: a failed chunk aborts the remainder (already-sent
64
69
  * chunks are not recalled) and the error surfaces in the loop's lastError.
65
70
  */
66
- protected pushProactive(target: SessionTarget, text: string): Promise<void>;
71
+ protected pushProactive(target: SessionTarget, text: string, sourceLabel?: string): Promise<void>;
67
72
  private getProactiveToken;
68
73
  private sendCardInteractionFeedback;
69
74
  private sendProactiveChunk;
@@ -90,11 +95,31 @@ export declare class DingtalkChannel extends ChannelBase {
90
95
  protected processInbound(envelope: Envelope): Promise<void>;
91
96
  private untrackBufferedMentionTarget;
92
97
  protected onPromptEnd(chatId: string, sessionId: string, messageId?: string): void;
93
- protected sendResponseMessage(chatId: string, text: string, sessionId: string): Promise<void>;
98
+ /**
99
+ * Turn end is the only point where the block projector's held state can be
100
+ * settled: ChannelBase drains the turn's queued block sends before calling
101
+ * onPromptEnd, so everything already appended belongs to this turn. Flush
102
+ * the held candidate bytes (a trailing `[FILE:` prefix the stream never
103
+ * completed) before deleting the entry — a later delete-without-settle
104
+ * would silently drop them from the delivered answer. Also disarm the
105
+ * session: /clear eviction and session death settle WITHOUT draining the
106
+ * turn's send chain, and any block that lands afterwards must be dropped
107
+ * rather than recreating projector state.
108
+ */
109
+ private settleBlockFileProjector;
110
+ /**
111
+ * Out-of-turn one-shot sends (background responses) must not flow through
112
+ * the session's block-streaming projector: a second sender interleaving with
113
+ * mid-projection state can swallow the send or split a held marker.
114
+ */
115
+ protected deliverBackgroundReply(chatId: string, text: string, sessionId: string, sourceLabel?: string): Promise<void>;
116
+ protected sendResponseMessage(chatId: string, text: string, sessionId: string, sourceLabel?: string): Promise<void>;
117
+ private projectBlockStreamChunk;
94
118
  private sendFallbackReply;
95
119
  protected onResponseComplete(chatId: string, text: string, sessionId: string, segment?: ChannelOutputSegmentContext): Promise<void>;
96
120
  protected onOutputSegmentEnd(_chatId: string, _sessionId: string, segment: ChannelOutputSegmentContext, reason: ChannelOutputSegmentEndReason): void | Promise<void>;
97
121
  protected onResponseChunk(_chatId: string, chunk: string, _sessionId: string, segment?: ChannelOutputSegmentContext): void;
122
+ private deleteFileProjectorsForRun;
98
123
  protected presentUserInputRequest(context: ChannelUserInputRequestContext): Promise<UserInputPresentationResult>;
99
124
  /**
100
125
  * Extract quoted/referenced message context from a reply.
@@ -1 +1 @@
1
- {"version":3,"file":"DingtalkAdapter.d.ts","sourceRoot":"","sources":["../src/DingtalkAdapter.ts"],"names":[],"mappings":"AAYA,OAAO,EACL,WAAW,EAMZ,MAAM,yBAAyB,CAAC;AAcjC,OAAO,EAAE,6BAA6B,EAAE,MAAM,8BAA8B,CAAC;AAC7E,OAAO,EAIL,KAAK,oBAAoB,EACzB,KAAK,0BAA0B,EAEhC,MAAM,6BAA6B,CAAC;AAIrC,OAAO,KAAK,EACV,aAAa,EACb,kBAAkB,EAClB,QAAQ,EACR,kBAAkB,EAClB,2BAA2B,EAC3B,6BAA6B,EAC7B,yBAAyB,EACzB,8BAA8B,EAC9B,aAAa,EACb,2BAA2B,EAC5B,MAAM,yBAAyB,CAAC;AAkmBjC,qBAAa,eAAgB,SAAQ,WAAW;IAC9C,OAAO,CAAC,MAAM,CAAW;IACzB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAU;IACnC,OAAO,CAAC,iBAAiB,CAAC,CAAsC;IAChE,OAAO,CAAC,YAAY,CAAkC;IACtD,OAAO,CAAC,cAAc,CAA6B;IACnD,OAAO,CAAC,qBAAqB,CAA6B;IAC1D,OAAO,CAAC,sBAAsB,CAAqB;IACnD,OAAO,CAAC,+BAA+B,CAAkC;IACzE,OAAO,CAAC,UAAU,CAAC,CAAiC;IACpD,0EAA0E;IAC1E,OAAO,CAAC,QAAQ,CAAkC;IAClD,OAAO,CAAC,kBAAkB,CAAqB;IAC/C,gFAAgF;IAChF,OAAO,CAAC,mBAAmB,CAGvB;IACJ;;;;OAIG;IACH,OAAO,CAAC,iBAAiB,CAAqB;IAC9C;;;OAGG;IACH,OAAO,CAAC,cAAc,CAAC,CAAuC;IAC9D,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAAgC;IACtE,SAAS,CAAC,QAAQ,CAAC,qBAAqB,CAAC,EAAE,6BAA6B,CAAC;IACzE,OAAO,CAAC,oBAAoB,CAAC,CAAuB;IACpD,OAAO,CAAC,sBAAsB,CAAC,CAAyB;IACxD,OAAO,CAAC,oBAAoB,CAAC,CAA+B;IAC5D,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAyC;IAC3E,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAA6B;IAC9D,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAyC;gBAGhE,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,aAAa,EACrB,MAAM,EAAE,kBAAkB,EAC1B,OAAO,CAAC,EAAE,kBAAkB;IA4G9B,OAAO,CAAC,YAAY;IAYpB,OAAO,CAAC,kCAAkC;IAU1C,OAAO,CAAC,sBAAsB;IAe9B,OAAO,CAAC,cAAc;IAoCtB,SAAS,CAAC,iBAAiB,CACzB,QAAQ,EAAE,oBAAoB,GAC7B,0BAA0B;IAiB7B,OAAO,CAAC,YAAY;IAuEpB,OAAO,CAAC,qBAAqB;IAiB7B,OAAO,CAAC,gBAAgB;IAmBlB,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAoB9B;;;;OAIG;IACH,MAAM,CAAC,wBAAwB,CAC7B,OAAO,EAAE,OAAO,EAChB,cAAc,EAAE,MAAM,GAAG,SAAS,GACjC,OAAO;YAII,mBAAmB;YAqDnB,SAAS;IAiEjB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIrD,qBAAqB,IAAI,OAAO;cAMtB,uBAAuB,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO;cAQvD,+BAA+B,CAChD,MAAM,EAAE,aAAa,GACpB,OAAO;cAQS,8BAA8B,CAC/C,MAAM,EAAE,aAAa,GACpB,OAAO;IAQV;;;OAGG;cACsB,aAAa,CACpC,MAAM,EAAE,aAAa,EACrB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,IAAI,CAAC;YAiBF,iBAAiB;IAqC/B,OAAO,CAAC,2BAA2B;YA8BrB,kBAAkB;IA2FhC,OAAO,CAAC,cAAc;YAIR,UAAU;YAuDV,cAAc;YAOd,cAAc;IAO5B,UAAU,IAAI,IAAI;IAclB,2EAA2E;IAC3E,OAAO,CAAC,gBAAgB;IAIxB,OAAO,CAAC,WAAW;IAInB,OAAO,CAAC,wBAAwB;IAShC,OAAO,CAAC,kBAAkB;IAM1B,OAAO,CAAC,aAAa;IAmCrB,OAAO,CAAC,YAAY;IAoBpB,0FAA0F;IACjF,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;cA8B5B,eAAe,CAAC,KAAK,EAAE,yBAAyB,GAAG,IAAI;cAoDvD,qBAAqB,CACtC,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAAE,GACnB,IAAI;cAQY,qBAAqB,CACtC,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAAE,GACnB,IAAI;cAUY,gBAAgB,CACjC,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,SAAS,CAAC,EAAE,MAAM,GACjB,IAAI;cAYY,aAAa,CAC9B,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,SAAS,CAAC,EAAE,MAAM,GACjB,IAAI;IAaQ,aAAa,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;cAiCtC,cAAc,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAW1E,OAAO,CAAC,4BAA4B;cAWjB,WAAW,CAC5B,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,SAAS,CAAC,EAAE,MAAM,GACjB,IAAI;cAKkB,mBAAmB,CAC1C,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC;YAQF,iBAAiB;cAaN,kBAAkB,CACzC,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,EACjB,OAAO,CAAC,EAAE,2BAA2B,GACpC,OAAO,CAAC,IAAI,CAAC;cAiBG,kBAAkB,CACnC,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,2BAA2B,EACpC,MAAM,EAAE,6BAA6B,GACpC,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;cAOJ,eAAe,CAChC,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,EACb,UAAU,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE,2BAA2B,GACpC,IAAI;cAIkB,uBAAuB,CAC9C,OAAO,EAAE,8BAA8B,GACtC,OAAO,CAAC,2BAA2B,CAAC;IAWvC;;;OAGG;IACH,OAAO,CAAC,oBAAoB;IA+C5B;;;;;;OAMG;IACH,OAAO,CAAC,mBAAmB;IAO3B;;;;;;;;OAQG;IACH,OAAO,CAAC,+BAA+B;IASvC;;;OAGG;IACH,OAAO,CAAC,uBAAuB;IA8C/B;;;;OAIG;IACH,OAAO,CAAC,oBAAoB;IAU5B;;;;OAIG;IACH,OAAO,CAAC,cAAc;IAyFtB;;;;;;;;;;OAUG;YACW,WAAW;IA+GzB,OAAO,CAAC,SAAS;CAkLlB"}
1
+ {"version":3,"file":"DingtalkAdapter.d.ts","sourceRoot":"","sources":["../src/DingtalkAdapter.ts"],"names":[],"mappings":"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"}
@@ -5,9 +5,10 @@ import { tmpdir } from 'node:os';
5
5
  import { Buffer } from 'node:buffer';
6
6
  import { DWClient, TOPIC_CARD, TOPIC_ROBOT, EventAck, } from 'dingtalk-stream-sdk-nodejs';
7
7
  import { ChannelBase, isTerminalTaskLifecycleType, sanitizeLogText, sanitizePromptText, sanitizeSenderName, truncateUtf16Units, } from '@qwen-code/channel-base';
8
- import { normalizeDingTalkMarkdown, extractTitle } from './markdown.js';
8
+ import { DINGTALK_CHUNK_LIMIT, escapeDingTalkMarkdown, normalizeDingTalkMarkdown, extractTitle, } from './markdown.js';
9
9
  import { downloadMedia } from './media.js';
10
10
  import { DingTalkMediaUploadError, findImageMarkers, readValidatedImage, replaceImageMarkers, uploadDingTalkImage, } from './outbound-image.js';
11
+ import { OutboundFileProjector, projectFileText, withFileUnavailableNotice, } from './outbound-file.js';
11
12
  import { DingtalkConnectionManager, } from './DingtalkConnectionManager.js';
12
13
  import { DingtalkInteractiveCardClient } from './interactive-card-client.js';
13
14
  import { parseDingtalkCardActorId, parseDingtalkCardCallback, parseDingtalkInteractiveCardConfig, } from './interactive-card-types.js';
@@ -418,6 +419,7 @@ const DIRECT_MSG_API = 'https://api.dingtalk.com/v1.0/robot/oToMessages/batchSen
418
419
  const PROACTIVE_MSG_KEY = 'sampleMarkdown'; // DingTalk's built-in {title, text} markdown template key
419
420
  const TOKEN_API = 'https://oapi.dingtalk.com/gettoken';
420
421
  const PROACTIVE_FETCH_TIMEOUT_MS = 15_000;
422
+ const REPLY_FETCH_TIMEOUT_MS = 15_000;
421
423
  // Extensions for generated media store names, keyed by the download's mime
422
424
  // type. The agent reads stored media via `read_file`, whose type detection is
423
425
  // extension-first: an extensionless name falls through to the binary content
@@ -495,6 +497,18 @@ export class DingtalkChannel extends ChannelBase {
495
497
  inboundCardOwners = new Map();
496
498
  cardRunBySession = new Map();
497
499
  cardRuns = new Map();
500
+ // Keyed by runId, not segmentId: a mid-turn segment reset (response
501
+ // boundary, input requested) mints a fresh segment UUID but the projection
502
+ // state must survive it, or a marker split across the reset leaks.
503
+ fileProjectors = new Map();
504
+ blockFileProjectors = new Map();
505
+ // Sessions armed for block projection by onPromptStart and disarmed when
506
+ // the turn settles (or the session dies). A block send that finds NO
507
+ // projector state is only legitimate as a turn's FIRST block, which always
508
+ // lands while armed: late sends from an evicted (/clear) or dead session
509
+ // must be dropped, because recreating state would post the tail of a
510
+ // force-split [FILE: ...] marker verbatim.
511
+ blockProjectionArmed = new Set();
498
512
  constructor(name, config, bridge, options) {
499
513
  super(name, config, bridge, options);
500
514
  this.atSender =
@@ -542,7 +556,7 @@ export class DingtalkChannel extends ChannelBase {
542
556
  this.questionCardController = new QuestionCardController({
543
557
  client: this.interactiveCardClient,
544
558
  timeoutMs: this.interactiveCardConfig.questionCard.timeoutMs,
545
- sendFallback: (chatId, text) => this.sendMessage(chatId, text),
559
+ sendFallback: (chatId, text, sourceLabel) => this.sendReply(chatId, text, undefined, sourceLabel),
546
560
  reserveRunProjection: (runId) => this.interactionPresenter?.reserveProjection(runId),
547
561
  onError: (operation, error) => {
548
562
  process.stderr.write(`[DingTalk:${this.name}] ${operation} failed: ${sanitizeLogText(String(error), 300)}\n`);
@@ -555,7 +569,7 @@ export class DingtalkChannel extends ChannelBase {
555
569
  questionCards: this.questionCardController,
556
570
  ...(config.blockStreaming !== 'on'
557
571
  ? {
558
- sendFallback: (chatId, text, sessionId) => this.sendFallbackReply(chatId, text, sessionId),
572
+ sendFallback: (chatId, text, sessionId, sourceLabel) => this.sendFallbackReply(chatId, text, sessionId, sourceLabel),
559
573
  }
560
574
  : {}),
561
575
  });
@@ -744,10 +758,27 @@ export class DingtalkChannel extends ChannelBase {
744
758
  static isUnroutableGroupMessage(isGroup, conversationId) {
745
759
  return isGroup && !conversationId;
746
760
  }
747
- async prepareOutgoingText(text) {
748
- const markers = findImageMarkers(text);
761
+ projectOutgoingFileText(text, streamed) {
762
+ const projection = projectFileText(text);
763
+ // Markers are counted when their opening bytes arrive, so the streamed
764
+ // count also fails closed when the final text no longer carries a marker
765
+ // the stream already delivered. Whole-turn hash comparison is NOT
766
+ // viable: the bridges return only post-last-boundary chunks as the final
767
+ // text, so any routine multi-tool turn would diverge by construction.
768
+ const streamedMarkers = streamed ? streamed.result('').markerCount : 0;
769
+ if (projection.markerCount === 0 && streamedMarkers === 0) {
770
+ return projection.text;
771
+ }
772
+ // Counts only — never paths — so a redaction event stays debuggable
773
+ // without leaking what was redacted.
774
+ process.stderr.write(`[DingTalk:${this.name}] file markers redacted (final=${projection.markerCount}, streamed=${streamedMarkers})\n`);
775
+ return withFileUnavailableNotice(projection.text);
776
+ }
777
+ async prepareOutgoingText(text, streamed) {
778
+ const fileSafeText = this.projectOutgoingFileText(text, streamed);
779
+ const markers = findImageMarkers(fileSafeText);
749
780
  if (markers.length === 0)
750
- return text;
781
+ return fileSafeText;
751
782
  const replacements = [];
752
783
  for (const marker of markers) {
753
784
  const fileName = basename(marker.path)
@@ -784,9 +815,9 @@ export class DingtalkChannel extends ChannelBase {
784
815
  replacements.push(`[Image delivery failed: ${fileName}]`);
785
816
  }
786
817
  }
787
- return replaceImageMarkers(text, markers, replacements);
818
+ return replaceImageMarkers(fileSafeText, markers, replacements);
788
819
  }
789
- async sendReply(chatId, text, atUserId) {
820
+ async sendReply(chatId, text, atUserId, sourceLabel) {
790
821
  // chatId is a conversationId — resolve to the latest sessionWebhook
791
822
  const webhook = this.webhooks.get(chatId);
792
823
  if (!webhook) {
@@ -795,7 +826,14 @@ export class DingtalkChannel extends ChannelBase {
795
826
  }
796
827
  const outgoingText = await this.prepareOutgoingText(text);
797
828
  const mentionPrefix = atUserId ? `@${atUserId}\n\n` : '';
798
- const chunks = normalizeDingTalkMarkdown(mentionPrefix + outgoingText);
829
+ const sourcePrefix = sourceLabel && outgoingText.trim().length > 0
830
+ ? `${escapeDingTalkMarkdown(sourceLabel)}\n\n`
831
+ : '';
832
+ const contentLimit = DINGTALK_CHUNK_LIMIT - mentionPrefix.length - sourcePrefix.length;
833
+ if (contentLimit <= 0) {
834
+ throw new Error('DingTalk source label exceeds the message limit.');
835
+ }
836
+ const chunks = normalizeDingTalkMarkdown(outgoingText, contentLimit).map((chunk, index) => `${index === 0 ? mentionPrefix : ''}${sourcePrefix}${chunk}`);
799
837
  const title = extractTitle(outgoingText);
800
838
  for (let i = 0; i < chunks.length; i++) {
801
839
  const chunk = chunks[i];
@@ -808,11 +846,19 @@ export class DingtalkChannel extends ChannelBase {
808
846
  },
809
847
  ...(isMention ? { at: { atUserIds: [atUserId] } } : {}),
810
848
  };
811
- const resp = await fetch(webhook, {
812
- method: 'POST',
813
- headers: { 'Content-Type': 'application/json' },
814
- body: JSON.stringify(body),
815
- });
849
+ let resp;
850
+ try {
851
+ resp = await fetch(webhook, {
852
+ method: 'POST',
853
+ headers: { 'Content-Type': 'application/json' },
854
+ body: JSON.stringify(body),
855
+ signal: AbortSignal.timeout(REPLY_FETCH_TIMEOUT_MS),
856
+ });
857
+ }
858
+ catch (err) {
859
+ process.stderr.write(`[DingTalk:${this.name}] sendMessage failed: ${sanitizeLogText(err instanceof Error ? err.message : String(err), 300)}\n`);
860
+ throw err;
861
+ }
816
862
  if (isMention && process.env['QWEN_CHANNEL_DEBUG_MENTIONS'] === '1') {
817
863
  const payload = (await resp
818
864
  .clone()
@@ -836,6 +882,9 @@ export class DingtalkChannel extends ChannelBase {
836
882
  async sendMessage(chatId, text) {
837
883
  await this.sendReply(chatId, text);
838
884
  }
885
+ async sendThreadMessage(chatId, _threadId, text, sourceLabel) {
886
+ await this.sendReply(chatId, text, undefined, sourceLabel);
887
+ }
839
888
  supportsProactiveSend() {
840
889
  return true;
841
890
  }
@@ -860,11 +909,18 @@ export class DingtalkChannel extends ChannelBase {
860
909
  * Single-shot cold send: a failed chunk aborts the remainder (already-sent
861
910
  * chunks are not recalled) and the error surfaces in the loop's lastError.
862
911
  */
863
- async pushProactive(target, text) {
912
+ async pushProactive(target, text, sourceLabel) {
864
913
  if (!text.trim())
865
914
  return;
866
915
  const outgoingText = await this.prepareOutgoingText(text);
867
- const chunks = normalizeDingTalkMarkdown(outgoingText);
916
+ const sourcePrefix = sourceLabel
917
+ ? `${escapeDingTalkMarkdown(sourceLabel)}\n\n`
918
+ : '';
919
+ const contentLimit = DINGTALK_CHUNK_LIMIT - sourcePrefix.length;
920
+ if (contentLimit <= 0) {
921
+ throw new Error('DingTalk source label exceeds the message limit.');
922
+ }
923
+ const chunks = normalizeDingTalkMarkdown(outgoingText, contentLimit).map((chunk) => `${sourcePrefix}${chunk}`);
868
924
  const title = extractTitle(outgoingText);
869
925
  for (let i = 0; i < chunks.length; i++) {
870
926
  await this.sendProactiveChunk(target, i === 0 ? title : `${title} (cont.)`, chunks[i], `chunk ${i + 1}/${chunks.length}`);
@@ -1120,6 +1176,12 @@ export class DingtalkChannel extends ChannelBase {
1120
1176
  }
1121
1177
  /** Recall reactions left behind when a session dies without terminal lifecycle events. */
1122
1178
  onSessionDied(sessionId) {
1179
+ this.blockProjectionArmed.delete(sessionId);
1180
+ this.blockFileProjectors.delete(sessionId);
1181
+ for (const [runId, state] of this.fileProjectors) {
1182
+ if (state.sessionId === sessionId)
1183
+ this.fileProjectors.delete(runId);
1184
+ }
1123
1185
  const bufferedTargets = this.bufferedMentionTargetsBySession.get(sessionId);
1124
1186
  if (bufferedTargets) {
1125
1187
  this.bufferedMentionTargetsBySession.delete(sessionId);
@@ -1161,7 +1223,8 @@ export class DingtalkChannel extends ChannelBase {
1161
1223
  inboundOwner?.ownerId === event.owner.id) {
1162
1224
  this.cardRuns.set(event.runId, inboundOwner);
1163
1225
  this.cardRunBySession.set(event.sessionId, event.runId);
1164
- this.interactionPresenter?.registerRun(event.runId, event.owner.id, inboundOwner.target, event.sessionId, inboundOwner.sender);
1226
+ const sourceLabel = this.getResponseSourceLabel(event.sessionId);
1227
+ this.interactionPresenter?.registerRun(event.runId, event.owner.id, inboundOwner.target, event.sessionId, inboundOwner.sender, ...(sourceLabel ? [sourceLabel] : []));
1165
1228
  this.interactionPresenter?.startStatusCard(event.runId);
1166
1229
  }
1167
1230
  return;
@@ -1171,6 +1234,7 @@ export class DingtalkChannel extends ChannelBase {
1171
1234
  this.mentionTargets.delete(event.messageId);
1172
1235
  this.stopReaction(event.chatId, event.messageId, event.sessionId);
1173
1236
  if (event.runId) {
1237
+ this.deleteFileProjectorsForRun(event.runId);
1174
1238
  if (event.type === 'failed') {
1175
1239
  this.interactionPresenter?.terminalizeRun(event.runId, 'failed', event.error);
1176
1240
  }
@@ -1215,6 +1279,7 @@ export class DingtalkChannel extends ChannelBase {
1215
1279
  }
1216
1280
  }
1217
1281
  onPromptStart(chatId, sessionId, messageId) {
1282
+ this.blockProjectionArmed.add(sessionId);
1218
1283
  if (messageId) {
1219
1284
  this.bufferedMentionTargets.delete(messageId);
1220
1285
  this.untrackBufferedMentionTarget(sessionId, messageId);
@@ -1229,34 +1294,36 @@ export class DingtalkChannel extends ChannelBase {
1229
1294
  async handleInbound(envelope) {
1230
1295
  if (!(await this.preflightInbound(envelope)))
1231
1296
  return;
1232
- const messageId = envelope.messageId;
1233
- if (messageId && envelope.senderId) {
1234
- this.inboundCardOwners.delete(messageId);
1235
- this.inboundCardOwners.set(messageId, {
1236
- ownerId: envelope.senderId,
1237
- target: {
1238
- chatId: envelope.chatId,
1239
- isGroup: envelope.isGroup,
1240
- },
1241
- ...(this.atSender && envelope.isGroup
1242
- ? {
1243
- sender: {
1244
- senderName: envelope.senderName,
1245
- },
1246
- }
1247
- : {}),
1248
- });
1249
- if (this.inboundCardOwners.size > 1000) {
1250
- const oldest = this.inboundCardOwners.keys().next().value;
1251
- if (oldest !== undefined)
1252
- this.inboundCardOwners.delete(oldest);
1297
+ await this.processPreflightedInbound(envelope, async () => {
1298
+ const messageId = envelope.messageId;
1299
+ if (messageId && envelope.senderId) {
1300
+ this.inboundCardOwners.delete(messageId);
1301
+ this.inboundCardOwners.set(messageId, {
1302
+ ownerId: envelope.senderId,
1303
+ target: {
1304
+ chatId: envelope.chatId,
1305
+ isGroup: envelope.isGroup,
1306
+ },
1307
+ ...(this.atSender && envelope.isGroup
1308
+ ? {
1309
+ sender: {
1310
+ senderName: envelope.senderName,
1311
+ },
1312
+ }
1313
+ : {}),
1314
+ });
1315
+ if (this.inboundCardOwners.size > 1000) {
1316
+ const oldest = this.inboundCardOwners.keys().next().value;
1317
+ if (oldest !== undefined)
1318
+ this.inboundCardOwners.delete(oldest);
1319
+ }
1253
1320
  }
1254
- }
1255
- const atUserId = envelope[mentionTarget];
1256
- if (this.atSender && messageId && atUserId) {
1257
- this.mentionTargets.set(messageId, atUserId);
1258
- }
1259
- await this.processInbound(envelope);
1321
+ const atUserId = envelope[mentionTarget];
1322
+ if (this.atSender && messageId && atUserId) {
1323
+ this.mentionTargets.set(messageId, atUserId);
1324
+ }
1325
+ await this.processInbound(envelope);
1326
+ });
1260
1327
  }
1261
1328
  async processInbound(envelope) {
1262
1329
  const messageId = envelope.messageId;
@@ -1278,35 +1345,116 @@ export class DingtalkChannel extends ChannelBase {
1278
1345
  this.bufferedMentionTargetsBySession.delete(sessionId);
1279
1346
  }
1280
1347
  onPromptEnd(chatId, sessionId, messageId) {
1348
+ this.settleBlockFileProjector(chatId, sessionId);
1281
1349
  this.sessionMentionTargets.delete(sessionId);
1282
1350
  this.stopReaction(chatId, messageId, sessionId);
1283
1351
  }
1284
- async sendResponseMessage(chatId, text, sessionId) {
1285
- const atUserId = this.atSender
1352
+ /**
1353
+ * Turn end is the only point where the block projector's held state can be
1354
+ * settled: ChannelBase drains the turn's queued block sends before calling
1355
+ * onPromptEnd, so everything already appended belongs to this turn. Flush
1356
+ * the held candidate bytes (a trailing `[FILE:` prefix the stream never
1357
+ * completed) before deleting the entry — a later delete-without-settle
1358
+ * would silently drop them from the delivered answer. Also disarm the
1359
+ * session: /clear eviction and session death settle WITHOUT draining the
1360
+ * turn's send chain, and any block that lands afterwards must be dropped
1361
+ * rather than recreating projector state.
1362
+ */
1363
+ settleBlockFileProjector(chatId, sessionId) {
1364
+ this.blockProjectionArmed.delete(sessionId);
1365
+ const state = this.blockFileProjectors.get(sessionId);
1366
+ if (!state)
1367
+ return;
1368
+ this.blockFileProjectors.delete(sessionId);
1369
+ const tail = state.projector.complete();
1370
+ if (!tail.trim())
1371
+ return;
1372
+ // Deliberately fire-and-forget: complete() can only return a strict
1373
+ // prefix of '[FILE:' (at most 5 chars, no path bytes), so the worst case
1374
+ // is a stray fragment landing out of order with the next turn — not a
1375
+ // leak — and blocking settle on a delivery that may hang is worse.
1376
+ void this.sendReply(chatId, tail, undefined, this.getResponseSourceLabel(sessionId)).catch((err) => {
1377
+ process.stderr.write(`[DingTalk:${this.name}] projector tail delivery failed for session ${sessionId}: ${err instanceof Error ? err.message : String(err)}\n`);
1378
+ });
1379
+ }
1380
+ /**
1381
+ * Out-of-turn one-shot sends (background responses) must not flow through
1382
+ * the session's block-streaming projector: a second sender interleaving with
1383
+ * mid-projection state can swallow the send or split a held marker.
1384
+ */
1385
+ async deliverBackgroundReply(chatId, text, sessionId, sourceLabel) {
1386
+ if (this.config.blockStreaming !== 'on') {
1387
+ return super.deliverBackgroundReply(chatId, text, sessionId, sourceLabel);
1388
+ }
1389
+ await this.sendReply(chatId, text, undefined, sourceLabel);
1390
+ }
1391
+ async sendResponseMessage(chatId, text, sessionId, sourceLabel) {
1392
+ let outgoingText = text;
1393
+ let consumesMention = true;
1394
+ if (this.config.blockStreaming === 'on') {
1395
+ const projected = this.projectBlockStreamChunk(text, sessionId);
1396
+ if (!projected.text.trim())
1397
+ return;
1398
+ outgoingText = projected.text;
1399
+ // A notice-only block must not consume the prompt's mention target:
1400
+ // the @mention belongs to the block carrying the actual answer.
1401
+ consumesMention = projected.hasContent;
1402
+ }
1403
+ const atUserId = consumesMention && this.atSender
1286
1404
  ? this.sessionMentionTargets.get(sessionId)
1287
1405
  : undefined;
1288
1406
  if (atUserId)
1289
1407
  this.sessionMentionTargets.delete(sessionId);
1290
- await this.sendReply(chatId, text, atUserId);
1408
+ await this.sendReply(chatId, outgoingText, atUserId, sourceLabel ?? this.getResponseSourceLabel(sessionId));
1409
+ }
1410
+ projectBlockStreamChunk(text, sessionId) {
1411
+ let state = this.blockFileProjectors.get(sessionId);
1412
+ if (!state) {
1413
+ if (!this.blockProjectionArmed.has(sessionId)) {
1414
+ return { text: '', hasContent: false };
1415
+ }
1416
+ state = { projector: new OutboundFileProjector(), reportedMarkers: 0 };
1417
+ this.blockFileProjectors.set(sessionId, state);
1418
+ }
1419
+ const safe = state.projector.append(text);
1420
+ const hasContent = safe.trim().length > 0;
1421
+ const result = state.projector.result(safe);
1422
+ let outgoingText = safe;
1423
+ if (result.markerCount > state.reportedMarkers) {
1424
+ outgoingText = withFileUnavailableNotice(safe);
1425
+ state.reportedMarkers = result.markerCount;
1426
+ process.stderr.write(`[DingTalk:${this.name}] file markers redacted in block stream (session ${sessionId}, markers=${result.markerCount})\n`);
1427
+ }
1428
+ return { text: outgoingText, hasContent };
1291
1429
  }
1292
- async sendFallbackReply(chatId, text, sessionId) {
1430
+ async sendFallbackReply(chatId, text, sessionId, sourceLabel) {
1293
1431
  // Mid-run fallbacks must not consume the prompt's mention target: the
1294
1432
  // final answer of the same run still needs it.
1295
1433
  const atUserId = this.atSender
1296
1434
  ? this.sessionMentionTargets.get(sessionId)
1297
1435
  : undefined;
1298
- await this.sendReply(chatId, text, atUserId);
1436
+ await this.sendReply(chatId, text, atUserId, sourceLabel);
1299
1437
  }
1300
1438
  async onResponseComplete(chatId, text, sessionId, segment) {
1439
+ const streamed = segment
1440
+ ? this.fileProjectors.get(segment.runId)?.projector
1441
+ : undefined;
1442
+ if (segment)
1443
+ this.fileProjectors.delete(segment.runId);
1444
+ const outgoingText = await this.prepareOutgoingText(text, streamed);
1301
1445
  if (segment && this.interactionPresenter) {
1302
- const outgoingText = await this.prepareOutgoingText(text);
1303
1446
  if (await this.interactionPresenter.closeOutput(segment.segmentId, outgoingText, 'completed', segment)) {
1304
1447
  return;
1305
1448
  }
1306
1449
  }
1307
- await this.sendResponseMessage(chatId, text, sessionId);
1450
+ await this.sendResponseMessage(chatId, outgoingText, sessionId, segment?.sourceLabel);
1308
1451
  }
1309
1452
  onOutputSegmentEnd(_chatId, _sessionId, segment, reason) {
1453
+ if (reason === 'completed' ||
1454
+ reason === 'failed' ||
1455
+ reason === 'cancelled') {
1456
+ this.fileProjectors.delete(segment.runId);
1457
+ }
1310
1458
  if (!this.interactionPresenter)
1311
1459
  return;
1312
1460
  return this.interactionPresenter
@@ -1314,8 +1462,22 @@ export class DingtalkChannel extends ChannelBase {
1314
1462
  .then(() => undefined);
1315
1463
  }
1316
1464
  onResponseChunk(_chatId, chunk, _sessionId, segment) {
1317
- if (segment)
1318
- this.interactionPresenter?.appendOutput(segment, chunk);
1465
+ if (!segment)
1466
+ return;
1467
+ let state = this.fileProjectors.get(segment.runId);
1468
+ if (!state) {
1469
+ state = {
1470
+ sessionId: segment.sessionId,
1471
+ projector: new OutboundFileProjector(),
1472
+ };
1473
+ this.fileProjectors.set(segment.runId, state);
1474
+ }
1475
+ const safe = state.projector.append(chunk);
1476
+ if (safe)
1477
+ this.interactionPresenter?.appendOutput(segment, safe);
1478
+ }
1479
+ deleteFileProjectorsForRun(runId) {
1480
+ this.fileProjectors.delete(runId);
1319
1481
  }
1320
1482
  async presentUserInputRequest(context) {
1321
1483
  const run = this.cardRuns.get(context.runId);
@@ -1558,13 +1720,7 @@ export class DingtalkChannel extends ChannelBase {
1558
1720
  const media = await downloadMedia(downloadCode, robotCode, token);
1559
1721
  if (!media)
1560
1722
  return;
1561
- // ChannelBase fills a single imageBase64 slot from the FIRST data-only
1562
- // image attachment and silently drops every later one, so an image
1563
- // arriving after the slot is taken (e.g. a quoted picture alongside the
1564
- // message's own picture) falls through to the file-backed path — the
1565
- // `saved to:` prompt line is what keeps it reachable for the agent.
1566
- const inlineImageSlotFree = !(envelope.attachments || []).some((attachment) => attachment.type === 'image' && attachment.data);
1567
- if (mediaType === 'image' && inlineImageSlotFree) {
1723
+ if (mediaType === 'image') {
1568
1724
  const mimeType = media.mimeType.startsWith('image/')
1569
1725
  ? media.mimeType
1570
1726
  : 'image/jpeg';
@@ -1725,20 +1881,27 @@ export class DingtalkChannel extends ChannelBase {
1725
1881
  if (this.atSender && isGroup && senderStaffId) {
1726
1882
  envelope[mentionTarget] = senderStaffId;
1727
1883
  }
1728
- const processMessage = async () => {
1729
- // Download media if present (first downloadCode only for images)
1730
- if (content.downloadCodes.length > 0 && content.mediaType) {
1731
- await this.attachMedia(envelope, content.downloadCodes[0], content.mediaType, content.fileName, content.placeholder);
1732
- }
1733
- if (quoted.media) {
1734
- await this.attachMedia(envelope, quoted.media.downloadCode, quoted.media.mediaType, quoted.media.fileName);
1735
- }
1736
- await this.handleInbound(envelope);
1737
- };
1738
- // Don't await — stream callback should return quickly
1739
- processMessage().catch((err) => {
1884
+ const processMessage = content.downloadCodes.length > 0 || quoted.media
1885
+ ? this.prepareThenHandleInbound(envelope, async () => {
1886
+ // Download media in callback order.
1887
+ if (content.downloadCodes.length > 0 && content.mediaType) {
1888
+ for (const downloadCode of content.downloadCodes) {
1889
+ await this.attachMedia(envelope, downloadCode, content.mediaType, content.fileName, content.placeholder);
1890
+ }
1891
+ }
1892
+ if (quoted.media) {
1893
+ await this.attachMedia(envelope, quoted.media.downloadCode, quoted.media.mediaType, quoted.media.fileName);
1894
+ }
1895
+ })
1896
+ : this.handleInbound(envelope);
1897
+ processMessage.catch((err) => {
1898
+ // Don't await — stream callback should return quickly
1740
1899
  process.stderr.write(`[DingTalk:${this.name}] Error handling message: ${err}\n`);
1741
- this.sendMessage(chatId, 'Sorry, something went wrong processing your message.').catch(() => { });
1900
+ const sourceLabel = this.getInboundErrorSourceLabel(envelope);
1901
+ const delivery = sourceLabel
1902
+ ? this.sendThreadMessage(chatId, envelope.threadId, 'Sorry, something went wrong processing your message.', sourceLabel)
1903
+ : this.sendMessage(chatId, 'Sorry, something went wrong processing your message.');
1904
+ delivery.catch(() => { });
1742
1905
  });
1743
1906
  }
1744
1907
  catch (err) {