@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.
- package/dist/DingtalkAdapter.d.ts +13 -4
- package/dist/DingtalkAdapter.d.ts.map +1 -1
- package/dist/DingtalkAdapter.js +354 -45
- package/dist/DingtalkAdapter.js.map +1 -1
- package/dist/interaction-presenter.d.ts +5 -2
- package/dist/interaction-presenter.d.ts.map +1 -1
- package/dist/interaction-presenter.js +53 -14
- package/dist/interaction-presenter.js.map +1 -1
- package/dist/interactive-card-client.d.ts +6 -0
- package/dist/interactive-card-client.d.ts.map +1 -1
- package/dist/interactive-card-client.js +40 -16
- package/dist/interactive-card-client.js.map +1 -1
- package/dist/markdown.d.ts +4 -2
- package/dist/markdown.d.ts.map +1 -1
- package/dist/markdown.js +33 -9
- package/dist/markdown.js.map +1 -1
- package/dist/outbound-file.d.ts +12 -4
- package/dist/outbound-file.d.ts.map +1 -1
- package/dist/outbound-file.js +122 -4
- package/dist/outbound-file.js.map +1 -1
- package/dist/question-card-controller.d.ts +2 -1
- package/dist/question-card-controller.d.ts.map +1 -1
- package/dist/question-card-controller.js +16 -7
- package/dist/question-card-controller.js.map +1 -1
- package/dist/status-card-controller.d.ts +24 -1
- package/dist/status-card-controller.d.ts.map +1 -1
- package/dist/status-card-controller.js +314 -84
- package/dist/status-card-controller.js.map +1 -1
- package/package.json +2 -2
|
@@ -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
|
|
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":"AAYA,OAAO,EACL,WAAW,EAMZ,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"DingtalkAdapter.d.ts","sourceRoot":"","sources":["../src/DingtalkAdapter.ts"],"names":[],"mappings":"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"}
|
package/dist/DingtalkAdapter.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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
|
|
770
|
-
|
|
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
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
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
|
|
778
|
-
const
|
|
779
|
-
const markers = findImageMarkers(fileSafeText);
|
|
1012
|
+
async prepareOutgoingText(text) {
|
|
1013
|
+
const markers = findImageMarkers(text);
|
|
780
1014
|
if (markers.length === 0)
|
|
781
|
-
return
|
|
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(
|
|
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 =
|
|
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
|
|
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.
|
|
906
|
-
|
|
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
|
|
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
|
|
975
|
-
msgParam: JSON.stringify(
|
|
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.
|
|
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.
|
|
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
|
-
|
|
1882
|
-
|
|
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) {
|