@soimy/dingtalk 3.6.2-beta.1 → 3.6.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/dist/index.d.ts.map +1 -1
- package/dist/index.js +1550 -1220
- package/dist/index.js.map +4 -4
- package/dist/src/card-draft-controller.d.ts.map +1 -1
- package/dist/src/card-service.d.ts.map +1 -1
- package/dist/src/message-utils.d.ts +10 -0
- package/dist/src/message-utils.d.ts.map +1 -1
- package/dist/src/reply-strategy-card.d.ts.map +1 -1
- package/dist/src/reply-strategy-markdown.d.ts.map +1 -1
- package/dist/src/reply-strategy-types.d.ts +2 -0
- package/dist/src/reply-strategy-types.d.ts.map +1 -1
- package/dist/src/send-service.d.ts.map +1 -1
- package/index.ts +234 -16
- package/package.json +1 -1
- package/src/card-callback-service.ts +1 -1
- package/src/card-draft-controller.ts +4 -3
- package/src/card-service.ts +28 -4
- package/src/message-utils.ts +103 -7
- package/src/reply-strategy-card.ts +12 -3
- package/src/reply-strategy-markdown.ts +139 -20
- package/src/reply-strategy-types.ts +3 -0
- package/src/send-service.ts +67 -65
|
@@ -102,7 +102,6 @@ export function createCardReplyStrategy(
|
|
|
102
102
|
};
|
|
103
103
|
const { mode, usedDeprecatedCardRealTimeStream } = resolveCardStreamingMode(config);
|
|
104
104
|
const streamAnswerLive = mode === "answer" || mode === "all";
|
|
105
|
-
const renderAnswerBlocksLive = mode === "all";
|
|
106
105
|
const streamThinkingLive = mode === "all";
|
|
107
106
|
let lifecycleState: CardReplyLifecycleState = "open";
|
|
108
107
|
const shouldAcceptAnswerSnapshot = () => lifecycleState === "open";
|
|
@@ -261,7 +260,10 @@ export function createCardReplyStrategy(
|
|
|
261
260
|
finalTextForFallback = normalized.answerText;
|
|
262
261
|
return;
|
|
263
262
|
}
|
|
264
|
-
await controller.updateAnswer(normalized.answerText
|
|
263
|
+
await controller.updateAnswer(normalized.answerText, {
|
|
264
|
+
stream: streamAnswerLive,
|
|
265
|
+
renderBlocks: !streamAnswerLive,
|
|
266
|
+
});
|
|
265
267
|
}
|
|
266
268
|
};
|
|
267
269
|
|
|
@@ -304,7 +306,8 @@ export function createCardReplyStrategy(
|
|
|
304
306
|
|
|
305
307
|
await controller.updateAnswer(answerSnapshot, {
|
|
306
308
|
stream: streamAnswerLive,
|
|
307
|
-
|
|
309
|
+
// Active answer previews live in the content field; blockList is committed at boundaries/finalize.
|
|
310
|
+
renderBlocks: false,
|
|
308
311
|
});
|
|
309
312
|
};
|
|
310
313
|
|
|
@@ -388,6 +391,11 @@ export function createCardReplyStrategy(
|
|
|
388
391
|
// Card mode keeps runtime block streaming disabled, but still consumes
|
|
389
392
|
// reasoning blocks through explicit callbacks and delivery metadata.
|
|
390
393
|
disableBlockStreaming: ctx.disableBlockStreaming ?? true,
|
|
394
|
+
// DingTalk card mode owns the visible reply surface. In group chats,
|
|
395
|
+
// OpenClaw defaults source replies to message-tool-only; override that
|
|
396
|
+
// so final replies are delivered into this card instead of spawning a
|
|
397
|
+
// separate visible message/card via the message tool.
|
|
398
|
+
sourceReplyDeliveryMode: "automatic",
|
|
391
399
|
|
|
392
400
|
onAssistantMessageStart: async () => {
|
|
393
401
|
if (isLifecycleSealed() || isStopRequested?.()) {
|
|
@@ -671,6 +679,7 @@ export function createCardReplyStrategy(
|
|
|
671
679
|
try {
|
|
672
680
|
await flushPendingReasoning();
|
|
673
681
|
|
|
682
|
+
await controller.clearStreamingContent?.();
|
|
674
683
|
await controller.flush();
|
|
675
684
|
await controller.waitForInFlight();
|
|
676
685
|
|
|
@@ -6,7 +6,15 @@
|
|
|
6
6
|
* Reasoning display is intentionally unsupported on DingTalk markdown.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { resolveRelativePath } from "./config";
|
|
11
|
+
import { prepareMediaInput, resolveOutboundMediaType } from "./media-utils";
|
|
12
|
+
import type {
|
|
13
|
+
DeliverPayload,
|
|
14
|
+
ReplyOptions,
|
|
15
|
+
ReplyStrategy,
|
|
16
|
+
ReplyStrategyContext,
|
|
17
|
+
} from "./reply-strategy-types";
|
|
10
18
|
import { sendMessage } from "./send-service";
|
|
11
19
|
|
|
12
20
|
const EMPTY_FINAL_FALLBACK_TEXT = "✅ Done";
|
|
@@ -14,7 +22,7 @@ const EMPTY_FINAL_FALLBACK_TEXT = "✅ Done";
|
|
|
14
22
|
function renderQuotedSegment(text: string): string {
|
|
15
23
|
return text
|
|
16
24
|
.split("\n")
|
|
17
|
-
.map((line) => line.length > 0 ? `> ${line}` : ">")
|
|
25
|
+
.map((line) => (line.length > 0 ? `> ${line}` : ">"))
|
|
18
26
|
.join("\n");
|
|
19
27
|
}
|
|
20
28
|
|
|
@@ -52,9 +60,12 @@ function computeSharedPrefixTail(previous: string, next: string): string {
|
|
|
52
60
|
return suffix.trim() ? suffix : "";
|
|
53
61
|
}
|
|
54
62
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
63
|
+
function renderMarkdownImage(mediaPath: string): string {
|
|
64
|
+
const filename = path.basename(mediaPath) || "image";
|
|
65
|
+
return ``;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function createMarkdownReplyStrategy(ctx: ReplyStrategyContext): ReplyStrategy {
|
|
58
69
|
let finalText: string | undefined;
|
|
59
70
|
let activeAnswerText = "";
|
|
60
71
|
let lastSentAnswerText = "";
|
|
@@ -79,7 +90,10 @@ export function createMarkdownReplyStrategy(
|
|
|
79
90
|
sentVisibleContent = true;
|
|
80
91
|
};
|
|
81
92
|
|
|
82
|
-
const
|
|
93
|
+
const prepareAnswerSuffix = (text: string | undefined): {
|
|
94
|
+
text: string;
|
|
95
|
+
markSent: () => void;
|
|
96
|
+
} | null => {
|
|
83
97
|
const current = typeof text === "string" ? text : "";
|
|
84
98
|
if (current.length > 0) {
|
|
85
99
|
activeAnswerText = current;
|
|
@@ -88,42 +102,146 @@ export function createMarkdownReplyStrategy(
|
|
|
88
102
|
|
|
89
103
|
const suffix = computeIncrementalSuffix(lastSentAnswerText, current);
|
|
90
104
|
if (suffix) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
105
|
+
return {
|
|
106
|
+
text: suffix,
|
|
107
|
+
markSent: () => {
|
|
108
|
+
lastSentAnswerText = current;
|
|
109
|
+
},
|
|
110
|
+
};
|
|
94
111
|
}
|
|
95
112
|
|
|
96
113
|
if (current.trim() && lastSentAnswerText && !current.startsWith(lastSentAnswerText)) {
|
|
97
114
|
const suffix = computeSharedPrefixTail(lastSentAnswerText, current);
|
|
98
115
|
ctx.log?.warn?.(
|
|
99
116
|
`[DingTalk][Markdown] answer prefix drift detected; falling back to shared-prefix tail ` +
|
|
100
|
-
|
|
117
|
+
`prevLen=${lastSentAnswerText.length} currentLen=${current.length}`,
|
|
101
118
|
);
|
|
102
|
-
lastSentAnswerText = "";
|
|
103
119
|
if (suffix) {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
120
|
+
return {
|
|
121
|
+
text: suffix,
|
|
122
|
+
markSent: () => {
|
|
123
|
+
lastSentAnswerText = current;
|
|
124
|
+
},
|
|
125
|
+
};
|
|
107
126
|
}
|
|
108
|
-
|
|
109
|
-
|
|
127
|
+
return {
|
|
128
|
+
text: current,
|
|
129
|
+
markSent: () => {
|
|
130
|
+
lastSentAnswerText = current;
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return null;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const emitAnswerSuffix = async (text: string | undefined): Promise<void> => {
|
|
139
|
+
const suffix = prepareAnswerSuffix(text);
|
|
140
|
+
if (suffix) {
|
|
141
|
+
await sendMarkdownSegment(suffix.text);
|
|
142
|
+
suffix.markSent();
|
|
110
143
|
}
|
|
111
144
|
};
|
|
112
145
|
|
|
146
|
+
const prepareMarkdownImageAttachments = async (
|
|
147
|
+
mediaUrls: string[],
|
|
148
|
+
): Promise<{
|
|
149
|
+
imageMarkdown: string[];
|
|
150
|
+
passthroughMediaUrls: string[];
|
|
151
|
+
cleanups: Array<() => Promise<void>>;
|
|
152
|
+
}> => {
|
|
153
|
+
const imageMarkdown: string[] = [];
|
|
154
|
+
const passthroughMediaUrls: string[] = [];
|
|
155
|
+
const cleanups: Array<() => Promise<void>> = [];
|
|
156
|
+
|
|
157
|
+
for (const rawMediaUrl of mediaUrls) {
|
|
158
|
+
const preparedMedia = await prepareMediaInput(
|
|
159
|
+
rawMediaUrl,
|
|
160
|
+
ctx.log,
|
|
161
|
+
ctx.config.mediaUrlAllowlist,
|
|
162
|
+
);
|
|
163
|
+
const actualMediaPath = preparedMedia.cleanup
|
|
164
|
+
? preparedMedia.path
|
|
165
|
+
: resolveRelativePath(preparedMedia.path);
|
|
166
|
+
const mediaType = resolveOutboundMediaType({
|
|
167
|
+
mediaPath: actualMediaPath,
|
|
168
|
+
asVoice: false,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
if (mediaType === "image") {
|
|
172
|
+
imageMarkdown.push(renderMarkdownImage(actualMediaPath));
|
|
173
|
+
if (preparedMedia.cleanup) {
|
|
174
|
+
cleanups.push(preparedMedia.cleanup);
|
|
175
|
+
}
|
|
176
|
+
} else {
|
|
177
|
+
await preparedMedia.cleanup?.();
|
|
178
|
+
passthroughMediaUrls.push(rawMediaUrl);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return { imageMarkdown, passthroughMediaUrls, cleanups };
|
|
183
|
+
};
|
|
184
|
+
|
|
113
185
|
return {
|
|
114
186
|
getReplyOptions(): ReplyOptions {
|
|
115
187
|
return {
|
|
116
188
|
disableBlockStreaming: ctx.disableBlockStreaming === true,
|
|
189
|
+
// DingTalk markdown/sessionWebhook mode owns the visible reply surface.
|
|
190
|
+
// Keep runtime final replies on this strategy even when group chats
|
|
191
|
+
// default source replies to message-tool-only.
|
|
192
|
+
sourceReplyDeliveryMode: "automatic",
|
|
117
193
|
};
|
|
118
194
|
},
|
|
119
195
|
|
|
120
196
|
async deliver(payload: DeliverPayload): Promise<void> {
|
|
197
|
+
let answerTextSentWithImages = false;
|
|
198
|
+
let toolTextSentWithImages = false;
|
|
199
|
+
|
|
121
200
|
if (payload.mediaUrls.length > 0) {
|
|
122
|
-
|
|
123
|
-
|
|
201
|
+
const prepared =
|
|
202
|
+
payload.audioAsVoice === true
|
|
203
|
+
? {
|
|
204
|
+
imageMarkdown: [],
|
|
205
|
+
passthroughMediaUrls: payload.mediaUrls,
|
|
206
|
+
cleanups: [],
|
|
207
|
+
}
|
|
208
|
+
: await prepareMarkdownImageAttachments(payload.mediaUrls);
|
|
209
|
+
try {
|
|
210
|
+
if (prepared.passthroughMediaUrls.length > 0) {
|
|
211
|
+
await ctx.deliverMedia(prepared.passthroughMediaUrls, {
|
|
212
|
+
audioAsVoice: payload.audioAsVoice,
|
|
213
|
+
});
|
|
214
|
+
sentVisibleContent = true;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (prepared.imageMarkdown.length > 0) {
|
|
218
|
+
const answerSuffix =
|
|
219
|
+
payload.kind === "block" || payload.kind === "final"
|
|
220
|
+
? prepareAnswerSuffix(payload.text)
|
|
221
|
+
: typeof payload.text === "string"
|
|
222
|
+
? { text: renderQuotedSegment(payload.text), markSent: () => {} }
|
|
223
|
+
: null;
|
|
224
|
+
const markdownParts = [answerSuffix?.text || "", ...prepared.imageMarkdown].filter(
|
|
225
|
+
(part) => part.trim().length > 0,
|
|
226
|
+
);
|
|
227
|
+
if (markdownParts.length > 0) {
|
|
228
|
+
await sendMarkdownSegment(markdownParts.join("\n\n"));
|
|
229
|
+
answerSuffix?.markSent();
|
|
230
|
+
}
|
|
231
|
+
answerTextSentWithImages = payload.kind === "block" || payload.kind === "final";
|
|
232
|
+
toolTextSentWithImages = payload.kind === "tool";
|
|
233
|
+
}
|
|
234
|
+
} finally {
|
|
235
|
+
for (const cleanup of prepared.cleanups) {
|
|
236
|
+
await cleanup();
|
|
237
|
+
}
|
|
238
|
+
}
|
|
124
239
|
}
|
|
125
240
|
|
|
126
241
|
if (payload.kind === "tool") {
|
|
242
|
+
if (toolTextSentWithImages) {
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
127
245
|
const text = typeof payload.text === "string" ? payload.text : "";
|
|
128
246
|
if (!text.trim()) {
|
|
129
247
|
return;
|
|
@@ -133,8 +251,9 @@ export function createMarkdownReplyStrategy(
|
|
|
133
251
|
}
|
|
134
252
|
|
|
135
253
|
if (
|
|
136
|
-
(payload.kind === "block" || payload.kind === "final")
|
|
137
|
-
|
|
254
|
+
(payload.kind === "block" || payload.kind === "final") &&
|
|
255
|
+
typeof payload.text === "string" &&
|
|
256
|
+
!answerTextSentWithImages
|
|
138
257
|
) {
|
|
139
258
|
await emitAnswerSuffix(payload.text);
|
|
140
259
|
}
|
|
@@ -16,6 +16,8 @@ export type InternalReplyStrategyConfig = DingTalkConfig & {
|
|
|
16
16
|
cardStreamReasoning?: boolean;
|
|
17
17
|
};
|
|
18
18
|
|
|
19
|
+
export type SourceReplyDeliveryMode = "automatic" | "message_tool_only";
|
|
20
|
+
|
|
19
21
|
// ---- Public interfaces ----
|
|
20
22
|
|
|
21
23
|
export interface DeliverPayload {
|
|
@@ -33,6 +35,7 @@ export interface DeliverPayload {
|
|
|
33
35
|
|
|
34
36
|
export interface ReplyOptions {
|
|
35
37
|
disableBlockStreaming: boolean;
|
|
38
|
+
sourceReplyDeliveryMode?: SourceReplyDeliveryMode;
|
|
36
39
|
onPartialReply?: (payload: { text?: string }) => void | Promise<void>;
|
|
37
40
|
onReasoningStream?: (payload: { text?: string }) => void | Promise<void>;
|
|
38
41
|
onAssistantMessageStart?: () => void | Promise<void>;
|
package/src/send-service.ts
CHANGED
|
@@ -182,6 +182,10 @@ function buildPersistedOutboundText(text: string, options: SendMessageOptions):
|
|
|
182
182
|
return text;
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
+
function shouldRouteSessionMediaViaProactive(mediaType?: string | null): mediaType is "voice" | "video" | "file" {
|
|
186
|
+
return mediaType === "voice" || mediaType === "video" || mediaType === "file";
|
|
187
|
+
}
|
|
188
|
+
|
|
185
189
|
const DINGTALK_TEXT_CHUNK_LIMIT = 3800;
|
|
186
190
|
const CARD_MEDIA_CONTROLLER_ATTACH_WAIT_MS = 150;
|
|
187
191
|
const CARD_MEDIA_CONTROLLER_ATTACH_POLL_MS = 25;
|
|
@@ -726,55 +730,30 @@ export async function sendBySession(
|
|
|
726
730
|
const token = await getAccessToken(config, options.log);
|
|
727
731
|
const log = options.log || getLogger();
|
|
728
732
|
|
|
729
|
-
//
|
|
733
|
+
// Keep session webhooks on text/markdown. Images can render through markdown
|
|
734
|
+
// media references; other media types are routed by sendMessage via OpenAPI.
|
|
730
735
|
if (options.mediaPath && options.mediaType) {
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
if (options.mediaType === "image") {
|
|
739
|
-
body = { msgtype: "image", image: { media_id: mediaId } };
|
|
740
|
-
} else if (options.mediaType === "voice") {
|
|
741
|
-
const durationMs = uploadedDurationMs
|
|
742
|
-
?? await getVoiceDurationMs(options.mediaPath, options.mediaType, log, { preReadBuffer: buffer });
|
|
743
|
-
body = { msgtype: "voice", voice: { media_id: mediaId, duration: String(durationMs) } };
|
|
744
|
-
log?.debug?.(
|
|
745
|
-
`[DingTalk] Sending session voice message mediaId=${mediaId} durationMs=${durationMs}`,
|
|
746
|
-
);
|
|
747
|
-
} else if (options.mediaType === "video") {
|
|
748
|
-
body = { msgtype: "video", video: { media_id: mediaId } };
|
|
749
|
-
} else if (options.mediaType === "file") {
|
|
750
|
-
body = { msgtype: "file", file: { media_id: mediaId } };
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
if (body) {
|
|
754
|
-
const result = await axios({
|
|
755
|
-
url: sessionWebhook,
|
|
756
|
-
method: "POST",
|
|
757
|
-
data: body,
|
|
758
|
-
headers: { "x-acs-dingtalk-access-token": token, "Content-Type": "application/json" },
|
|
759
|
-
...getProxyBypassOption(config),
|
|
760
|
-
});
|
|
736
|
+
if (options.mediaType === "image") {
|
|
737
|
+
const uploadResult = await uploadMedia(config, options.mediaPath, options.mediaType, log, {
|
|
738
|
+
mediaLocalRoots: options.mediaLocalRoots,
|
|
739
|
+
});
|
|
740
|
+
if (uploadResult) {
|
|
741
|
+
const imageMarkdown = ``;
|
|
742
|
+
text = text ? `${text}\n\n${imageMarkdown}` : imageMarkdown;
|
|
761
743
|
log?.debug?.(
|
|
762
|
-
`[DingTalk] Session webhook
|
|
744
|
+
`[DingTalk] Session webhook image will be delivered as markdown media reference mediaId=${uploadResult.mediaId}`,
|
|
763
745
|
);
|
|
764
|
-
|
|
765
|
-
const
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
`[DingTalk] Session webhook ${body.msgtype} response missing delivery metadata; ` +
|
|
769
|
-
summarizeSessionWebhookResponse(result.data),
|
|
770
|
-
);
|
|
771
|
-
}
|
|
772
|
-
return result.data;
|
|
746
|
+
} else {
|
|
747
|
+
const mediaHint = options.mediaUrl || options.mediaPath || options.filePath || "(媒体发送失败)";
|
|
748
|
+
text = `${text}\n\n📎 媒体发送失败,兜底链接/路径:${mediaHint}`.trim();
|
|
749
|
+
log?.warn?.("[DingTalk] Media upload failed, falling back to text description");
|
|
773
750
|
}
|
|
774
751
|
} else {
|
|
775
752
|
const mediaHint = options.mediaUrl || options.mediaPath || options.filePath || "(媒体发送失败)";
|
|
776
|
-
text = `${text}\n\n📎
|
|
777
|
-
log?.warn?.(
|
|
753
|
+
text = `${text}\n\n📎 当前会话无法直接发送 ${options.mediaType},兜底链接/路径:${mediaHint}`.trim();
|
|
754
|
+
log?.warn?.(
|
|
755
|
+
`[DingTalk] Session webhook does not support native ${options.mediaType} replies; falling back to text description`,
|
|
756
|
+
);
|
|
778
757
|
}
|
|
779
758
|
}
|
|
780
759
|
|
|
@@ -835,6 +814,51 @@ export async function sendMessage(
|
|
|
835
814
|
const messageType = config.messageType || "markdown";
|
|
836
815
|
const log = options.log || getLogger();
|
|
837
816
|
|
|
817
|
+
if (options.sessionWebhook && options.mediaPath && shouldRouteSessionMediaViaProactive(options.mediaType)) {
|
|
818
|
+
log?.debug?.(
|
|
819
|
+
`[DingTalk] Session webhook does not support ${options.mediaType} replies reliably; ` +
|
|
820
|
+
"using proactive media API instead",
|
|
821
|
+
);
|
|
822
|
+
const proactiveMediaResult = await sendProactiveMedia(
|
|
823
|
+
config,
|
|
824
|
+
conversationId,
|
|
825
|
+
options.mediaPath,
|
|
826
|
+
options.mediaType,
|
|
827
|
+
options,
|
|
828
|
+
);
|
|
829
|
+
if (!proactiveMediaResult.ok) {
|
|
830
|
+
log?.warn?.(
|
|
831
|
+
`[DingTalk] Proactive ${options.mediaType} reply failed; falling back to session markdown: ` +
|
|
832
|
+
(proactiveMediaResult.error || "unknown"),
|
|
833
|
+
);
|
|
834
|
+
const data = await sendBySession(config, options.sessionWebhook, text, options);
|
|
835
|
+
const delivery = extractOutboundDeliveryMetadata(data);
|
|
836
|
+
const messageId = delivery.messageId || delivery.processQueryKey || delivery.outTrackId;
|
|
837
|
+
const persistedText = buildPersistedOutboundText(text, options);
|
|
838
|
+
persistOutboundMessageContext({
|
|
839
|
+
storePath: options.storePath,
|
|
840
|
+
accountId: options.accountId,
|
|
841
|
+
conversationId: options.conversationId || conversationId,
|
|
842
|
+
text: persistedText,
|
|
843
|
+
messageType: "outbound-media",
|
|
844
|
+
quotedRef: options.quotedRef,
|
|
845
|
+
log,
|
|
846
|
+
...DEFAULT_OUTBOUND_SENDER,
|
|
847
|
+
chatType: inferConversationChatType(options.conversationId || conversationId),
|
|
848
|
+
delivery: {
|
|
849
|
+
...delivery,
|
|
850
|
+
kind: "session",
|
|
851
|
+
},
|
|
852
|
+
});
|
|
853
|
+
return { ok: true, data, messageId };
|
|
854
|
+
}
|
|
855
|
+
return {
|
|
856
|
+
ok: true,
|
|
857
|
+
data: proactiveMediaResult.data,
|
|
858
|
+
messageId: proactiveMediaResult.messageId,
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
|
|
838
862
|
if (messageType === "card" && options.card && !options.forceMarkdown) {
|
|
839
863
|
const card = options.card;
|
|
840
864
|
if (isCardInTerminalState(card.state)) {
|
|
@@ -858,28 +882,6 @@ export async function sendMessage(
|
|
|
858
882
|
}
|
|
859
883
|
}
|
|
860
884
|
|
|
861
|
-
if (options.sessionWebhook && options.mediaPath && options.mediaType === "voice") {
|
|
862
|
-
log?.debug?.(
|
|
863
|
-
"[DingTalk] Session webhook does not support voice replies reliably; " +
|
|
864
|
-
"using proactive media API for this voice response",
|
|
865
|
-
);
|
|
866
|
-
const proactiveVoiceResult = await sendProactiveMedia(
|
|
867
|
-
config,
|
|
868
|
-
conversationId,
|
|
869
|
-
options.mediaPath,
|
|
870
|
-
options.mediaType,
|
|
871
|
-
options,
|
|
872
|
-
);
|
|
873
|
-
if (!proactiveVoiceResult.ok) {
|
|
874
|
-
return { ok: false, error: proactiveVoiceResult.error || "Voice reply send failed" };
|
|
875
|
-
}
|
|
876
|
-
return {
|
|
877
|
-
ok: true,
|
|
878
|
-
data: proactiveVoiceResult.data,
|
|
879
|
-
messageId: proactiveVoiceResult.messageId,
|
|
880
|
-
};
|
|
881
|
-
}
|
|
882
|
-
|
|
883
885
|
if (options.sessionWebhook) {
|
|
884
886
|
const data = await sendBySession(config, options.sessionWebhook, text, options);
|
|
885
887
|
const delivery = extractOutboundDeliveryMetadata(data);
|