@qwen-code/channel-dingtalk 0.22.3 → 0.23.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.
@@ -55,10 +55,17 @@ export declare class DingtalkChannel extends ChannelBase {
55
55
  * dropped on ingestion. Exposed for testing the drop rule.
56
56
  */
57
57
  static isUnroutableGroupMessage(isGroup: boolean, conversationId: string | undefined): boolean;
58
- private projectOutgoingFileText;
58
+ private resolveSessionWebhook;
59
+ private uploadOutboundFile;
60
+ private deliverFiles;
61
+ private sendSessionFile;
62
+ private appendFileNotices;
63
+ private prepareReplyOutput;
64
+ private prepareFileOutput;
59
65
  private prepareOutgoingText;
60
66
  private sendReply;
61
67
  sendMessage(chatId: string, text: string): Promise<void>;
68
+ protected sendThreadMessage(chatId: string, _threadId: string | undefined, text: string, sourceLabel?: string): Promise<void>;
62
69
  supportsProactiveSend(): boolean;
63
70
  protected supportsProactiveTarget(target: SessionTarget): boolean;
64
71
  protected supportsProactiveDeliveryTarget(target: SessionTarget): boolean;
@@ -67,10 +74,12 @@ export declare class DingtalkChannel extends ChannelBase {
67
74
  * Single-shot cold send: a failed chunk aborts the remainder (already-sent
68
75
  * chunks are not recalled) and the error surfaces in the loop's lastError.
69
76
  */
70
- protected pushProactive(target: SessionTarget, text: string): Promise<void>;
77
+ protected pushProactive(target: SessionTarget, text: string, sourceLabel?: string): Promise<void>;
71
78
  private getProactiveToken;
72
79
  private sendCardInteractionFeedback;
73
80
  private sendProactiveChunk;
81
+ private sendProactiveFile;
82
+ private sendProactivePayload;
74
83
  private getAccessToken;
75
84
  private emotionApi;
76
85
  private attachReaction;
@@ -111,8 +120,8 @@ export declare class DingtalkChannel extends ChannelBase {
111
120
  * the session's block-streaming projector: a second sender interleaving with
112
121
  * mid-projection state can swallow the send or split a held marker.
113
122
  */
114
- protected deliverBackgroundReply(chatId: string, text: string, sessionId: string): Promise<void>;
115
- protected sendResponseMessage(chatId: string, text: string, sessionId: string): Promise<void>;
123
+ protected deliverBackgroundReply(chatId: string, text: string, sessionId: string, sourceLabel?: string): Promise<void>;
124
+ protected sendResponseMessage(chatId: string, text: string, sessionId: string, sourceLabel?: string, prepared?: boolean): Promise<void>;
116
125
  private projectBlockStreamChunk;
117
126
  private sendFallbackReply;
118
127
  protected onResponseComplete(chatId: string, text: string, sessionId: string, segment?: ChannelOutputSegmentContext): 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":"AAYA,OAAO,EACL,WAAW,EAMZ,MAAM,yBAAyB,CAAC;AA6BjC,OAAO,EAEL,6BAA6B,EAC9B,MAAM,8BAA8B,CAAC;AACtC,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;AAquBjC,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;IAIlE,OAAO,CAAC,QAAQ,CAAC,cAAc,CAG3B;IACJ,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAGhC;IAOJ,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAAqB;gBAGxD,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,aAAa,EACrB,MAAM,EAAE,kBAAkB,EAC1B,OAAO,CAAC,EAAE,kBAAkB;IA0H9B,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;IAIV,OAAO,CAAC,qBAAqB;YAef,kBAAkB;YAuBlB,YAAY;YAyBZ,eAAe;IAgD7B,OAAO,CAAC,iBAAiB;YAMX,kBAAkB;YAiBlB,iBAAiB;YAyCjB,mBAAmB;YAqDnB,SAAS;IA8FjB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;cAIrC,iBAAiB,CACxC,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,IAAI,EAAE,MAAM,EACZ,WAAW,CAAC,EAAE,MAAM,GACnB,OAAO,CAAC,IAAI,CAAC;IAIP,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,EACZ,WAAW,CAAC,EAAE,MAAM,GACnB,OAAO,CAAC,IAAI,CAAC;YA6BF,iBAAiB;IAsC/B,OAAO,CAAC,2BAA2B;YA8BrB,kBAAkB;YAclB,iBAAiB;YAajB,oBAAoB;IAkIlC,OAAO,CAAC,cAAc;YAIR,UAAU;YAuDV,cAAc;YAOd,cAAc;IAO5B,UAAU,IAAI,IAAI;IAelB,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;cAmC5B,eAAe,CAAC,KAAK,EAAE,yBAAyB,GAAG,IAAI;cAuDvD,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;IAcQ,aAAa,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;cAmCtC,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;IAMP;;;;;;;;;;OAUG;IACH,OAAO,CAAC,wBAAwB;IAuBhC;;;;OAIG;cACsB,sBAAsB,CAC7C,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,EACjB,WAAW,CAAC,EAAE,MAAM,GACnB,OAAO,CAAC,IAAI,CAAC;cAOS,mBAAmB,CAC1C,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,EACjB,WAAW,CAAC,EAAE,MAAM,EACpB,QAAQ,UAAQ,GACf,OAAO,CAAC,IAAI,CAAC;IAyBhB,OAAO,CAAC,uBAAuB;YA0BjB,iBAAiB;cAcN,kBAAkB,CACzC,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,EACjB,OAAO,CAAC,EAAE,2BAA2B,GACpC,OAAO,CAAC,IAAI,CAAC;cA2BG,kBAAkB,CACnC,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,2BAA2B,EACpC,MAAM,EAAE,6BAA6B,GACpC,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;cAcJ,eAAe,CAChC,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,EACb,UAAU,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE,2BAA2B,GACpC,IAAI;IAcP,OAAO,CAAC,0BAA0B;cAIT,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;IAsGzB,OAAO,CAAC,SAAS;CAuMlB"}
@@ -5,12 +5,12 @@ 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
+ import { FILE_UNAVAILABLE_NOTICE, OutboundFileProjector, projectFileText, readValidatedFile, safeFileName, uploadDingTalkFile, withFileUnavailableNotice, } from './outbound-file.js';
12
12
  import { DingtalkConnectionManager, } from './DingtalkConnectionManager.js';
13
- import { DingtalkInteractiveCardClient } from './interactive-card-client.js';
13
+ import { DingtalkCardRequestError, DingtalkInteractiveCardClient, } 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
16
  import { QuestionCardController } from './question-card-controller.js';
@@ -417,9 +417,110 @@ const EMOTION_RETRY_BASE_DELAY_MS = 250;
417
417
  const GROUP_MSG_API = 'https://api.dingtalk.com/v1.0/robot/groupMessages/send';
418
418
  const DIRECT_MSG_API = 'https://api.dingtalk.com/v1.0/robot/oToMessages/batchSend';
419
419
  const PROACTIVE_MSG_KEY = 'sampleMarkdown'; // DingTalk's built-in {title, text} markdown template key
420
+ const PROACTIVE_FILE_MSG_KEY = 'sampleFile';
420
421
  const TOKEN_API = 'https://oapi.dingtalk.com/gettoken';
421
422
  const PROACTIVE_FETCH_TIMEOUT_MS = 15_000;
423
+ const ROBOT_MESSAGE_HOSTS = new Set(['api.dingtalk.com', 'oapi.dingtalk.com']);
424
+ /**
425
+ * gettoken business errors a retry cannot fix: an invalid appkey/secret or a
426
+ * missing app. Any other errcode (-1 system busy, 88 throttled, ...) is
427
+ * treated as transient so card recovery keeps retrying through it.
428
+ */
429
+ const PERMANENT_TOKEN_ERROR_CODES = new Set([
430
+ 40001, 40013, 40089, 40096, 90002, 90003,
431
+ ]);
422
432
  const REPLY_FETCH_TIMEOUT_MS = 15_000;
433
+ function presentInboundError(error) {
434
+ const parts = [];
435
+ let status;
436
+ try {
437
+ if (error instanceof Error) {
438
+ if (typeof error.name === 'string')
439
+ parts.push(error.name);
440
+ if (typeof error.message === 'string')
441
+ parts.push(error.message);
442
+ }
443
+ else if (typeof error === 'string') {
444
+ parts.push(error);
445
+ }
446
+ if (typeof error === 'object' && error !== null) {
447
+ const record = error;
448
+ if (typeof record['code'] === 'string')
449
+ parts.push(record['code']);
450
+ if (typeof record['status'] === 'number')
451
+ status = record['status'];
452
+ const body = record['body'];
453
+ if (typeof body === 'string') {
454
+ parts.push(body);
455
+ }
456
+ else if (typeof body === 'object' && body !== null) {
457
+ const bodyRecord = body;
458
+ for (const key of ['code', 'errorKind', 'message']) {
459
+ if (typeof bodyRecord[key] === 'string')
460
+ parts.push(bodyRecord[key]);
461
+ }
462
+ }
463
+ }
464
+ }
465
+ catch {
466
+ return {
467
+ status: 'Processing failed',
468
+ nextStep: 'Try again. If it keeps failing, contact the bot administrator.',
469
+ };
470
+ }
471
+ const diagnostic = parts.join(' ').slice(0, 2000).toLowerCase();
472
+ if (status === 401 ||
473
+ status === 403 ||
474
+ /unauthor|forbidden|authentication|credential|invalid.?token/.test(diagnostic)) {
475
+ return {
476
+ status: 'Bot configuration error',
477
+ nextStep: 'Contact the bot administrator.',
478
+ };
479
+ }
480
+ if (status === 408 ||
481
+ status === 504 ||
482
+ /timeout|timed?\s+out|deadline/.test(diagnostic)) {
483
+ return {
484
+ status: 'Request timed out',
485
+ nextStep: 'Try again. For a large request, split it into smaller parts.',
486
+ };
487
+ }
488
+ if (/cancel|abort/.test(diagnostic)) {
489
+ return {
490
+ status: 'Request was cancelled',
491
+ nextStep: 'Send the request again if you still need it.',
492
+ };
493
+ }
494
+ if (status === 429 ||
495
+ /overload|rate.?limit|queue.?full|too many|busy|pending prompts full/.test(diagnostic)) {
496
+ return {
497
+ status: 'Service is busy',
498
+ nextStep: 'Try again in a moment.',
499
+ };
500
+ }
501
+ if (status === 502 ||
502
+ status === 503 ||
503
+ /unavailable|econn|enotfound|etimedout|network|socket|fetch failed|connection|session[_ ](?:not[ _]found|closing)|workspace[_ ]draining|transport closed/.test(diagnostic)) {
504
+ return {
505
+ status: 'Service is temporarily unavailable',
506
+ nextStep: 'Try again in a moment. If it keeps failing, contact the bot administrator.',
507
+ };
508
+ }
509
+ return {
510
+ status: 'Processing failed',
511
+ nextStep: 'Try again. If it keeps failing, contact the bot administrator.',
512
+ };
513
+ }
514
+ function formatInboundErrorMessage(error, reference) {
515
+ const presentation = presentInboundError(error);
516
+ return [
517
+ '**Unable to process this message**',
518
+ '',
519
+ `**Status:** ${presentation.status}`,
520
+ `**Next step:** ${presentation.nextStep}`,
521
+ `**Reference:** \`${reference}\``,
522
+ ].join('\n');
523
+ }
423
524
  // Extensions for generated media store names, keyed by the download's mime
424
525
  // type. The agent reads stored media via `read_file`, whose type detection is
425
526
  // extension-first: an extensionless name falls through to the binary content
@@ -443,6 +544,15 @@ const IMAGE_INSTRUCTIONS = [
443
544
  '',
444
545
  'Only use a real image file inside the workspace or system temporary directory.',
445
546
  ].join('\n');
547
+ const FILE_INSTRUCTIONS = [
548
+ '',
549
+ 'When the user explicitly asks for a completed local file, send it by writing this on its own line:',
550
+ '`[FILE: /absolute/path/to/file]` (without the backticks)',
551
+ '',
552
+ 'Use at most five non-empty files inside the workspace or system temporary directory.',
553
+ 'File paths containing ] are not supported.',
554
+ 'Do not claim delivery succeeded; DingTalk shows successful files separately and reports failures in the final text.',
555
+ ].join('\n');
446
556
  function collectNonBotMentionIds(data) {
447
557
  if (!Array.isArray(data.atUsers) || typeof data.chatbotUserId !== 'string') {
448
558
  return [];
@@ -524,6 +634,10 @@ export class DingtalkChannel extends ChannelBase {
524
634
  else if (!this.config.instructions.includes('[IMAGE:')) {
525
635
  this.config.instructions += IMAGE_INSTRUCTIONS;
526
636
  }
637
+ if (config.blockStreaming !== 'on' &&
638
+ !this.config.instructions.includes('[FILE:')) {
639
+ this.config.instructions += FILE_INSTRUCTIONS;
640
+ }
527
641
  this.interactiveCardConfig = parseDingtalkInteractiveCardConfig(config.interactiveCards);
528
642
  if (!config.clientId || !config.clientSecret) {
529
643
  throw new Error(`Channel "${name}" requires clientId and clientSecret for DingTalk.`);
@@ -540,6 +654,11 @@ export class DingtalkChannel extends ChannelBase {
540
654
  this.interactiveCardClient = new DingtalkInteractiveCardClient({
541
655
  robotCode: config.clientId,
542
656
  getAccessToken: () => this.getProactiveToken(),
657
+ invalidateAccessToken: (token) => {
658
+ if (this.proactiveToken?.token === token) {
659
+ this.proactiveToken = undefined;
660
+ }
661
+ },
543
662
  });
544
663
  if (this.interactiveCardConfig.statusCard.enabled &&
545
664
  config.blockStreaming !== 'on') {
@@ -556,7 +675,7 @@ export class DingtalkChannel extends ChannelBase {
556
675
  this.questionCardController = new QuestionCardController({
557
676
  client: this.interactiveCardClient,
558
677
  timeoutMs: this.interactiveCardConfig.questionCard.timeoutMs,
559
- sendFallback: (chatId, text) => this.sendMessage(chatId, text),
678
+ sendFallback: (chatId, text, sourceLabel) => this.sendReply(chatId, text, undefined, sourceLabel),
560
679
  reserveRunProjection: (runId) => this.interactionPresenter?.reserveProjection(runId),
561
680
  onError: (operation, error) => {
562
681
  process.stderr.write(`[DingTalk:${this.name}] ${operation} failed: ${sanitizeLogText(String(error), 300)}\n`);
@@ -569,7 +688,7 @@ export class DingtalkChannel extends ChannelBase {
569
688
  questionCards: this.questionCardController,
570
689
  ...(config.blockStreaming !== 'on'
571
690
  ? {
572
- sendFallback: (chatId, text, sessionId) => this.sendFallbackReply(chatId, text, sessionId),
691
+ sendFallback: (chatId, text, sessionId, sourceLabel) => this.sendFallbackReply(chatId, text, sessionId, sourceLabel),
573
692
  }
574
693
  : {}),
575
694
  });
@@ -758,27 +877,142 @@ export class DingtalkChannel extends ChannelBase {
758
877
  static isUnroutableGroupMessage(isGroup, conversationId) {
759
878
  return isGroup && !conversationId;
760
879
  }
761
- projectOutgoingFileText(text, streamed) {
880
+ resolveSessionWebhook(chatId) {
881
+ const value = this.webhooks.get(chatId);
882
+ if (!value)
883
+ return undefined;
884
+ try {
885
+ const url = new URL(value);
886
+ return url.protocol === 'https:' &&
887
+ url.port === '' &&
888
+ ROBOT_MESSAGE_HOSTS.has(url.hostname)
889
+ ? url.toString()
890
+ : undefined;
891
+ }
892
+ catch {
893
+ return undefined;
894
+ }
895
+ }
896
+ async uploadOutboundFile(filePath) {
897
+ const file = readValidatedFile(filePath, this.config.cwd);
898
+ for (let attempt = 0; attempt < 2; attempt++) {
899
+ const token = await this.getProactiveToken();
900
+ try {
901
+ return { file, mediaId: await uploadDingTalkFile(file, token) };
902
+ }
903
+ catch (error) {
904
+ if (error instanceof DingTalkMediaUploadError &&
905
+ error.authFailure &&
906
+ attempt === 0) {
907
+ this.proactiveToken = undefined;
908
+ continue;
909
+ }
910
+ throw error;
911
+ }
912
+ }
913
+ throw new Error('DingTalk file upload returned no MediaID');
914
+ }
915
+ async deliverFiles(paths, send, preflight) {
916
+ const notices = [];
917
+ for (const filePath of paths) {
918
+ const displayName = safeFileName(filePath);
919
+ try {
920
+ preflight?.();
921
+ const { file, mediaId } = await this.uploadOutboundFile(filePath);
922
+ await send(file, mediaId);
923
+ }
924
+ catch (error) {
925
+ process.stderr.write(`[DingTalk:${this.name}] outbound file delivery failed (${sanitizeLogText(displayName, 200)}): ${sanitizeLogText(error instanceof Error ? error.message : String(error), 300)}\n`);
926
+ notices.push(`[File delivery failed: ${displayName}]`);
927
+ }
928
+ }
929
+ return notices;
930
+ }
931
+ async sendSessionFile(chatId, file, mediaId) {
932
+ const webhook = this.resolveSessionWebhook(chatId);
933
+ if (!webhook)
934
+ throw new Error('DingTalk session webhook unavailable');
935
+ let response;
936
+ try {
937
+ response = await fetch(webhook, {
938
+ method: 'POST',
939
+ redirect: 'error',
940
+ headers: { 'Content-Type': 'application/json' },
941
+ body: JSON.stringify({
942
+ msgtype: 'file',
943
+ file: {
944
+ mediaId,
945
+ fileName: file.fileName,
946
+ fileType: file.fileType,
947
+ },
948
+ }),
949
+ signal: AbortSignal.timeout(REPLY_FETCH_TIMEOUT_MS),
950
+ });
951
+ }
952
+ catch {
953
+ throw new Error('DingTalk file delivery failed: network request failed');
954
+ }
955
+ const body = await response.text().catch(() => '');
956
+ if (!response.ok) {
957
+ throw new Error(`DingTalk file delivery failed: HTTP ${response.status}`);
958
+ }
959
+ if (!body.trim())
960
+ return;
961
+ let data;
962
+ try {
963
+ const parsed = JSON.parse(body);
964
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
965
+ return;
966
+ data = parsed;
967
+ }
968
+ catch {
969
+ return;
970
+ }
971
+ const code = data['errcode'] ?? data['code'];
972
+ if (code !== undefined && String(code) !== '0') {
973
+ throw new Error(`DingTalk file delivery failed: API code ${code}`);
974
+ }
975
+ }
976
+ appendFileNotices(text, notices) {
977
+ if (notices.length === 0)
978
+ return text;
979
+ const prefix = text.trimEnd();
980
+ return `${prefix}${prefix ? '\n' : ''}${notices.join('\n')}`;
981
+ }
982
+ async prepareReplyOutput(chatId, text, streamed) {
983
+ return this.prepareFileOutput(text, (file, mediaId) => this.sendSessionFile(chatId, file, mediaId), streamed, () => {
984
+ if (!this.resolveSessionWebhook(chatId)) {
985
+ throw new Error('DingTalk session webhook unavailable');
986
+ }
987
+ });
988
+ }
989
+ async prepareFileOutput(text, send, streamed, preflight) {
762
990
  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
991
  const streamedMarkers = streamed ? streamed.result('').markerCount : 0;
769
- if (projection.markerCount === 0 && streamedMarkers === 0) {
770
- return projection.text;
992
+ if (projection.markerCount > 0 || streamedMarkers > 0) {
993
+ process.stderr.write(`[DingTalk:${this.name}] file markers projected (final=${projection.markerCount}, streamed=${streamedMarkers})\n`);
994
+ }
995
+ if (this.config.blockStreaming === 'on' &&
996
+ (projection.markerCount > 0 || streamedMarkers > 0)) {
997
+ return this.prepareOutgoingText(withFileUnavailableNotice(projection.text));
998
+ }
999
+ const notices = [];
1000
+ if (projection.invalidMarkers > 0) {
1001
+ notices.push('[File delivery failed: invalid marker]');
771
1002
  }
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);
1003
+ if (projection.excessMarkers > 0) {
1004
+ notices.push('[File delivery failed: response file limit exceeded]');
1005
+ }
1006
+ if (streamedMarkers > projection.markerCount) {
1007
+ notices.push(FILE_UNAVAILABLE_NOTICE);
1008
+ }
1009
+ notices.push(...(await this.deliverFiles(projection.paths, send, preflight)));
1010
+ return this.prepareOutgoingText(this.appendFileNotices(projection.text, notices));
776
1011
  }
777
- async prepareOutgoingText(text, streamed) {
778
- const fileSafeText = this.projectOutgoingFileText(text, streamed);
779
- const markers = findImageMarkers(fileSafeText);
1012
+ async prepareOutgoingText(text) {
1013
+ const markers = findImageMarkers(text);
780
1014
  if (markers.length === 0)
781
- return fileSafeText;
1015
+ return text;
782
1016
  const replacements = [];
783
1017
  for (const marker of markers) {
784
1018
  const fileName = basename(marker.path)
@@ -815,18 +1049,29 @@ export class DingtalkChannel extends ChannelBase {
815
1049
  replacements.push(`[Image delivery failed: ${fileName}]`);
816
1050
  }
817
1051
  }
818
- return replaceImageMarkers(fileSafeText, markers, replacements);
1052
+ return replaceImageMarkers(text, markers, replacements);
819
1053
  }
820
- async sendReply(chatId, text, atUserId) {
821
- // chatId is a conversationId — resolve to the latest sessionWebhook
1054
+ async sendReply(chatId, text, atUserId, sourceLabel, prepared = false) {
1055
+ // chatId is a conversationId — resolve to the latest sessionWebhook.
822
1056
  const webhook = this.webhooks.get(chatId);
823
1057
  if (!webhook) {
824
1058
  process.stderr.write(`[DingTalk:${this.name}] No webhook for chatId ${chatId}, cannot send.\n`);
825
1059
  return;
826
1060
  }
827
- const outgoingText = await this.prepareOutgoingText(text);
1061
+ const outgoingText = prepared
1062
+ ? text
1063
+ : await this.prepareReplyOutput(chatId, text);
1064
+ if (!outgoingText.trim())
1065
+ return;
828
1066
  const mentionPrefix = atUserId ? `@${atUserId}\n\n` : '';
829
- const chunks = normalizeDingTalkMarkdown(mentionPrefix + outgoingText);
1067
+ const sourcePrefix = sourceLabel && outgoingText.trim().length > 0
1068
+ ? `${escapeDingTalkMarkdown(sourceLabel)}\n\n`
1069
+ : '';
1070
+ const contentLimit = DINGTALK_CHUNK_LIMIT - mentionPrefix.length - sourcePrefix.length;
1071
+ if (contentLimit <= 0) {
1072
+ throw new Error('DingTalk source label exceeds the message limit.');
1073
+ }
1074
+ const chunks = normalizeDingTalkMarkdown(outgoingText, contentLimit).map((chunk, index) => `${index === 0 ? mentionPrefix : ''}${sourcePrefix}${chunk}`);
830
1075
  const title = extractTitle(outgoingText);
831
1076
  for (let i = 0; i < chunks.length; i++) {
832
1077
  const chunk = chunks[i];
@@ -875,6 +1120,9 @@ export class DingtalkChannel extends ChannelBase {
875
1120
  async sendMessage(chatId, text) {
876
1121
  await this.sendReply(chatId, text);
877
1122
  }
1123
+ async sendThreadMessage(chatId, _threadId, text, sourceLabel) {
1124
+ await this.sendReply(chatId, text, undefined, sourceLabel);
1125
+ }
878
1126
  supportsProactiveSend() {
879
1127
  return true;
880
1128
  }
@@ -899,11 +1147,20 @@ export class DingtalkChannel extends ChannelBase {
899
1147
  * Single-shot cold send: a failed chunk aborts the remainder (already-sent
900
1148
  * chunks are not recalled) and the error surfaces in the loop's lastError.
901
1149
  */
902
- async pushProactive(target, text) {
1150
+ async pushProactive(target, text, sourceLabel) {
903
1151
  if (!text.trim())
904
1152
  return;
905
- const outgoingText = await this.prepareOutgoingText(text);
906
- const chunks = normalizeDingTalkMarkdown(outgoingText);
1153
+ const outgoingText = await this.prepareFileOutput(text, (file, mediaId) => this.sendProactiveFile(target, file, mediaId));
1154
+ if (!outgoingText.trim())
1155
+ return;
1156
+ const sourcePrefix = sourceLabel
1157
+ ? `${escapeDingTalkMarkdown(sourceLabel)}\n\n`
1158
+ : '';
1159
+ const contentLimit = DINGTALK_CHUNK_LIMIT - sourcePrefix.length;
1160
+ if (contentLimit <= 0) {
1161
+ throw new Error('DingTalk source label exceeds the message limit.');
1162
+ }
1163
+ const chunks = normalizeDingTalkMarkdown(outgoingText, contentLimit).map((chunk) => `${sourcePrefix}${chunk}`);
907
1164
  const title = extractTitle(outgoingText);
908
1165
  for (let i = 0; i < chunks.length; i++) {
909
1166
  await this.sendProactiveChunk(target, i === 0 ? title : `${title} (cont.)`, chunks[i], `chunk ${i + 1}/${chunks.length}`);
@@ -928,7 +1185,7 @@ export class DingtalkChannel extends ChannelBase {
928
1185
  if (!data.access_token) {
929
1186
  const errmsg = sanitizeLogText(String(data.errmsg ?? ''), 200);
930
1187
  process.stderr.write(`[DingTalk:${this.name}] access token request failed: gettoken errcode=${data.errcode} ${errmsg}\n`);
931
- throw new Error(`DingTalk access token request failed: gettoken errcode=${data.errcode}${errmsg ? ` ${errmsg}` : ''}`);
1188
+ throw new DingtalkCardRequestError(`DingTalk access token request failed: gettoken errcode=${data.errcode}${errmsg ? ` ${errmsg}` : ''}`, !PERMANENT_TOKEN_ERROR_CODES.has(Number(data.errcode)));
932
1189
  }
933
1190
  this.proactiveToken = {
934
1191
  token: data.access_token,
@@ -954,6 +1211,12 @@ export class DingtalkChannel extends ChannelBase {
954
1211
  }, '卡片操作', '你无权操作这张卡片,仅任务发起人可以提交或停止。', 'card interaction feedback');
955
1212
  }
956
1213
  async sendProactiveChunk(target, title, text, chunkLabel) {
1214
+ return this.sendProactivePayload(target, PROACTIVE_MSG_KEY, { title, text }, chunkLabel);
1215
+ }
1216
+ async sendProactiveFile(target, file, mediaId) {
1217
+ return this.sendProactivePayload(target, PROACTIVE_FILE_MSG_KEY, { mediaId, fileName: file.fileName, fileType: file.fileType }, `file ${file.fileName}`);
1218
+ }
1219
+ async sendProactivePayload(target, msgKey, msgParam, chunkLabel) {
957
1220
  const targetKind = target.isGroup === true ? 'group' : 'dm';
958
1221
  for (let attempt = 0;; attempt++) {
959
1222
  const token = await this.getProactiveToken();
@@ -971,8 +1234,8 @@ export class DingtalkChannel extends ChannelBase {
971
1234
  body: JSON.stringify({
972
1235
  robotCode: this.config.clientId,
973
1236
  ...targetBody,
974
- msgKey: PROACTIVE_MSG_KEY,
975
- msgParam: JSON.stringify({ title, text }),
1237
+ msgKey,
1238
+ msgParam: JSON.stringify(msgParam),
976
1239
  }),
977
1240
  signal: AbortSignal.timeout(PROACTIVE_FETCH_TIMEOUT_MS),
978
1241
  });
@@ -993,6 +1256,32 @@ export class DingtalkChannel extends ChannelBase {
993
1256
  process.stderr.write(`[DingTalk:${this.name}] proactive send failed (${targetKind}, ${chunkLabel}): HTTP ${resp.status} ${detail}\n`);
994
1257
  throw new Error(`DingTalk proactive send failed: HTTP ${resp.status}${detail ? ` ${detail}` : ''}`);
995
1258
  }
1259
+ if (target.isGroup === true) {
1260
+ if (msgKey !== PROACTIVE_FILE_MSG_KEY) {
1261
+ await resp.body?.cancel();
1262
+ return;
1263
+ }
1264
+ let data;
1265
+ try {
1266
+ const parsed = (await resp.json());
1267
+ data =
1268
+ parsed && typeof parsed === 'object' && !Array.isArray(parsed)
1269
+ ? parsed
1270
+ : {};
1271
+ }
1272
+ catch {
1273
+ throw new Error('DingTalk file delivery failed: invalid JSON response');
1274
+ }
1275
+ const code = data['errcode'] ?? data['code'];
1276
+ if (code !== undefined && String(code) !== '0') {
1277
+ throw new Error(`DingTalk file delivery failed: API code ${code}`);
1278
+ }
1279
+ if (typeof data['processQueryKey'] !== 'string' ||
1280
+ !data['processQueryKey'].trim()) {
1281
+ throw new Error('DingTalk file delivery failed: missing processQueryKey');
1282
+ }
1283
+ return;
1284
+ }
996
1285
  if (target.isGroup === false) {
997
1286
  let data;
998
1287
  try {
@@ -1010,6 +1299,10 @@ export class DingtalkChannel extends ChannelBase {
1010
1299
  process.stderr.write(`[DingTalk:${this.name}] proactive send failed (${targetKind}, ${chunkLabel}): direct recipient rate limited\n`);
1011
1300
  throw new Error('DingTalk proactive send failed: direct recipient rate limited');
1012
1301
  }
1302
+ if (msgKey === PROACTIVE_FILE_MSG_KEY &&
1303
+ !data.processQueryKey?.trim()) {
1304
+ throw new Error('DingTalk file delivery failed: missing processQueryKey');
1305
+ }
1013
1306
  return;
1014
1307
  }
1015
1308
  await resp.body?.cancel();
@@ -1077,6 +1370,7 @@ export class DingtalkChannel extends ChannelBase {
1077
1370
  if (this.dedupTimer) {
1078
1371
  clearInterval(this.dedupTimer);
1079
1372
  }
1373
+ this.statusCardController?.dispose();
1080
1374
  this.activeReactionKeys.clear();
1081
1375
  this.sessionReactionKeys.clear();
1082
1376
  if (this.connectionManager) {
@@ -1206,7 +1500,8 @@ export class DingtalkChannel extends ChannelBase {
1206
1500
  inboundOwner?.ownerId === event.owner.id) {
1207
1501
  this.cardRuns.set(event.runId, inboundOwner);
1208
1502
  this.cardRunBySession.set(event.sessionId, event.runId);
1209
- this.interactionPresenter?.registerRun(event.runId, event.owner.id, inboundOwner.target, event.sessionId, inboundOwner.sender);
1503
+ const sourceLabel = this.getResponseSourceLabel(event.sessionId);
1504
+ this.interactionPresenter?.registerRun(event.runId, event.owner.id, inboundOwner.target, event.sessionId, inboundOwner.sender, ...(sourceLabel ? [sourceLabel] : []));
1210
1505
  this.interactionPresenter?.startStatusCard(event.runId);
1211
1506
  }
1212
1507
  return;
@@ -1355,7 +1650,7 @@ export class DingtalkChannel extends ChannelBase {
1355
1650
  // prefix of '[FILE:' (at most 5 chars, no path bytes), so the worst case
1356
1651
  // is a stray fragment landing out of order with the next turn — not a
1357
1652
  // leak — and blocking settle on a delivery that may hang is worse.
1358
- void this.sendReply(chatId, tail).catch((err) => {
1653
+ void this.sendReply(chatId, tail, undefined, this.getResponseSourceLabel(sessionId)).catch((err) => {
1359
1654
  process.stderr.write(`[DingTalk:${this.name}] projector tail delivery failed for session ${sessionId}: ${err instanceof Error ? err.message : String(err)}\n`);
1360
1655
  });
1361
1656
  }
@@ -1364,13 +1659,13 @@ export class DingtalkChannel extends ChannelBase {
1364
1659
  * the session's block-streaming projector: a second sender interleaving with
1365
1660
  * mid-projection state can swallow the send or split a held marker.
1366
1661
  */
1367
- async deliverBackgroundReply(chatId, text, sessionId) {
1662
+ async deliverBackgroundReply(chatId, text, sessionId, sourceLabel) {
1368
1663
  if (this.config.blockStreaming !== 'on') {
1369
- return super.deliverBackgroundReply(chatId, text, sessionId);
1664
+ return super.deliverBackgroundReply(chatId, text, sessionId, sourceLabel);
1370
1665
  }
1371
- await this.sendReply(chatId, text);
1666
+ await this.sendReply(chatId, text, undefined, sourceLabel);
1372
1667
  }
1373
- async sendResponseMessage(chatId, text, sessionId) {
1668
+ async sendResponseMessage(chatId, text, sessionId, sourceLabel, prepared = false) {
1374
1669
  let outgoingText = text;
1375
1670
  let consumesMention = true;
1376
1671
  if (this.config.blockStreaming === 'on') {
@@ -1387,7 +1682,7 @@ export class DingtalkChannel extends ChannelBase {
1387
1682
  : undefined;
1388
1683
  if (atUserId)
1389
1684
  this.sessionMentionTargets.delete(sessionId);
1390
- await this.sendReply(chatId, outgoingText, atUserId);
1685
+ await this.sendReply(chatId, outgoingText, atUserId, sourceLabel ?? this.getResponseSourceLabel(sessionId), prepared);
1391
1686
  }
1392
1687
  projectBlockStreamChunk(text, sessionId) {
1393
1688
  let state = this.blockFileProjectors.get(sessionId);
@@ -1409,13 +1704,13 @@ export class DingtalkChannel extends ChannelBase {
1409
1704
  }
1410
1705
  return { text: outgoingText, hasContent };
1411
1706
  }
1412
- async sendFallbackReply(chatId, text, sessionId) {
1707
+ async sendFallbackReply(chatId, text, sessionId, sourceLabel) {
1413
1708
  // Mid-run fallbacks must not consume the prompt's mention target: the
1414
1709
  // final answer of the same run still needs it.
1415
1710
  const atUserId = this.atSender
1416
1711
  ? this.sessionMentionTargets.get(sessionId)
1417
1712
  : undefined;
1418
- await this.sendReply(chatId, text, atUserId);
1713
+ await this.sendReply(chatId, text, atUserId, sourceLabel);
1419
1714
  }
1420
1715
  async onResponseComplete(chatId, text, sessionId, segment) {
1421
1716
  const streamed = segment
@@ -1423,13 +1718,13 @@ export class DingtalkChannel extends ChannelBase {
1423
1718
  : undefined;
1424
1719
  if (segment)
1425
1720
  this.fileProjectors.delete(segment.runId);
1426
- const outgoingText = await this.prepareOutgoingText(text, streamed);
1721
+ const outgoingText = await this.prepareReplyOutput(chatId, text, streamed);
1427
1722
  if (segment && this.interactionPresenter) {
1428
1723
  if (await this.interactionPresenter.closeOutput(segment.segmentId, outgoingText, 'completed', segment)) {
1429
1724
  return;
1430
1725
  }
1431
1726
  }
1432
- await this.sendResponseMessage(chatId, outgoingText, sessionId);
1727
+ await this.sendResponseMessage(chatId, outgoingText, sessionId, segment?.sourceLabel, true);
1433
1728
  }
1434
1729
  onOutputSegmentEnd(_chatId, _sessionId, segment, reason) {
1435
1730
  if (reason === 'completed' ||
@@ -1878,8 +2173,22 @@ export class DingtalkChannel extends ChannelBase {
1878
2173
  : this.handleInbound(envelope);
1879
2174
  processMessage.catch((err) => {
1880
2175
  // Don't await — stream callback should return quickly
1881
- process.stderr.write(`[DingTalk:${this.name}] Error handling message: ${err}\n`);
1882
- this.sendMessage(chatId, 'Sorry, something went wrong processing your message.').catch(() => { });
2176
+ const reference = randomUUID().slice(0, 8);
2177
+ let errorSummary = 'Unknown error';
2178
+ try {
2179
+ errorSummary =
2180
+ err instanceof Error ? `${err.name}: ${err.message}` : String(err);
2181
+ }
2182
+ catch {
2183
+ // The user-facing fallback below must survive arbitrary rejections.
2184
+ }
2185
+ process.stderr.write(`[DingTalk:${this.name}] Error handling message ref=${reference}: ${sanitizeLogText(errorSummary, 300)}\n`);
2186
+ const fallbackMessage = formatInboundErrorMessage(err, reference);
2187
+ const sourceLabel = this.getInboundErrorSourceLabel(envelope);
2188
+ const delivery = sourceLabel
2189
+ ? this.sendThreadMessage(chatId, envelope.threadId, fallbackMessage, sourceLabel)
2190
+ : this.sendMessage(chatId, fallbackMessage);
2191
+ delivery.catch(() => { });
1883
2192
  });
1884
2193
  }
1885
2194
  catch (err) {