@soimy/dingtalk 3.5.2 → 3.5.3
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/README.md +6 -23
- package/openclaw.plugin.json +695 -0
- package/package.json +5 -5
- package/src/card/card-streaming-mode.ts +30 -0
- package/src/card/reasoning-answer-split.ts +162 -0
- package/src/card-draft-controller.ts +85 -6
- package/src/card-service.ts +111 -0
- package/src/channel.ts +60 -41
- package/src/config-schema.ts +62 -38
- package/src/config.ts +25 -3
- package/src/inbound-handler.ts +355 -38
- package/src/media-utils.ts +163 -7
- package/src/message-utils.ts +33 -5
- package/src/messaging/quoted-file-service.ts +9 -4
- package/src/onboarding.ts +29 -0
- package/src/plugin-sdk-channel-actions-augment.ts +11 -0
- package/src/reply-strategy-card.ts +247 -32
- package/src/reply-strategy-markdown.ts +1 -1
- package/src/reply-strategy.ts +18 -2
- package/src/send-service.ts +110 -4
- package/src/targeting/agent-routing.ts +44 -28
- package/src/types.ts +60 -4
- package/src/utils.ts +25 -0
package/src/inbound-handler.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
2
3
|
import axios from "./http-client";
|
|
3
4
|
import { normalizeAllowFrom, isSenderAllowed, resolveGroupAccess } from "./access-control";
|
|
4
5
|
import { buildAgentSessionKey, resolveSubAgentRoute, dispatchSubAgents } from "./targeting/agent-routing";
|
|
@@ -9,7 +10,7 @@ import { extractAttachmentText } from "./messaging/attachment-text-extractor";
|
|
|
9
10
|
import { getAccessToken } from "./auth";
|
|
10
11
|
import { createAICard, finishAICard, isCardInTerminalState } from "./card-service";
|
|
11
12
|
import { handleInboundCommandDispatch } from "./command/inbound-command-dispatch-service";
|
|
12
|
-
import { resolveAckReactionSetting, resolveGroupConfig, resolveRobotCode } from "./config";
|
|
13
|
+
import { resolveAckReactionSetting, resolveGroupConfig, resolveRelativePath, resolveRobotCode } from "./config";
|
|
13
14
|
import { AICardStatus } from "./types";
|
|
14
15
|
import {
|
|
15
16
|
isCardRunStopRequested,
|
|
@@ -57,13 +58,38 @@ import {
|
|
|
57
58
|
upsertObservedUserTarget,
|
|
58
59
|
} from "./targeting/target-directory-store";
|
|
59
60
|
import type { DingTalkConfig, HandleDingTalkMessageParams, Logger, MediaFile } from "./types";
|
|
60
|
-
import { formatDingTalkErrorPayloadLog, getErrorMessage, getErrorResponseData, maskSensitiveData } from "./utils";
|
|
61
|
+
import { formatDingTalkErrorPayloadLog, getErrorMessage, getErrorResponseData, maskSensitiveData, parseBooleanLike } from "./utils";
|
|
61
62
|
import { isAbortRequestText } from "openclaw/plugin-sdk/reply-runtime";
|
|
63
|
+
import { parseInlineDirectives } from "openclaw/plugin-sdk/text-runtime";
|
|
62
64
|
|
|
63
65
|
const DEFAULT_PROACTIVE_HINT_COOLDOWN_HOURS = 24;
|
|
64
66
|
const MIN_THINKING_REACTION_VISIBLE_MS = 1200;
|
|
65
67
|
const MAX_DYNAMIC_ACK_DISPOSE_WAIT_MS = 500;
|
|
66
68
|
const ATTACHMENT_TEXT_PREFIX = "[附件内容摘录]";
|
|
69
|
+
const MEDIA_DIRECTIVE_PREFIX = "MEDIA:";
|
|
70
|
+
const STANDALONE_MEDIA_PATH_EXTENSIONS = new Set([
|
|
71
|
+
".jpg",
|
|
72
|
+
".jpeg",
|
|
73
|
+
".png",
|
|
74
|
+
".gif",
|
|
75
|
+
".bmp",
|
|
76
|
+
".ogg",
|
|
77
|
+
".amr",
|
|
78
|
+
".mp3",
|
|
79
|
+
".wav",
|
|
80
|
+
".mp4",
|
|
81
|
+
".avi",
|
|
82
|
+
".mov",
|
|
83
|
+
".doc",
|
|
84
|
+
".docx",
|
|
85
|
+
".xls",
|
|
86
|
+
".xlsx",
|
|
87
|
+
".ppt",
|
|
88
|
+
".pptx",
|
|
89
|
+
".zip",
|
|
90
|
+
".pdf",
|
|
91
|
+
".rar",
|
|
92
|
+
]);
|
|
67
93
|
const proactiveHintLastSentAt = new Map<string, number>();
|
|
68
94
|
const sessionReasoningLevelCache = new Map<string, {
|
|
69
95
|
updatedAt?: number;
|
|
@@ -71,6 +97,71 @@ const sessionReasoningLevelCache = new Map<string, {
|
|
|
71
97
|
}>();
|
|
72
98
|
type ReplyMode = "card" | "markdown";
|
|
73
99
|
|
|
100
|
+
function resolveQuotedContextAllowFrom(
|
|
101
|
+
config: DingTalkConfig,
|
|
102
|
+
groupId: string,
|
|
103
|
+
): string[] | undefined {
|
|
104
|
+
const groupConfig = resolveGroupConfig(config, groupId);
|
|
105
|
+
return groupConfig?.groupAllowFrom ?? config.groupAllowFrom;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function resolveQuotedVisibilitySenderId(params: {
|
|
109
|
+
quotedSenderId?: string;
|
|
110
|
+
currentSenderId: string;
|
|
111
|
+
currentSenderOriginalId: string;
|
|
112
|
+
}): string | undefined {
|
|
113
|
+
const quotedSenderId = (params.quotedSenderId || "").trim();
|
|
114
|
+
if (!quotedSenderId) {
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
if (quotedSenderId === params.currentSenderOriginalId) {
|
|
118
|
+
return params.currentSenderId;
|
|
119
|
+
}
|
|
120
|
+
return quotedSenderId;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function filterQuotedRuntimeContext(params: {
|
|
124
|
+
context: ReturnType<typeof resolveQuotedRuntimeContext>;
|
|
125
|
+
config: DingTalkConfig;
|
|
126
|
+
isDirect: boolean;
|
|
127
|
+
groupId: string;
|
|
128
|
+
quotedSenderId?: string;
|
|
129
|
+
currentSenderId: string;
|
|
130
|
+
currentSenderOriginalId: string;
|
|
131
|
+
}): ReturnType<typeof resolveQuotedRuntimeContext> {
|
|
132
|
+
const { context, config, isDirect, groupId, quotedSenderId, currentSenderId, currentSenderOriginalId } = params;
|
|
133
|
+
if (!context || isDirect) {
|
|
134
|
+
return context;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const mode = config.contextVisibility || "all";
|
|
138
|
+
if (mode === "all") {
|
|
139
|
+
return context;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const allow = normalizeAllowFrom(resolveQuotedContextAllowFrom(config, groupId));
|
|
143
|
+
const senderId = resolveQuotedVisibilitySenderId({
|
|
144
|
+
quotedSenderId,
|
|
145
|
+
currentSenderId,
|
|
146
|
+
currentSenderOriginalId,
|
|
147
|
+
});
|
|
148
|
+
const senderAllowed =
|
|
149
|
+
allow.hasEntries && !!senderId
|
|
150
|
+
? isSenderAllowed({ allow, senderId })
|
|
151
|
+
: false;
|
|
152
|
+
|
|
153
|
+
if (senderAllowed) {
|
|
154
|
+
return context;
|
|
155
|
+
}
|
|
156
|
+
if (mode === "allowlist_quote") {
|
|
157
|
+
return {
|
|
158
|
+
...context,
|
|
159
|
+
untrustedContext: undefined,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
|
|
74
165
|
function readSessionReasoningLevel(params: {
|
|
75
166
|
storePath?: string;
|
|
76
167
|
sessionKey: string;
|
|
@@ -132,6 +223,29 @@ function shouldDisableBlockStreamingForReplyMode(params: {
|
|
|
132
223
|
return shouldDisable;
|
|
133
224
|
}
|
|
134
225
|
|
|
226
|
+
function resolveLegacyCardStreamReasoningForInternalUse(params: {
|
|
227
|
+
cfg: HandleDingTalkMessageParams["cfg"];
|
|
228
|
+
accountId: string;
|
|
229
|
+
}): boolean | undefined {
|
|
230
|
+
const dingtalk = (params.cfg?.channels?.dingtalk ?? null) as
|
|
231
|
+
| (Record<string, unknown> & {
|
|
232
|
+
cardStreamReasoning?: unknown;
|
|
233
|
+
accounts?: Record<string, Record<string, unknown> | undefined>;
|
|
234
|
+
})
|
|
235
|
+
| null;
|
|
236
|
+
if (!dingtalk) {
|
|
237
|
+
return undefined;
|
|
238
|
+
}
|
|
239
|
+
const accountConfig = params.accountId ? dingtalk.accounts?.[params.accountId] : undefined;
|
|
240
|
+
if (typeof accountConfig?.cardStreamReasoning === "boolean") {
|
|
241
|
+
return accountConfig.cardStreamReasoning;
|
|
242
|
+
}
|
|
243
|
+
if (typeof dingtalk.cardStreamReasoning === "boolean") {
|
|
244
|
+
return dingtalk.cardStreamReasoning;
|
|
245
|
+
}
|
|
246
|
+
return undefined;
|
|
247
|
+
}
|
|
248
|
+
|
|
135
249
|
function resolvePinnedMainDmOwner(params: {
|
|
136
250
|
dmScope?: string;
|
|
137
251
|
allowFrom?: string[];
|
|
@@ -290,6 +404,7 @@ export async function downloadMedia(
|
|
|
290
404
|
config: DingTalkConfig,
|
|
291
405
|
downloadCode: string,
|
|
292
406
|
log?: any,
|
|
407
|
+
originalFilename?: string,
|
|
293
408
|
): Promise<MediaFile | null> {
|
|
294
409
|
const rt = getDingTalkRuntime();
|
|
295
410
|
let downloadUrl: string | undefined;
|
|
@@ -363,9 +478,13 @@ export async function downloadMedia(
|
|
|
363
478
|
|
|
364
479
|
const maxBytes =
|
|
365
480
|
config.mediaMaxMb && config.mediaMaxMb > 0 ? config.mediaMaxMb * 1024 * 1024 : undefined;
|
|
366
|
-
const saved =
|
|
367
|
-
|
|
368
|
-
|
|
481
|
+
const saved = await rt.channel.media.saveMediaBuffer(
|
|
482
|
+
buffer,
|
|
483
|
+
contentType,
|
|
484
|
+
"inbound",
|
|
485
|
+
maxBytes,
|
|
486
|
+
originalFilename,
|
|
487
|
+
);
|
|
369
488
|
log?.debug?.(`[DingTalk] Media saved: ${saved.path}`);
|
|
370
489
|
return { path: saved.path, mimeType: saved.contentType ?? contentType };
|
|
371
490
|
} catch (err: any) {
|
|
@@ -771,6 +890,8 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
|
|
|
771
890
|
messageType: content.messageType,
|
|
772
891
|
text: content.text,
|
|
773
892
|
quotedRef,
|
|
893
|
+
senderId,
|
|
894
|
+
senderName,
|
|
774
895
|
createdAt: data.createAt,
|
|
775
896
|
ttlMs: ttlDaysToMs(journalTTLDays),
|
|
776
897
|
ttlReferenceMs: data.createAt,
|
|
@@ -795,7 +916,12 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
|
|
|
795
916
|
mediaType = preDownloadedMedia.mediaType;
|
|
796
917
|
} else if (content.mediaPath && robotCode) {
|
|
797
918
|
// Download media only if not pre-downloaded
|
|
798
|
-
const media = await downloadMedia(
|
|
919
|
+
const media = await downloadMedia(
|
|
920
|
+
dingtalkConfig,
|
|
921
|
+
content.mediaPath,
|
|
922
|
+
log,
|
|
923
|
+
attachmentContextFileName,
|
|
924
|
+
);
|
|
799
925
|
if (media) {
|
|
800
926
|
mediaPath = media.path;
|
|
801
927
|
mediaType = media.mimeType;
|
|
@@ -816,6 +942,7 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
|
|
|
816
942
|
spaceId: data.content?.spaceId,
|
|
817
943
|
fileId: data.content?.fileId,
|
|
818
944
|
},
|
|
945
|
+
attachmentFileName: attachmentContextFileName,
|
|
819
946
|
ttlMs: DEFAULT_MEDIA_CONTEXT_TTL_MS,
|
|
820
947
|
topic: null,
|
|
821
948
|
});
|
|
@@ -840,6 +967,7 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
|
|
|
840
967
|
spaceId: content.docSpaceId,
|
|
841
968
|
fileId: content.docFileId,
|
|
842
969
|
},
|
|
970
|
+
attachmentFileName: attachmentContextFileName,
|
|
843
971
|
ttlMs: DEFAULT_MEDIA_CONTEXT_TTL_MS,
|
|
844
972
|
topic: null,
|
|
845
973
|
});
|
|
@@ -853,6 +981,7 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
|
|
|
853
981
|
content.docFileId,
|
|
854
982
|
unionId,
|
|
855
983
|
log,
|
|
984
|
+
attachmentContextFileName,
|
|
856
985
|
);
|
|
857
986
|
if (docMedia) {
|
|
858
987
|
mediaPath = docMedia.path;
|
|
@@ -871,22 +1000,30 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
|
|
|
871
1000
|
quotedRef,
|
|
872
1001
|
log,
|
|
873
1002
|
});
|
|
874
|
-
const quotedRuntimeContext =
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
1003
|
+
const quotedRuntimeContext = filterQuotedRuntimeContext({
|
|
1004
|
+
context: resolveQuotedRuntimeContext({
|
|
1005
|
+
storePath: accountStorePath,
|
|
1006
|
+
accountId,
|
|
1007
|
+
conversationId: data.conversationId,
|
|
1008
|
+
quotedRef,
|
|
1009
|
+
firstRecord: quotedRecord,
|
|
1010
|
+
firstPreview:
|
|
1011
|
+
content.quoted?.previewText ||
|
|
1012
|
+
content.quoted?.previewMessageType
|
|
1013
|
+
? {
|
|
1014
|
+
text: content.quoted.previewText,
|
|
1015
|
+
messageType: content.quoted.previewMessageType,
|
|
1016
|
+
senderId: content.quoted.previewSenderId,
|
|
1017
|
+
}
|
|
1018
|
+
: undefined,
|
|
1019
|
+
log,
|
|
1020
|
+
}),
|
|
1021
|
+
config: dingtalkConfig,
|
|
1022
|
+
isDirect,
|
|
1023
|
+
groupId,
|
|
1024
|
+
quotedSenderId: quotedRecord?.senderId || content.quoted?.previewSenderId,
|
|
1025
|
+
currentSenderId: senderId,
|
|
1026
|
+
currentSenderOriginalId: senderOriginalId,
|
|
890
1027
|
});
|
|
891
1028
|
|
|
892
1029
|
// Try downloading a quoted file from cached downloadCode/spaceId+fileId.
|
|
@@ -899,13 +1036,14 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
|
|
|
899
1036
|
fileId?: string;
|
|
900
1037
|
};
|
|
901
1038
|
} | null,
|
|
1039
|
+
originalFilename?: string,
|
|
902
1040
|
): Promise<MediaFile | null> => {
|
|
903
1041
|
if (!record?.media) {
|
|
904
1042
|
return null;
|
|
905
1043
|
}
|
|
906
1044
|
let media: MediaFile | null = null;
|
|
907
1045
|
if (record.media.downloadCode) {
|
|
908
|
-
media = await downloadMedia(dingtalkConfig, record.media.downloadCode, log);
|
|
1046
|
+
media = await downloadMedia(dingtalkConfig, record.media.downloadCode, log, originalFilename);
|
|
909
1047
|
if (media) {
|
|
910
1048
|
log?.debug?.(
|
|
911
1049
|
`[DingTalk][QuotedRef] Recovered quoted media from cached downloadCode ` +
|
|
@@ -922,6 +1060,7 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
|
|
|
922
1060
|
record.media.fileId,
|
|
923
1061
|
unionId,
|
|
924
1062
|
log,
|
|
1063
|
+
originalFilename,
|
|
925
1064
|
);
|
|
926
1065
|
if (media) {
|
|
927
1066
|
log?.debug?.(
|
|
@@ -938,9 +1077,15 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
|
|
|
938
1077
|
|
|
939
1078
|
// Quoted picture: download via existing downloadMedia.
|
|
940
1079
|
if (!mediaPath && content.quoted?.mediaDownloadCode && robotCode) {
|
|
1080
|
+
const quotedOriginalFilename = content.quoted.previewFileName;
|
|
941
1081
|
const media =
|
|
942
|
-
(await tryDownloadFromRecord(quotedRecord)) ||
|
|
943
|
-
(await downloadMedia(
|
|
1082
|
+
(await tryDownloadFromRecord(quotedRecord, quotedOriginalFilename)) ||
|
|
1083
|
+
(await downloadMedia(
|
|
1084
|
+
dingtalkConfig,
|
|
1085
|
+
content.quoted.mediaDownloadCode,
|
|
1086
|
+
log,
|
|
1087
|
+
quotedOriginalFilename,
|
|
1088
|
+
));
|
|
944
1089
|
if (media) {
|
|
945
1090
|
if (!quotedRecord) {
|
|
946
1091
|
log?.debug?.(
|
|
@@ -965,7 +1110,12 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
|
|
|
965
1110
|
|
|
966
1111
|
// Step 0: Direct download via downloadCode from quoted payload (file/audio/video msgType).
|
|
967
1112
|
if (!fileResolved && content.quoted.fileDownloadCode && robotCode) {
|
|
968
|
-
const media = await downloadMedia(
|
|
1113
|
+
const media = await downloadMedia(
|
|
1114
|
+
dingtalkConfig,
|
|
1115
|
+
content.quoted.fileDownloadCode,
|
|
1116
|
+
log,
|
|
1117
|
+
content.quoted.previewFileName,
|
|
1118
|
+
);
|
|
969
1119
|
if (media) {
|
|
970
1120
|
mediaPath = media.path;
|
|
971
1121
|
mediaType = media.mimeType;
|
|
@@ -982,7 +1132,10 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
|
|
|
982
1132
|
|
|
983
1133
|
// Step 1: Prefer quotedRef-backed record lookup, then msgId-based cache.
|
|
984
1134
|
if (!fileResolved) {
|
|
985
|
-
const cachedMedia = await tryDownloadFromRecord(
|
|
1135
|
+
const cachedMedia = await tryDownloadFromRecord(
|
|
1136
|
+
quotedRecord,
|
|
1137
|
+
quotedRecord?.attachmentFileName || content.quoted.previewFileName,
|
|
1138
|
+
);
|
|
986
1139
|
if (cachedMedia) {
|
|
987
1140
|
mediaPath = cachedMedia.path;
|
|
988
1141
|
mediaType = cachedMedia.mimeType;
|
|
@@ -1056,7 +1209,10 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
|
|
|
1056
1209
|
if (!mediaPath && content.quoted?.isQuotedDocCard) {
|
|
1057
1210
|
let docResolved = false;
|
|
1058
1211
|
|
|
1059
|
-
const cachedDocMedia = await tryDownloadFromRecord(
|
|
1212
|
+
const cachedDocMedia = await tryDownloadFromRecord(
|
|
1213
|
+
quotedRecord,
|
|
1214
|
+
quotedRecord?.attachmentFileName || content.quoted?.previewFileName,
|
|
1215
|
+
);
|
|
1060
1216
|
if (cachedDocMedia) {
|
|
1061
1217
|
mediaPath = cachedDocMedia.path;
|
|
1062
1218
|
mediaType = cachedDocMedia.mimeType;
|
|
@@ -1380,8 +1536,117 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
|
|
|
1380
1536
|
}
|
|
1381
1537
|
}
|
|
1382
1538
|
|
|
1539
|
+
function parseInlineReplyPayloadText(text: unknown): {
|
|
1540
|
+
text?: string;
|
|
1541
|
+
mediaUrls: string[];
|
|
1542
|
+
audioAsVoice: boolean;
|
|
1543
|
+
} {
|
|
1544
|
+
if (typeof text !== "string") {
|
|
1545
|
+
return { text: undefined, mediaUrls: [], audioAsVoice: false };
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
const normalizedText = text.replace(/\r\n/g, "\n");
|
|
1549
|
+
const parsedInline = normalizedText.includes("[[")
|
|
1550
|
+
? parseInlineDirectives(normalizedText, {
|
|
1551
|
+
stripAudioTag: true,
|
|
1552
|
+
stripReplyTags: false,
|
|
1553
|
+
})
|
|
1554
|
+
: {
|
|
1555
|
+
text: normalizedText,
|
|
1556
|
+
audioAsVoice: false,
|
|
1557
|
+
replyToCurrent: false,
|
|
1558
|
+
hasAudioTag: false,
|
|
1559
|
+
hasReplyTag: false,
|
|
1560
|
+
};
|
|
1561
|
+
const mediaUrls: string[] = [];
|
|
1562
|
+
const contentLines: string[] = [];
|
|
1563
|
+
|
|
1564
|
+
for (const line of parsedInline.text.split("\n")) {
|
|
1565
|
+
const trimmed = line.trim();
|
|
1566
|
+
const mediaCandidate = trimmed.replace(/^(?:\[\[[^[\]]+\]\]\s*)+/, "");
|
|
1567
|
+
if (mediaCandidate.startsWith(MEDIA_DIRECTIVE_PREFIX)) {
|
|
1568
|
+
const mediaSource = mediaCandidate.slice(MEDIA_DIRECTIVE_PREFIX.length).trim();
|
|
1569
|
+
if (mediaSource) {
|
|
1570
|
+
mediaUrls.push(mediaSource);
|
|
1571
|
+
continue;
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
contentLines.push(line);
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
const cleanedText = contentLines.join("\n").trim();
|
|
1578
|
+
const inlineTextWasTransformed = parsedInline.text !== normalizedText;
|
|
1579
|
+
if (mediaUrls.length === 0) {
|
|
1580
|
+
const standaloneMediaSource = extractStandaloneMediaSource(cleanedText);
|
|
1581
|
+
if (standaloneMediaSource) {
|
|
1582
|
+
return {
|
|
1583
|
+
text: undefined,
|
|
1584
|
+
mediaUrls: [standaloneMediaSource],
|
|
1585
|
+
audioAsVoice: parsedInline.audioAsVoice,
|
|
1586
|
+
};
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
return {
|
|
1591
|
+
// Keep ordinary text formatting stable except for newline normalization.
|
|
1592
|
+
// Once inline parsing actually strips directives/media lines, return the
|
|
1593
|
+
// cleaned body text instead of the original raw payload.
|
|
1594
|
+
text: mediaUrls.length > 0 || inlineTextWasTransformed ? cleanedText || undefined : normalizedText,
|
|
1595
|
+
mediaUrls,
|
|
1596
|
+
audioAsVoice: parsedInline.audioAsVoice,
|
|
1597
|
+
};
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
function extractStandaloneMediaSource(text: string): string | undefined {
|
|
1601
|
+
const trimmed = text.trim();
|
|
1602
|
+
if (!trimmed || trimmed.includes("\n") || /\s/.test(trimmed)) {
|
|
1603
|
+
return undefined;
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
const hasPathLikeShape =
|
|
1607
|
+
/^https?:\/\/\S+$/i.test(trimmed) ||
|
|
1608
|
+
trimmed.startsWith("~/") ||
|
|
1609
|
+
trimmed.startsWith("~\\") ||
|
|
1610
|
+
trimmed.startsWith("./") ||
|
|
1611
|
+
trimmed.startsWith(".\\") ||
|
|
1612
|
+
trimmed.startsWith("../") ||
|
|
1613
|
+
trimmed.startsWith("..\\") ||
|
|
1614
|
+
trimmed.startsWith("/") ||
|
|
1615
|
+
trimmed.startsWith("\\") ||
|
|
1616
|
+
/^[a-zA-Z]:[\\/]/.test(trimmed) ||
|
|
1617
|
+
trimmed.includes("/") ||
|
|
1618
|
+
trimmed.includes("\\");
|
|
1619
|
+
if (!hasPathLikeShape) {
|
|
1620
|
+
return undefined;
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
return STANDALONE_MEDIA_PATH_EXTENSIONS.has(path.extname(trimmed).toLowerCase())
|
|
1624
|
+
? trimmed
|
|
1625
|
+
: undefined;
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1383
1628
|
// ---- Shared media delivery helper ----
|
|
1384
|
-
|
|
1629
|
+
function extractSharedAudioAsVoice(
|
|
1630
|
+
payload: ReplyStreamPayload,
|
|
1631
|
+
inlineReplyPayload?: ReturnType<typeof parseInlineReplyPayloadText>,
|
|
1632
|
+
): boolean {
|
|
1633
|
+
// Normalize all reply-time voice hints into the shared `audioAsVoice`
|
|
1634
|
+
// semantic that strategies and media delivery use downstream.
|
|
1635
|
+
const richPayload = payload as ReplyStreamPayload & {
|
|
1636
|
+
audioAsVoice?: unknown;
|
|
1637
|
+
asVoice?: unknown;
|
|
1638
|
+
};
|
|
1639
|
+
const sharedValue = parseBooleanLike(richPayload.audioAsVoice);
|
|
1640
|
+
if (sharedValue !== undefined) {
|
|
1641
|
+
return sharedValue;
|
|
1642
|
+
}
|
|
1643
|
+
if (parseBooleanLike(richPayload.asVoice) === true) {
|
|
1644
|
+
return true;
|
|
1645
|
+
}
|
|
1646
|
+
return inlineReplyPayload?.audioAsVoice === true;
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
async function deliverMediaAttachments(urls: string[], options?: { audioAsVoice?: boolean }) {
|
|
1385
1650
|
for (const rawMediaUrl of urls) {
|
|
1386
1651
|
const preparedMedia = await prepareMediaInput(
|
|
1387
1652
|
rawMediaUrl,
|
|
@@ -1389,10 +1654,12 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
|
|
|
1389
1654
|
dingtalkConfig.mediaUrlAllowlist,
|
|
1390
1655
|
);
|
|
1391
1656
|
try {
|
|
1392
|
-
const actualMediaPath = preparedMedia.
|
|
1657
|
+
const actualMediaPath = preparedMedia.cleanup
|
|
1658
|
+
? preparedMedia.path
|
|
1659
|
+
: resolveRelativePath(preparedMedia.path);
|
|
1393
1660
|
const outMediaType = resolveOutboundMediaType({
|
|
1394
1661
|
mediaPath: actualMediaPath,
|
|
1395
|
-
asVoice:
|
|
1662
|
+
asVoice: options?.audioAsVoice === true,
|
|
1396
1663
|
});
|
|
1397
1664
|
if (sessionWebhook) {
|
|
1398
1665
|
const sendResult = await sendMessage(dingtalkConfig, to, "", {
|
|
@@ -1433,18 +1700,24 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
|
|
|
1433
1700
|
}
|
|
1434
1701
|
|
|
1435
1702
|
// ---- Extract mediaUrls from runtime payload ----
|
|
1436
|
-
function extractMediaUrls(
|
|
1703
|
+
function extractMediaUrls(
|
|
1704
|
+
payload: ReplyStreamPayload,
|
|
1705
|
+
inlineReplyPayload?: ReturnType<typeof parseInlineReplyPayloadText>,
|
|
1706
|
+
): string[] {
|
|
1437
1707
|
const richPayload = payload as typeof payload & {
|
|
1438
1708
|
mediaUrl?: string;
|
|
1439
1709
|
mediaUrls?: string[];
|
|
1440
1710
|
};
|
|
1441
|
-
|
|
1711
|
+
const explicitMediaUrls = Array.isArray(richPayload.mediaUrls)
|
|
1442
1712
|
? richPayload.mediaUrls.filter((entry: unknown) => typeof entry === "string" && entry.trim())
|
|
1443
1713
|
: richPayload.mediaUrl &&
|
|
1444
1714
|
typeof richPayload.mediaUrl === "string" &&
|
|
1445
1715
|
richPayload.mediaUrl.trim()
|
|
1446
1716
|
? [richPayload.mediaUrl]
|
|
1447
1717
|
: [];
|
|
1718
|
+
return explicitMediaUrls.length > 0
|
|
1719
|
+
? explicitMediaUrls
|
|
1720
|
+
: inlineReplyPayload?.mediaUrls ?? [];
|
|
1448
1721
|
}
|
|
1449
1722
|
|
|
1450
1723
|
// Serialize dispatchReply + card finalize per session to prevent the runtime
|
|
@@ -1503,8 +1776,16 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
|
|
|
1503
1776
|
sessionUpdatedAt: previousTimestamp,
|
|
1504
1777
|
log,
|
|
1505
1778
|
});
|
|
1779
|
+
const legacyCardStreamReasoning = resolveLegacyCardStreamReasoningForInternalUse({
|
|
1780
|
+
cfg,
|
|
1781
|
+
accountId,
|
|
1782
|
+
});
|
|
1783
|
+
const strategyConfig =
|
|
1784
|
+
legacyCardStreamReasoning === undefined
|
|
1785
|
+
? dingtalkConfig
|
|
1786
|
+
: { ...dingtalkConfig, cardStreamReasoning: legacyCardStreamReasoning };
|
|
1506
1787
|
const strategy = createReplyStrategy({
|
|
1507
|
-
config:
|
|
1788
|
+
config: strategyConfig,
|
|
1508
1789
|
card: currentAICard,
|
|
1509
1790
|
useCardMode: replyMode === "card",
|
|
1510
1791
|
to,
|
|
@@ -1529,7 +1810,8 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
|
|
|
1529
1810
|
});
|
|
1530
1811
|
|
|
1531
1812
|
try {
|
|
1532
|
-
|
|
1813
|
+
let deliveredFinalCount = 0;
|
|
1814
|
+
const dispatchResult = await rt.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
1533
1815
|
ctx,
|
|
1534
1816
|
cfg,
|
|
1535
1817
|
dispatcherOptions: {
|
|
@@ -1538,13 +1820,18 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
|
|
|
1538
1820
|
if (isCurrentCardStopRequested()) {
|
|
1539
1821
|
log?.debug?.("[DingTalk][CardStop] Ignoring reply delivery because stop was already requested");
|
|
1540
1822
|
return;
|
|
1541
|
-
|
|
1823
|
+
}
|
|
1542
1824
|
try {
|
|
1543
|
-
|
|
1825
|
+
if (info?.kind === "final") {
|
|
1826
|
+
deliveredFinalCount += 1;
|
|
1827
|
+
}
|
|
1828
|
+
const inlineReplyPayload = parseInlineReplyPayloadText(payload.text);
|
|
1829
|
+
const mediaUrls = extractMediaUrls(payload, inlineReplyPayload);
|
|
1544
1830
|
const richPayload = payload as ReplyStreamPayload & { isReasoning?: boolean };
|
|
1545
1831
|
await strategy.deliver({
|
|
1546
|
-
text:
|
|
1832
|
+
text: inlineReplyPayload.text,
|
|
1547
1833
|
mediaUrls,
|
|
1834
|
+
audioAsVoice: extractSharedAudioAsVoice(payload, inlineReplyPayload),
|
|
1548
1835
|
kind: (info?.kind as DeliverPayload["kind"]) || "block",
|
|
1549
1836
|
isReasoning: richPayload.isReasoning === true,
|
|
1550
1837
|
});
|
|
@@ -1560,6 +1847,36 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
|
|
|
1560
1847
|
},
|
|
1561
1848
|
replyOptions: strategy.getReplyOptions(),
|
|
1562
1849
|
});
|
|
1850
|
+
|
|
1851
|
+
const bufferedFinal =
|
|
1852
|
+
dispatchResult && typeof dispatchResult === "object" && "queuedFinal" in dispatchResult
|
|
1853
|
+
? (dispatchResult as { queuedFinal?: unknown }).queuedFinal
|
|
1854
|
+
: undefined;
|
|
1855
|
+
const finalCount =
|
|
1856
|
+
dispatchResult && typeof dispatchResult === "object" && "counts" in dispatchResult
|
|
1857
|
+
? (dispatchResult as { counts?: { final?: unknown } }).counts?.final
|
|
1858
|
+
: undefined;
|
|
1859
|
+
|
|
1860
|
+
log?.info?.(
|
|
1861
|
+
`[DingTalk][Dispatch] completed — deliveredFinalCount=${deliveredFinalCount} ` +
|
|
1862
|
+
`counts.final=${typeof finalCount === "number" ? finalCount : "n/a"} ` +
|
|
1863
|
+
`queuedFinalType=${typeof bufferedFinal}`,
|
|
1864
|
+
);
|
|
1865
|
+
|
|
1866
|
+
if (deliveredFinalCount === 0 && typeof bufferedFinal === "string" && bufferedFinal.trim()) {
|
|
1867
|
+
const inlineReplyPayload = parseInlineReplyPayloadText(bufferedFinal);
|
|
1868
|
+
const richBufferedPayload = {
|
|
1869
|
+
text: bufferedFinal,
|
|
1870
|
+
mediaUrls: [],
|
|
1871
|
+
} as ReplyStreamPayload;
|
|
1872
|
+
await strategy.deliver({
|
|
1873
|
+
text: inlineReplyPayload.text,
|
|
1874
|
+
mediaUrls: extractMediaUrls(richBufferedPayload, inlineReplyPayload),
|
|
1875
|
+
audioAsVoice: extractSharedAudioAsVoice(richBufferedPayload, inlineReplyPayload),
|
|
1876
|
+
kind: "final",
|
|
1877
|
+
isReasoning: false,
|
|
1878
|
+
});
|
|
1879
|
+
}
|
|
1563
1880
|
} catch (dispatchErr: unknown) {
|
|
1564
1881
|
const error = dispatchErr instanceof Error ? dispatchErr : new Error(getErrorMessage(dispatchErr));
|
|
1565
1882
|
await strategy.abort(error);
|