@qwen-code/channel-dingtalk 0.23.4 → 0.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.
@@ -1,7 +1,7 @@
1
1
  import { ChannelBase } from '@qwen-code/channel-base';
2
2
  import { DingtalkInteractiveCardClient } from './interactive-card-client.js';
3
3
  import { type DingtalkCardCallback, type DingtalkCardCallbackResult } from './interactive-card-types.js';
4
- import type { ChannelConfig, ChannelBaseOptions, Envelope, ChannelAgentBridge, ChannelOutputSegmentContext, ChannelOutputSegmentEndReason, ChannelPermissionRequestContext, ChannelTaskLifecycleEvent, ChannelUserInputRequestContext, SessionTarget, UserInputPresentationResult } from '@qwen-code/channel-base';
4
+ import type { BackgroundResponseContext, ChannelConfig, ChannelBaseOptions, Envelope, ChannelAgentBridge, ChannelOutputSegmentContext, ChannelOutputSegmentEndReason, ChannelPermissionRequestContext, ChannelTaskLifecycleEvent, ChannelUserInputRequestContext, SessionTarget, UserInputPresentationResult } from '@qwen-code/channel-base';
5
5
  export declare class DingtalkChannel extends ChannelBase {
6
6
  private client;
7
7
  private readonly atSender;
@@ -19,7 +19,7 @@ export declare class DingtalkChannel extends ChannelBase {
19
19
  private reactionStates;
20
20
  /** sessionId → reaction keys, so a dead session's reactions can be recalled. */
21
21
  private sessionReactionKeys;
22
- /** Settles when the reaction cleanup queued by disconnect() has run. */
22
+ /** Settles after background output and reaction cleanup finish on disconnect. */
23
23
  private disconnectDrain;
24
24
  /**
25
25
  * Real inbound message ids (insertion-ordered, size-capped). Unlike the
@@ -33,6 +33,7 @@ export declare class DingtalkChannel extends ChannelBase {
33
33
  */
34
34
  private proactiveToken?;
35
35
  private readonly interactiveCardConfig;
36
+ private readonly outputMode?;
36
37
  protected readonly interactiveCardClient?: DingtalkInteractiveCardClient;
37
38
  private statusCardController?;
38
39
  private questionCardController?;
@@ -42,6 +43,7 @@ export declare class DingtalkChannel extends ChannelBase {
42
43
  private readonly cardRunBySession;
43
44
  private readonly cardRuns;
44
45
  private readonly fileProjectors;
46
+ private readonly backgroundOutputCoordinator;
45
47
  constructor(name: string, config: ChannelConfig, bridge: ChannelAgentBridge, options?: ChannelBaseOptions);
46
48
  private createClient;
47
49
  private installStructuredDownstreamHandler;
@@ -119,6 +121,7 @@ export declare class DingtalkChannel extends ChannelBase {
119
121
  * the sessions on a fresh bridge.
120
122
  */
121
123
  onBridgeDisconnected(): void;
124
+ protected onSessionRetiring(sessionId: string): void;
122
125
  protected onTaskLifecycle(event: ChannelTaskLifecycleEvent): void;
123
126
  protected onPromptBufferDropped(_chatId: string, sessionId: string, messageIds: string[]): void;
124
127
  protected onPromptBufferDrained(_chatId: string, sessionId: string, messageIds: string[]): void;
@@ -128,7 +131,28 @@ export declare class DingtalkChannel extends ChannelBase {
128
131
  protected processInbound(envelope: Envelope): Promise<void>;
129
132
  private untrackBufferedMentionTarget;
130
133
  protected onPromptEnd(chatId: string, sessionId: string, messageId?: string): void;
131
- protected sendResponseMessage(chatId: string, text: string, sessionId: string, sourceLabel?: string, prepared?: boolean): Promise<void>;
134
+ dispatchBackgroundResponse(sessionId: string, text: string, context?: BackgroundResponseContext): Promise<void>;
135
+ private createBackgroundOutputDelivery;
136
+ private formatBackgroundOutputHeader;
137
+ private formatBackgroundTaskLabel;
138
+ private prepareBackgroundOutput;
139
+ protected deliverBackgroundResponseToTarget(sessionId: string, text: string, delivery: {
140
+ target: SessionTarget;
141
+ sourceLabel?: string;
142
+ }, prepared?: boolean, result?: {
143
+ status: string;
144
+ partial?: boolean;
145
+ }): Promise<void>;
146
+ /**
147
+ * Body-identical to the base implementation — this override exists only to
148
+ * widen the signature, which the base seam does not declare. The background
149
+ * output delivery passes `prepared` so the body it already projected isn't
150
+ * projected a second time (that would re-upload its files), and
151
+ * `failOnHttpError` so a failed send throws instead of being swallowed,
152
+ * letting the flush capture the delivery plan for the next retry.
153
+ */
154
+ protected deliverBackgroundReply(chatId: string, text: string, sessionId: string, sourceLabel?: string, prepared?: boolean, failOnHttpError?: boolean): Promise<void>;
155
+ protected sendResponseMessage(chatId: string, text: string, sessionId: string, sourceLabel?: string, prepared?: boolean, failOnHttpError?: boolean): Promise<void>;
132
156
  private sendFallbackReply;
133
157
  protected onResponseComplete(chatId: string, text: string, sessionId: string, segment?: ChannelOutputSegmentContext): Promise<void>;
134
158
  protected onOutputSegmentEnd(_chatId: string, _sessionId: string, segment: ChannelOutputSegmentContext, reason: ChannelOutputSegmentEndReason): void | Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"DingtalkAdapter.d.ts","sourceRoot":"","sources":["../src/DingtalkAdapter.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"DingtalkAdapter.d.ts","sourceRoot":"","sources":["../src/DingtalkAdapter.ts"],"names":[],"mappings":"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"}
@@ -4,16 +4,16 @@ import { basename, join } from 'node:path';
4
4
  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
- import { ChannelBase, isTerminalTaskLifecycleType, sanitizeLogText, sanitizePromptText, sanitizeSenderName, truncateUtf16Units, } from '@qwen-code/channel-base';
7
+ import { ChannelBase, BackgroundOutputCoordinator, parseChannelOutputMode, isTerminalTaskLifecycleType, sanitizeLogText, sanitizePromptText, sanitizeSenderName, truncateUtf16Units, } from '@qwen-code/channel-base';
8
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
11
  import { FILE_UNAVAILABLE_NOTICE, OutboundFileProjector, projectFileText, readValidatedFile, safeFileName, uploadDingTalkFile, } from './outbound-file.js';
12
12
  import { DingtalkConnectionManager, } from './DingtalkConnectionManager.js';
13
- import { DingtalkCardRequestError, DingtalkInteractiveCardClient, } from './interactive-card-client.js';
13
+ import { DingtalkCardRequestError, DingtalkInteractiveCardClient, isRetryableDingtalkStatus, } from './interactive-card-client.js';
14
14
  import { parseDingtalkCardActorId, parseDingtalkCardCallback, parseDingtalkInteractiveCardConfig, } from './interactive-card-types.js';
15
15
  import { StatusCardController } from './status-card-controller.js';
16
- import { isChinesePresentationLanguage, lifecyclePresentationPhase, presentationPhaseLabel, } from './presentation-phase.js';
16
+ import { isChinesePresentationLanguage, lifecyclePresentationPhase, markPartialOutput, partialOutputLabel, presentationPhaseLabel, } from './presentation-phase.js';
17
17
  import { QuestionCardController } from './question-card-controller.js';
18
18
  import { PermissionCardController } from './permission-card-controller.js';
19
19
  import { DingtalkInteractionPresenter } from './interaction-presenter.js';
@@ -413,6 +413,10 @@ function formatChatRecord(content, budget = MAX_CHAT_RECORD_CHARS) {
413
413
  }
414
414
  /** Track seen msgIds to deduplicate retried callbacks. */
415
415
  const DEDUP_TTL_MS = 5 * 60 * 1000; // 5 minutes
416
+ /** The charset ChannelBase's own slash classifier requires of a leading
417
+ * `/token`. Slash-prefixed prose — a path, a `//` or block comment — fails it
418
+ * and stays prose. */
419
+ const SLASH_COMMAND_TOKEN_RE = /^[a-zA-Z0-9_:-]+$/;
416
420
  const ACK_REACTION_NAME = '👀';
417
421
  const ACK_EMOTION_ID = '2659900';
418
422
  const ACK_EMOTION_BG_ID = 'im_bg_1';
@@ -625,6 +629,28 @@ async function withConnectLoggingSuppressed(connect) {
625
629
  }
626
630
  }
627
631
  }
632
+ class ProactiveTextDeliveryError extends Error {
633
+ plan;
634
+ retryable;
635
+ constructor(plan, cause) {
636
+ super(cause instanceof Error ? cause.message : String(cause), { cause });
637
+ this.plan = plan;
638
+ if (cause instanceof DingtalkCardRequestError) {
639
+ this.retryable = cause.retryable;
640
+ }
641
+ }
642
+ }
643
+ class ReplyTextDeliveryError extends Error {
644
+ plan;
645
+ retryable;
646
+ constructor(plan, cause) {
647
+ super(cause instanceof Error ? cause.message : String(cause), { cause });
648
+ this.plan = plan;
649
+ if (cause instanceof DingtalkCardRequestError) {
650
+ this.retryable = cause.retryable;
651
+ }
652
+ }
653
+ }
628
654
  export class DingtalkChannel extends ChannelBase {
629
655
  client;
630
656
  atSender;
@@ -642,7 +668,7 @@ export class DingtalkChannel extends ChannelBase {
642
668
  reactionStates = new Map();
643
669
  /** sessionId → reaction keys, so a dead session's reactions can be recalled. */
644
670
  sessionReactionKeys = new Map();
645
- /** Settles when the reaction cleanup queued by disconnect() has run. */
671
+ /** Settles after background output and reaction cleanup finish on disconnect. */
646
672
  disconnectDrain;
647
673
  /**
648
674
  * Real inbound message ids (insertion-ordered, size-capped). Unlike the
@@ -656,6 +682,7 @@ export class DingtalkChannel extends ChannelBase {
656
682
  */
657
683
  proactiveToken;
658
684
  interactiveCardConfig;
685
+ outputMode;
659
686
  interactiveCardClient;
660
687
  statusCardController;
661
688
  questionCardController;
@@ -668,6 +695,7 @@ export class DingtalkChannel extends ChannelBase {
668
695
  // boundary, input requested) mints a fresh segment UUID but the projection
669
696
  // state must survive it, or a marker split across the reset leaks.
670
697
  fileProjectors = new Map();
698
+ backgroundOutputCoordinator;
671
699
  constructor(name, config, bridge, options) {
672
700
  super(name, config, bridge, options);
673
701
  this.atSender =
@@ -688,6 +716,19 @@ export class DingtalkChannel extends ChannelBase {
688
716
  this.config.instructions += FILE_INSTRUCTIONS;
689
717
  }
690
718
  this.interactiveCardConfig = parseDingtalkInteractiveCardConfig(config.interactiveCards);
719
+ this.outputMode = parseChannelOutputMode(name, config.outputMode, true);
720
+ this.backgroundOutputCoordinator = new BackgroundOutputCoordinator({
721
+ outputMode: this.outputMode,
722
+ getTarget: (sessionId) => this.router.getTarget(sessionId),
723
+ getSourceLabel: (sessionId) => this.getBackgroundResponseSourceLabel(sessionId),
724
+ resolveDelivery: (sessionId) => this.resolveBackgroundResponseDelivery(sessionId),
725
+ createDelivery: (sessionId, target) => this.createBackgroundOutputDelivery(sessionId, target),
726
+ isRetryableError: (error) => !((error instanceof ProactiveTextDeliveryError ||
727
+ error instanceof ReplyTextDeliveryError ||
728
+ error instanceof DingtalkCardRequestError) &&
729
+ error.retryable === false),
730
+ log: (message) => process.stderr.write(`[DingTalk:${this.name}] ${message}`),
731
+ });
691
732
  if (!config.clientId || !config.clientSecret) {
692
733
  throw new Error(`Channel "${name}" requires clientId and clientSecret for DingTalk.`);
693
734
  }
@@ -744,20 +785,18 @@ export class DingtalkChannel extends ChannelBase {
744
785
  },
745
786
  });
746
787
  }
747
- if (this.statusCardController ||
748
- this.questionCardController ||
749
- this.permissionCardController) {
750
- this.interactionPresenter = new DingtalkInteractionPresenter({
751
- statusCards: this.statusCardController,
752
- questionCards: this.questionCardController,
753
- permissionCards: this.permissionCardController,
754
- ...(options?.displayLanguage
755
- ? { language: options.displayLanguage }
756
- : {}),
757
- sendFallback: (chatId, text, sessionId, sourceLabel) => this.sendFallbackReply(chatId, text, sessionId, sourceLabel),
758
- });
759
- }
760
788
  }
789
+ this.interactionPresenter = new DingtalkInteractionPresenter({
790
+ outputMode: this.outputMode,
791
+ prepareOutput: (chatId, text) => this.prepareReplyOutput(chatId, text),
792
+ statusCards: this.statusCardController,
793
+ questionCards: this.questionCardController,
794
+ permissionCards: this.permissionCardController,
795
+ ...(options?.displayLanguage
796
+ ? { language: options.displayLanguage }
797
+ : {}),
798
+ sendFallback: (chatId, text, sessionId, sourceLabel) => this.sendFallbackReply(chatId, text, sessionId, sourceLabel),
799
+ });
761
800
  if (useConnectionManager) {
762
801
  this.connectionManager = new DingtalkConnectionManager({
763
802
  initialClient: this.client,
@@ -1121,11 +1160,14 @@ export class DingtalkChannel extends ChannelBase {
1121
1160
  }
1122
1161
  return replaceImageMarkers(text, markers, replacements);
1123
1162
  }
1124
- async sendReply(chatId, text, atUserId, sourceLabel, prepared = false) {
1163
+ async sendReply(chatId, text, atUserId, sourceLabel, prepared = false, failOnHttpError = false) {
1125
1164
  // chatId is a conversationId — resolve to the latest sessionWebhook.
1126
1165
  const webhook = this.webhooks.get(chatId);
1127
1166
  if (!webhook) {
1128
1167
  process.stderr.write(`[DingTalk:${this.name}] No webhook for chatId ${chatId}, cannot send.\n`);
1168
+ if (failOnHttpError) {
1169
+ throw new Error('DingTalk session webhook unavailable');
1170
+ }
1129
1171
  return;
1130
1172
  }
1131
1173
  const outgoingText = prepared
@@ -1134,7 +1176,12 @@ export class DingtalkChannel extends ChannelBase {
1134
1176
  if (!outgoingText.trim())
1135
1177
  return;
1136
1178
  const plan = this.createReplyTextDelivery(outgoingText, atUserId, sourceLabel);
1137
- await this.deliverReplyText(chatId, plan);
1179
+ try {
1180
+ await this.deliverReplyText(chatId, plan, failOnHttpError);
1181
+ }
1182
+ catch (error) {
1183
+ throw new ReplyTextDeliveryError(plan, error);
1184
+ }
1138
1185
  }
1139
1186
  createReplyTextDelivery(outgoingText, atUserId, sourceLabel) {
1140
1187
  const mentionPrefix = atUserId ? `@${atUserId}\n\n` : '';
@@ -1153,9 +1200,12 @@ export class DingtalkChannel extends ChannelBase {
1153
1200
  ...(atUserId ? { atUserId } : {}),
1154
1201
  };
1155
1202
  }
1156
- async deliverReplyText(chatId, plan) {
1203
+ async deliverReplyText(chatId, plan, failOnHttpError = false) {
1157
1204
  const webhook = this.webhooks.get(chatId);
1158
1205
  if (!webhook) {
1206
+ if (failOnHttpError) {
1207
+ throw new Error('DingTalk session webhook unavailable');
1208
+ }
1159
1209
  return;
1160
1210
  }
1161
1211
  while (plan.nextChunk < plan.chunks.length) {
@@ -1200,6 +1250,24 @@ export class DingtalkChannel extends ChannelBase {
1200
1250
  if (!resp.ok) {
1201
1251
  const detail = await resp.text().catch(() => '');
1202
1252
  process.stderr.write(`[DingTalk:${this.name}] sendMessage failed: HTTP ${resp.status} ${detail}\n`);
1253
+ if (failOnHttpError) {
1254
+ throw new DingtalkCardRequestError(`DingTalk reply send failed: HTTP ${resp.status} ${detail}`, isRetryableDingtalkStatus(resp.status));
1255
+ }
1256
+ }
1257
+ else if (failOnHttpError) {
1258
+ const payload = (await resp
1259
+ .clone()
1260
+ .json()
1261
+ .catch(() => undefined));
1262
+ const response = payload && typeof payload === 'object'
1263
+ ? payload
1264
+ : undefined;
1265
+ const value = response?.['errcode'] ?? response?.['code'];
1266
+ if (value !== undefined && String(value) !== '0') {
1267
+ const detail = sanitizeLogText(String(response?.['errmsg'] ?? response?.['message'] ?? value), 300);
1268
+ process.stderr.write(`[DingTalk:${this.name}] sendMessage failed: ${detail}\n`);
1269
+ throw new Error(`DingTalk reply send failed: ${detail}`);
1270
+ }
1203
1271
  }
1204
1272
  plan.nextChunk++;
1205
1273
  }
@@ -1240,7 +1308,12 @@ export class DingtalkChannel extends ChannelBase {
1240
1308
  const plan = await this.createProactiveTextDelivery(target, text, sourceLabel);
1241
1309
  if (!plan)
1242
1310
  return;
1243
- await this.deliverProactiveText(target, plan);
1311
+ try {
1312
+ await this.deliverProactiveText(target, plan);
1313
+ }
1314
+ catch (error) {
1315
+ throw new ProactiveTextDeliveryError(plan, error);
1316
+ }
1244
1317
  }
1245
1318
  async createProactiveTextDelivery(target, text, sourceLabel) {
1246
1319
  const outgoingText = await this.prepareFileOutput(text, (file, mediaId) => this.sendProactiveFile(target, file, mediaId));
@@ -1362,7 +1435,7 @@ export class DingtalkChannel extends ChannelBase {
1362
1435
  if (!resp.ok) {
1363
1436
  const detail = sanitizeLogText(await resp.text().catch(() => ''), 300);
1364
1437
  process.stderr.write(`[DingTalk:${this.name}] proactive send failed (${targetKind}, ${chunkLabel}): HTTP ${resp.status} ${detail}\n`);
1365
- throw new Error(`DingTalk proactive send failed: HTTP ${resp.status}${detail ? ` ${detail}` : ''}`);
1438
+ throw new DingtalkCardRequestError(`DingTalk proactive send failed: HTTP ${resp.status}${detail ? ` ${detail}` : ''}`, isRetryableDingtalkStatus(resp.status));
1366
1439
  }
1367
1440
  if (target.isGroup === true) {
1368
1441
  if (msgKey !== PROACTIVE_FILE_MSG_KEY) {
@@ -1484,7 +1557,7 @@ export class DingtalkChannel extends ChannelBase {
1484
1557
  for (const state of reactionStates) {
1485
1558
  this.finishReaction(state.chatId, state.messageId, state.sessionId);
1486
1559
  }
1487
- this.statusCardController?.dispose();
1560
+ const backgroundDrain = this.backgroundOutputCoordinator.drain();
1488
1561
  this.activeReactionKeys.clear();
1489
1562
  this.sessionReactionKeys.clear();
1490
1563
  this.reactionStates.clear();
@@ -1495,7 +1568,12 @@ export class DingtalkChannel extends ChannelBase {
1495
1568
  this.client.disconnect();
1496
1569
  }
1497
1570
  process.stderr.write(`[DingTalk:${this.name}] Disconnected.\n`);
1498
- this.disconnectDrain = Promise.allSettled(reactionStates.map((state) => state.tail)).then(() => undefined);
1571
+ this.disconnectDrain = Promise.allSettled([
1572
+ backgroundDrain,
1573
+ ...reactionStates.map((state) => state.tail),
1574
+ ]).then(() => {
1575
+ this.statusCardController?.dispose();
1576
+ });
1499
1577
  }
1500
1578
  waitForDisconnect() {
1501
1579
  return this.disconnectDrain ?? Promise.resolve();
@@ -1764,6 +1842,8 @@ export class DingtalkChannel extends ChannelBase {
1764
1842
  }
1765
1843
  }
1766
1844
  this.sessionMentionTargets.delete(sessionId);
1845
+ // Preserve a pending background turn's latest result if its session dies.
1846
+ this.backgroundOutputCoordinator.drain(sessionId);
1767
1847
  const cardRunId = this.cardRunBySession.get(sessionId);
1768
1848
  if (cardRunId) {
1769
1849
  this.cardRunBySession.delete(sessionId);
@@ -1787,6 +1867,7 @@ export class DingtalkChannel extends ChannelBase {
1787
1867
  * the sessions on a fresh bridge.
1788
1868
  */
1789
1869
  onBridgeDisconnected() {
1870
+ void this.backgroundOutputCoordinator.drain();
1790
1871
  for (const [sessionId, keys] of this.sessionReactionKeys) {
1791
1872
  this.sessionReactionKeys.delete(sessionId);
1792
1873
  for (const { messageId, chatId } of keys.values()) {
@@ -1799,6 +1880,9 @@ export class DingtalkChannel extends ChannelBase {
1799
1880
  this.cardRuns.delete(runId);
1800
1881
  }
1801
1882
  }
1883
+ onSessionRetiring(sessionId) {
1884
+ this.backgroundOutputCoordinator.drain(sessionId);
1885
+ }
1802
1886
  onTaskLifecycle(event) {
1803
1887
  if (event.type === 'started') {
1804
1888
  this.startReaction(event.chatId, event.messageId, event.sessionId);
@@ -1813,7 +1897,10 @@ export class DingtalkChannel extends ChannelBase {
1813
1897
  this.cardRuns.set(event.runId, inboundOwner);
1814
1898
  this.cardRunBySession.set(event.sessionId, event.runId);
1815
1899
  const sourceLabel = this.getResponseSourceLabel(event.sessionId);
1816
- this.interactionPresenter?.registerRun(event.runId, event.owner.id, inboundOwner.target, event.sessionId, inboundOwner.sender, ...(sourceLabel ? [sourceLabel] : []));
1900
+ const atUserId = this.atSender && event.messageId
1901
+ ? this.mentionTargets.get(event.messageId)
1902
+ : undefined;
1903
+ this.interactionPresenter?.registerRun(event.runId, event.owner.id, inboundOwner.target, event.sessionId, inboundOwner.sender, sourceLabel, (chatId, text, _sessionId, label) => this.sendReply(chatId, text, atUserId, label));
1817
1904
  this.interactionPresenter?.startStatusCard(event.runId);
1818
1905
  }
1819
1906
  return;
@@ -1944,13 +2031,177 @@ export class DingtalkChannel extends ChannelBase {
1944
2031
  this.sessionMentionTargets.delete(sessionId);
1945
2032
  this.stopReaction(chatId, messageId, sessionId);
1946
2033
  }
1947
- async sendResponseMessage(chatId, text, sessionId, sourceLabel, prepared = false) {
2034
+ async dispatchBackgroundResponse(sessionId, text, context) {
2035
+ const target = this.router.getTarget(sessionId);
2036
+ if (!target || target.channelName !== this.name) {
2037
+ return super.dispatchBackgroundResponse(sessionId, text, context);
2038
+ }
2039
+ if (await this.backgroundOutputCoordinator.dispatch(sessionId, text, context)) {
2040
+ return;
2041
+ }
2042
+ if (!text.trim())
2043
+ return;
2044
+ const delivery = await this.resolveBackgroundResponseDelivery(sessionId);
2045
+ if (!delivery || this.router.getTarget(sessionId) !== delivery.target)
2046
+ return;
2047
+ await this.deliverBackgroundResponseToTarget(sessionId, text, delivery, false, context);
2048
+ }
2049
+ createBackgroundOutputDelivery(sessionId, target) {
2050
+ let proactivePlan;
2051
+ let replyPlan;
2052
+ // Prepared output can already have sent attachment messages.
2053
+ let preparedReplyBody;
2054
+ let composedHeader;
2055
+ let composedTurnComplete = false;
2056
+ return async (output) => {
2057
+ const header = this.formatBackgroundOutputHeader(output);
2058
+ const body = [header, output.text].filter(Boolean).join('\n\n');
2059
+ const plan = proactivePlan ?? replyPlan;
2060
+ if (!plan || plan.nextChunk === 0) {
2061
+ const replaceHeader = (text) => {
2062
+ if (composedHeader === undefined || composedHeader === header) {
2063
+ return text;
2064
+ }
2065
+ const separator = output.text ? '\n\n' : '';
2066
+ const previous = composedHeader
2067
+ ? `${composedHeader}${separator}`
2068
+ : '';
2069
+ const next = header ? `${header}${separator}` : '';
2070
+ return text.startsWith(previous)
2071
+ ? `${next}${text.slice(previous.length)}`
2072
+ : text;
2073
+ };
2074
+ if (plan) {
2075
+ plan.title = extractTitle(body);
2076
+ const prefix = (replyPlan?.atUserId ? `@${replyPlan.atUserId}\n\n` : '') +
2077
+ (target.sourceLabel
2078
+ ? `${escapeDingTalkMarkdown(target.sourceLabel)}\n\n`
2079
+ : '');
2080
+ plan.chunks[0] =
2081
+ prefix + replaceHeader(plan.chunks[0].slice(prefix.length));
2082
+ }
2083
+ if (preparedReplyBody) {
2084
+ preparedReplyBody = replaceHeader(preparedReplyBody);
2085
+ }
2086
+ composedHeader = header;
2087
+ composedTurnComplete = output.turnComplete;
2088
+ }
2089
+ try {
2090
+ if (proactivePlan) {
2091
+ await this.deliverProactiveText(target.target, proactivePlan);
2092
+ }
2093
+ else if (replyPlan) {
2094
+ await this.deliverReplyText(target.target.chatId, replyPlan, true);
2095
+ }
2096
+ else {
2097
+ if (this.statusCardController ||
2098
+ !this.supportsProactiveSend() ||
2099
+ !this.supportsProactiveTarget(target.target)) {
2100
+ preparedReplyBody ??= await this.prepareBackgroundOutput(target.target, body);
2101
+ }
2102
+ await this.deliverBackgroundResponseToTarget(sessionId, preparedReplyBody ?? body, target, preparedReplyBody !== undefined, output);
2103
+ }
2104
+ return { turnComplete: composedTurnComplete };
2105
+ }
2106
+ catch (error) {
2107
+ if (error instanceof ProactiveTextDeliveryError) {
2108
+ proactivePlan = error.plan;
2109
+ }
2110
+ else if (error instanceof ReplyTextDeliveryError) {
2111
+ replyPlan = error.plan;
2112
+ }
2113
+ throw error;
2114
+ }
2115
+ };
2116
+ }
2117
+ formatBackgroundOutputHeader(delivery) {
2118
+ const chinese = isChinesePresentationLanguage(this.displayLanguage);
2119
+ const partialLabel = partialOutputLabel(this.displayLanguage);
2120
+ const icon = delivery.status === 'completed'
2121
+ ? '✅'
2122
+ : delivery.status === 'failed'
2123
+ ? '❌'
2124
+ : '⏹️';
2125
+ if (delivery.kind === 'agent') {
2126
+ if (delivery.text)
2127
+ return delivery.partial ? partialLabel : '';
2128
+ const status = delivery.status === 'completed'
2129
+ ? chinese
2130
+ ? '已完成'
2131
+ : 'completed'
2132
+ : delivery.status === 'failed'
2133
+ ? chinese
2134
+ ? '失败'
2135
+ : 'failed'
2136
+ : chinese
2137
+ ? '已停止'
2138
+ : 'stopped';
2139
+ return chinese
2140
+ ? `${icon} 后台任务${status}`
2141
+ : `${icon} Background task ${status}`;
2142
+ }
2143
+ const label = this.formatBackgroundTaskLabel(delivery.label);
2144
+ const kind = {
2145
+ shell: 'Shell',
2146
+ monitor: 'Monitor',
2147
+ workflow: 'Workflow',
2148
+ }[delivery.kind];
2149
+ return `## ${icon} ${kind} · ${label}${delivery.partial ? `${chinese ? '' : ' '}${partialLabel}` : ''}`;
2150
+ }
2151
+ formatBackgroundTaskLabel(label) {
2152
+ const normalized = label
2153
+ ?.replace(/\p{Cc}+/gu, ' ')
2154
+ .replace(/\s+/g, ' ')
2155
+ .trim();
2156
+ return escapeDingTalkMarkdown(normalized ||
2157
+ (isChinesePresentationLanguage(this.displayLanguage)
2158
+ ? '后台任务'
2159
+ : 'Background task'));
2160
+ }
2161
+ prepareBackgroundOutput(target, text) {
2162
+ return this.supportsProactiveSend() && this.supportsProactiveTarget(target)
2163
+ ? this.prepareFileOutput(text, (file, mediaId) => this.sendProactiveFile(target, file, mediaId))
2164
+ : this.prepareReplyOutput(target.chatId, text);
2165
+ }
2166
+ async deliverBackgroundResponseToTarget(sessionId, text, delivery, prepared = false, result) {
2167
+ const { target, sourceLabel } = delivery;
2168
+ if (this.statusCardController &&
2169
+ this.supportsProactiveDeliveryTarget(target) &&
2170
+ (target.isGroup === true || this.isStableTargetId(target.senderId))) {
2171
+ if (!prepared) {
2172
+ text = await this.prepareBackgroundOutput(target, text);
2173
+ prepared = true;
2174
+ }
2175
+ if (await this.statusCardController.deliverCompletedResult({
2176
+ chatId: target.isGroup ? target.chatId : target.senderId,
2177
+ isGroup: target.isGroup === true,
2178
+ }, text, sourceLabel, result)) {
2179
+ return;
2180
+ }
2181
+ }
2182
+ if (this.supportsProactiveSend() && this.supportsProactiveTarget(target)) {
2183
+ return super.deliverBackgroundResponseToTarget(sessionId, text, delivery);
2184
+ }
2185
+ await this.deliverBackgroundReply(target.chatId, text, sessionId, sourceLabel, prepared, true);
2186
+ }
2187
+ /**
2188
+ * Body-identical to the base implementation — this override exists only to
2189
+ * widen the signature, which the base seam does not declare. The background
2190
+ * output delivery passes `prepared` so the body it already projected isn't
2191
+ * projected a second time (that would re-upload its files), and
2192
+ * `failOnHttpError` so a failed send throws instead of being swallowed,
2193
+ * letting the flush capture the delivery plan for the next retry.
2194
+ */
2195
+ async deliverBackgroundReply(chatId, text, sessionId, sourceLabel, prepared = false, failOnHttpError = false) {
2196
+ await this.sendResponseMessage(chatId, text, sessionId, sourceLabel, prepared, failOnHttpError);
2197
+ }
2198
+ async sendResponseMessage(chatId, text, sessionId, sourceLabel, prepared = false, failOnHttpError = false) {
1948
2199
  const atUserId = this.atSender
1949
2200
  ? this.sessionMentionTargets.get(sessionId)
1950
2201
  : undefined;
1951
2202
  if (atUserId)
1952
2203
  this.sessionMentionTargets.delete(sessionId);
1953
- await this.sendReply(chatId, text, atUserId, sourceLabel ?? this.getResponseSourceLabel(sessionId), prepared);
2204
+ await this.sendReply(chatId, text, atUserId, sourceLabel ?? this.getResponseSourceLabel(sessionId), prepared, failOnHttpError);
1954
2205
  }
1955
2206
  async sendFallbackReply(chatId, text, sessionId, sourceLabel) {
1956
2207
  // Mid-run fallbacks must not consume the prompt's mention target: the
@@ -1972,7 +2223,9 @@ export class DingtalkChannel extends ChannelBase {
1972
2223
  return;
1973
2224
  }
1974
2225
  }
1975
- await this.sendResponseMessage(chatId, outgoingText, sessionId, segment?.sourceLabel, true);
2226
+ await this.sendResponseMessage(chatId, segment?.partial
2227
+ ? markPartialOutput(outgoingText, this.displayLanguage)
2228
+ : outgoingText, sessionId, segment?.sourceLabel, true);
1976
2229
  }
1977
2230
  onOutputSegmentEnd(_chatId, _sessionId, segment, reason) {
1978
2231
  if (reason === 'completed' ||
@@ -2409,6 +2662,27 @@ export class DingtalkChannel extends ChannelBase {
2409
2662
  const mentionedMemberIds = isGroup ? collectNonBotMentionIds(data) : [];
2410
2663
  const senderId = senderStaffId || senderIdValue || '';
2411
2664
  const senderName = senderNick || senderId || 'Unknown';
2665
+ // A user-scope 1:1 DM gets no [sender] tag from ChannelBase — that prefix
2666
+ // needs isGroup or sessionScope 'single' — so neither the model nor the
2667
+ // transcript (which renders exactly the prompt) would record who sent
2668
+ // this. metadata is appended AFTER command parsing, so locally dispatched
2669
+ // DM commands and `!` shell never see it; an agent-exposed command does,
2670
+ // and its parser sweeps everything past the command path into `args`, so
2671
+ // a DM opening on a bare command token gets no identity line. trimStart
2672
+ // because the audio and chat-record branches hand back untrimmed text. It
2673
+ // stays one line because that append folds CR/LF to spaces. The nick is
2674
+ // attacker-controlled and the ID is platform-opaque, so both go through
2675
+ // the shared name sanitizer. The ID is spelled out only next to a nick,
2676
+ // otherwise the same value reads twice.
2677
+ const dmSenderId = senderNick && senderId
2678
+ ? ` (sender ID: ${sanitizeSenderName(senderId)})`
2679
+ : '';
2680
+ const dmCommandToken = content.text.trimStart().match(/^\/(\S+)/)?.[1] ?? '';
2681
+ const dmSenderMetadata = !isGroup &&
2682
+ this.config.sessionScope !== 'single' &&
2683
+ !SLASH_COMMAND_TOKEN_RE.test(dmCommandToken)
2684
+ ? `Direct message from ${sanitizeSenderName(senderNick || senderId)}${dmSenderId}`
2685
+ : undefined;
2412
2686
  const envelope = {
2413
2687
  channelName: this.name,
2414
2688
  senderId,
@@ -2420,6 +2694,7 @@ export class DingtalkChannel extends ChannelBase {
2420
2694
  text: content.text,
2421
2695
  ...(content.syntheticText ? { syntheticText: true } : {}),
2422
2696
  ...(mentionedMemberIds.length > 0 ? { mentionedMemberIds } : {}),
2697
+ ...(dmSenderMetadata ? { metadata: dmSenderMetadata } : {}),
2423
2698
  isGroup,
2424
2699
  isMentioned,
2425
2700
  isReplyToBot: quoted.isReplyToBot,