@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.
- package/dist/DingtalkAdapter.d.ts +27 -3
- package/dist/DingtalkAdapter.d.ts.map +1 -1
- package/dist/DingtalkAdapter.js +303 -28
- package/dist/DingtalkAdapter.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/interaction-presenter.d.ts +4 -2
- package/dist/interaction-presenter.d.ts.map +1 -1
- package/dist/interaction-presenter.js +49 -37
- package/dist/interaction-presenter.js.map +1 -1
- package/dist/interactive-card-client.d.ts +1 -0
- package/dist/interactive-card-client.d.ts.map +1 -1
- package/dist/interactive-card-client.js +2 -2
- package/dist/interactive-card-client.js.map +1 -1
- package/dist/presentation-phase.d.ts +2 -0
- package/dist/presentation-phase.d.ts.map +1 -1
- package/dist/presentation-phase.js +7 -0
- package/dist/presentation-phase.js.map +1 -1
- package/dist/status-card-controller.d.ts +9 -1
- package/dist/status-card-controller.d.ts.map +1 -1
- package/dist/status-card-controller.js +47 -16
- package/dist/status-card-controller.js.map +1 -1
- package/package.json +2 -2
|
@@ -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
|
|
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
|
-
|
|
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":"AAYA,OAAO,EACL,WAAW,
|
|
1
|
+
{"version":3,"file":"DingtalkAdapter.d.ts","sourceRoot":"","sources":["../src/DingtalkAdapter.ts"],"names":[],"mappings":"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"}
|
package/dist/DingtalkAdapter.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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(
|
|
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.
|
|
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
|
|
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,
|
|
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,
|