@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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@soimy/dingtalk",
|
|
3
|
-
"version": "3.5.
|
|
3
|
+
"version": "3.5.3",
|
|
4
4
|
"description": "DingTalk (钉钉) channel plugin for OpenClaw",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bot",
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
"vitest": "^3.2.4"
|
|
65
65
|
},
|
|
66
66
|
"peerDependencies": {
|
|
67
|
-
"openclaw": ">=2026.3.
|
|
67
|
+
"openclaw": ">=2026.3.28"
|
|
68
68
|
},
|
|
69
69
|
"peerDependenciesMeta": {
|
|
70
70
|
"openclaw": {
|
|
@@ -73,10 +73,10 @@
|
|
|
73
73
|
},
|
|
74
74
|
"openclaw": {
|
|
75
75
|
"compat": {
|
|
76
|
-
"pluginApi": ">=2026.3.
|
|
76
|
+
"pluginApi": ">=2026.3.28"
|
|
77
77
|
},
|
|
78
78
|
"build": {
|
|
79
|
-
"openclawVersion": "2026.3.
|
|
79
|
+
"openclawVersion": "2026.3.28"
|
|
80
80
|
},
|
|
81
81
|
"extensions": [
|
|
82
82
|
"./index.ts"
|
|
@@ -99,7 +99,7 @@
|
|
|
99
99
|
]
|
|
100
100
|
},
|
|
101
101
|
"install": {
|
|
102
|
-
"minHostVersion": ">=2026.3.
|
|
102
|
+
"minHostVersion": ">=2026.3.28",
|
|
103
103
|
"npmSpec": "@soimy/dingtalk",
|
|
104
104
|
"localPath": ".",
|
|
105
105
|
"defaultChoice": "npm"
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { DingTalkConfig } from "../types";
|
|
2
|
+
|
|
3
|
+
export type CardStreamingMode = "off" | "answer" | "all";
|
|
4
|
+
|
|
5
|
+
// Process-lifetime one-shot warnings are intentional here: a given config key
|
|
6
|
+
// should only emit the deprecation notice once per runtime.
|
|
7
|
+
const warnedLegacyConfigs = new Set<string>();
|
|
8
|
+
|
|
9
|
+
export function resolveCardStreamingMode(
|
|
10
|
+
config: Pick<DingTalkConfig, "cardStreamingMode" | "cardRealTimeStream">,
|
|
11
|
+
): {
|
|
12
|
+
mode: CardStreamingMode;
|
|
13
|
+
usedDeprecatedCardRealTimeStream: boolean;
|
|
14
|
+
} {
|
|
15
|
+
if (config.cardStreamingMode) {
|
|
16
|
+
return { mode: config.cardStreamingMode, usedDeprecatedCardRealTimeStream: false };
|
|
17
|
+
}
|
|
18
|
+
if (config.cardRealTimeStream === true) {
|
|
19
|
+
return { mode: "all", usedDeprecatedCardRealTimeStream: true };
|
|
20
|
+
}
|
|
21
|
+
return { mode: "off", usedDeprecatedCardRealTimeStream: false };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function shouldWarnDeprecatedCardRealTimeStreamOnce(configKey: string): boolean {
|
|
25
|
+
if (warnedLegacyConfigs.has(configKey)) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
warnedLegacyConfigs.add(configKey);
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
export interface CardReasoningAnswerSplit {
|
|
2
|
+
reasoningText?: string;
|
|
3
|
+
answerText?: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const THINKING_TAG_RE = /<\s*(\/?)\s*(?:think(?:ing)?|thought|antthinking)\b[^<>]*>/gi;
|
|
7
|
+
|
|
8
|
+
function isWrappedReasoningLine(line: string): boolean {
|
|
9
|
+
const trimmed = line.trim();
|
|
10
|
+
return trimmed.startsWith("_") && trimmed.endsWith("_") && trimmed.length >= 3;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function cleanReasoningLine(line: string): string {
|
|
14
|
+
const trimmed = line.trim();
|
|
15
|
+
if (!trimmed) {
|
|
16
|
+
return "";
|
|
17
|
+
}
|
|
18
|
+
return trimmed.replace(/^_/, "").replace(/_$/, "").trim();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function joinAnswerParts(parts: string[]): string | undefined {
|
|
22
|
+
const joined = parts.map((part) => part.trim()).filter(Boolean).join("\n\n").trim();
|
|
23
|
+
return joined || undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function hasStructuredMarkdown(text: string): boolean {
|
|
27
|
+
const trimmed = text.trim();
|
|
28
|
+
if (!trimmed) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
return (
|
|
32
|
+
trimmed.includes("**")
|
|
33
|
+
|| trimmed.includes("```")
|
|
34
|
+
|| /(^|\n)\s*(?:[-*+]\s|#{1,6}\s|>\s|\d+\.\s)/.test(trimmed)
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function splitTopLevelReasoningPrefix(text: string): CardReasoningAnswerSplit | null {
|
|
39
|
+
const markerIndex = text.indexOf("Reasoning:");
|
|
40
|
+
if (markerIndex < 0) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const before = text.slice(0, markerIndex).trim();
|
|
45
|
+
if (before && hasStructuredMarkdown(before)) {
|
|
46
|
+
return {
|
|
47
|
+
answerText: text,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const trailing = text.slice(markerIndex + "Reasoning:".length);
|
|
52
|
+
const lines = trailing.split("\n");
|
|
53
|
+
const reasoningLines: string[] = [];
|
|
54
|
+
let started = false;
|
|
55
|
+
let remainderIndex = -1;
|
|
56
|
+
|
|
57
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
58
|
+
const line = lines[index];
|
|
59
|
+
const trimmed = line.trim();
|
|
60
|
+
|
|
61
|
+
if (!started) {
|
|
62
|
+
if (!trimmed) {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (!isWrappedReasoningLine(trimmed)) {
|
|
66
|
+
return {
|
|
67
|
+
answerText: text,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
started = true;
|
|
71
|
+
reasoningLines.push(cleanReasoningLine(trimmed));
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!trimmed) {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (isWrappedReasoningLine(trimmed)) {
|
|
80
|
+
reasoningLines.push(cleanReasoningLine(trimmed));
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
remainderIndex = index;
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (reasoningLines.length === 0) {
|
|
89
|
+
return {
|
|
90
|
+
answerText: text,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const after = remainderIndex >= 0 ? lines.slice(remainderIndex).join("\n").trim() : "";
|
|
95
|
+
return {
|
|
96
|
+
reasoningText: reasoningLines.join("\n").trim() || undefined,
|
|
97
|
+
answerText: joinAnswerParts([before, after]),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function splitTopLevelThinkingTags(text: string): CardReasoningAnswerSplit | null {
|
|
102
|
+
if (!THINKING_TAG_RE.test(text)) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
THINKING_TAG_RE.lastIndex = 0;
|
|
106
|
+
|
|
107
|
+
let reasoning = "";
|
|
108
|
+
let answer = "";
|
|
109
|
+
let lastIndex = 0;
|
|
110
|
+
let inThinking = false;
|
|
111
|
+
|
|
112
|
+
for (const match of text.matchAll(THINKING_TAG_RE)) {
|
|
113
|
+
const matchIndex = match.index ?? 0;
|
|
114
|
+
const segment = text.slice(lastIndex, matchIndex);
|
|
115
|
+
if (inThinking) {
|
|
116
|
+
reasoning += segment;
|
|
117
|
+
} else {
|
|
118
|
+
answer += segment;
|
|
119
|
+
}
|
|
120
|
+
inThinking = match[1] !== "/";
|
|
121
|
+
lastIndex = matchIndex + match[0].length;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const tail = text.slice(lastIndex);
|
|
125
|
+
if (inThinking) {
|
|
126
|
+
reasoning += tail;
|
|
127
|
+
} else {
|
|
128
|
+
answer += tail;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const cleanedReasoning = reasoning.trim();
|
|
132
|
+
if (!cleanedReasoning) {
|
|
133
|
+
return {
|
|
134
|
+
answerText: text,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
reasoningText: cleanedReasoning,
|
|
140
|
+
answerText: answer.trim() || undefined,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function splitCardReasoningAnswerText(text?: string): CardReasoningAnswerSplit {
|
|
145
|
+
if (typeof text !== "string") {
|
|
146
|
+
return {};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const prefixed = splitTopLevelReasoningPrefix(text);
|
|
150
|
+
if (prefixed) {
|
|
151
|
+
return prefixed;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const tagged = splitTopLevelThinkingTags(text);
|
|
155
|
+
if (tagged) {
|
|
156
|
+
return tagged;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
answerText: text,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
@@ -22,15 +22,18 @@ type TimelineEntry = {
|
|
|
22
22
|
};
|
|
23
23
|
|
|
24
24
|
export interface CardDraftController {
|
|
25
|
-
updateAnswer: (text: string) => Promise<void>;
|
|
25
|
+
updateAnswer: (text: string, options?: { stream?: boolean }) => Promise<void>;
|
|
26
26
|
updateReasoning: (text: string) => Promise<void>;
|
|
27
27
|
updateThinking: (text: string) => Promise<void>;
|
|
28
28
|
appendThinkingBlock: (text: string) => Promise<void>;
|
|
29
29
|
updateTool: (text: string) => Promise<void>;
|
|
30
30
|
appendTool: (text: string) => Promise<void>;
|
|
31
|
+
appendToolBeforeCurrentAnswer: (text: string) => Promise<void>;
|
|
31
32
|
/** Signal that a new assistant turn has started (e.g. after a tool call). */
|
|
32
33
|
notifyNewAssistantTurn: () => Promise<void>;
|
|
33
34
|
startAssistantTurn: () => Promise<void>;
|
|
35
|
+
/** Seal the active thinking entry (keep it in timeline) without removing it. */
|
|
36
|
+
sealActiveThinking: () => Promise<void>;
|
|
34
37
|
flush: () => Promise<void>;
|
|
35
38
|
waitForInFlight: () => Promise<void>;
|
|
36
39
|
stop: () => void;
|
|
@@ -78,6 +81,8 @@ export function createCardDraftController(params: {
|
|
|
78
81
|
let failed = false;
|
|
79
82
|
let stopped = false;
|
|
80
83
|
let lastSentContent = "";
|
|
84
|
+
let lastQueuedContent = "";
|
|
85
|
+
let inFlightContent = "";
|
|
81
86
|
let lastAnswerContent = "";
|
|
82
87
|
|
|
83
88
|
let timelineEntries: TimelineEntry[] = [];
|
|
@@ -121,6 +126,15 @@ export function createCardDraftController(params: {
|
|
|
121
126
|
return activeAnswerIndex;
|
|
122
127
|
};
|
|
123
128
|
|
|
129
|
+
const findLastAnswerEntryIndex = (): number | null => {
|
|
130
|
+
for (let index = timelineEntries.length - 1; index >= 0; index -= 1) {
|
|
131
|
+
if (timelineEntries[index]?.kind === "answer") {
|
|
132
|
+
return index;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return null;
|
|
136
|
+
};
|
|
137
|
+
|
|
124
138
|
const renderTimeline = (options: {
|
|
125
139
|
fallbackAnswer?: string;
|
|
126
140
|
overrideAnswer?: string;
|
|
@@ -179,13 +193,29 @@ export function createCardDraftController(params: {
|
|
|
179
193
|
activeAnswerIndex = null;
|
|
180
194
|
};
|
|
181
195
|
|
|
196
|
+
const clearPendingRender = () => {
|
|
197
|
+
loop.resetPending();
|
|
198
|
+
lastQueuedContent = "";
|
|
199
|
+
};
|
|
200
|
+
|
|
182
201
|
const queueRender = () => {
|
|
183
202
|
const rendered = renderTimeline({ compactProcessAnswerSpacing: true });
|
|
184
|
-
if (rendered) {
|
|
185
|
-
|
|
203
|
+
if (!rendered) {
|
|
204
|
+
clearPendingRender();
|
|
186
205
|
return;
|
|
187
206
|
}
|
|
188
|
-
|
|
207
|
+
if (rendered === lastSentContent) {
|
|
208
|
+
const hasNewerInFlight = !!inFlightContent && inFlightContent !== rendered;
|
|
209
|
+
if (!hasNewerInFlight) {
|
|
210
|
+
clearPendingRender();
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (rendered === lastQueuedContent) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
lastQueuedContent = rendered;
|
|
218
|
+
loop.update(rendered);
|
|
189
219
|
};
|
|
190
220
|
|
|
191
221
|
const flushBoundaryFrame = async () => {
|
|
@@ -220,14 +250,20 @@ export function createCardDraftController(params: {
|
|
|
220
250
|
throttleMs: effectiveThrottleMs,
|
|
221
251
|
isStopped: () => stopped || failed,
|
|
222
252
|
sendOrEditStreamMessage: async (content: string) => {
|
|
253
|
+
inFlightContent = content;
|
|
223
254
|
try {
|
|
224
255
|
await streamAICard(params.card, content, false, params.log);
|
|
225
256
|
lastSentContent = content;
|
|
257
|
+
lastQueuedContent = "";
|
|
226
258
|
lastAnswerContent = getFinalAnswerContent();
|
|
227
259
|
} catch (err: unknown) {
|
|
228
260
|
failed = true;
|
|
229
261
|
const message = err instanceof Error ? err.message : String(err);
|
|
230
262
|
params.log?.warn?.(`[DingTalk][AICard] Stream failed: ${message}`);
|
|
263
|
+
} finally {
|
|
264
|
+
if (inFlightContent === content) {
|
|
265
|
+
inFlightContent = "";
|
|
266
|
+
}
|
|
231
267
|
}
|
|
232
268
|
},
|
|
233
269
|
});
|
|
@@ -255,7 +291,7 @@ export function createCardDraftController(params: {
|
|
|
255
291
|
queueRender();
|
|
256
292
|
};
|
|
257
293
|
|
|
258
|
-
const updateAnswer = async (text: string) => {
|
|
294
|
+
const updateAnswer = async (text: string, options: { stream?: boolean } = {}) => {
|
|
259
295
|
await waitForPendingBoundary();
|
|
260
296
|
if (stopped || failed) {
|
|
261
297
|
return;
|
|
@@ -276,6 +312,10 @@ export function createCardDraftController(params: {
|
|
|
276
312
|
} else {
|
|
277
313
|
activeAnswerIndex = appendTimelineEntry("answer", normalized);
|
|
278
314
|
}
|
|
315
|
+
if (options.stream === false) {
|
|
316
|
+
clearPendingRender();
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
279
319
|
queueRender();
|
|
280
320
|
};
|
|
281
321
|
|
|
@@ -323,6 +363,35 @@ export function createCardDraftController(params: {
|
|
|
323
363
|
queueRender();
|
|
324
364
|
};
|
|
325
365
|
|
|
366
|
+
const appendToolBeforeCurrentAnswer = async (text: string) => {
|
|
367
|
+
await waitForPendingBoundary();
|
|
368
|
+
if (stopped || failed) {
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
const normalized = normalizeProcessText(text);
|
|
372
|
+
if (!normalized) {
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
if (timelineEntries.length > 0) {
|
|
376
|
+
await flushBoundaryFrame();
|
|
377
|
+
}
|
|
378
|
+
sealLiveThinking();
|
|
379
|
+
const insertionIndex = findCurrentSegmentAnswerIndex() ?? findLastAnswerEntryIndex();
|
|
380
|
+
if (insertionIndex !== null) {
|
|
381
|
+
timelineEntries.splice(insertionIndex, 0, { kind: "tool", text: normalized });
|
|
382
|
+
if (activeAnswerIndex !== null && activeAnswerIndex >= insertionIndex) {
|
|
383
|
+
activeAnswerIndex += 1;
|
|
384
|
+
}
|
|
385
|
+
if (activeThinkingIndex !== null && activeThinkingIndex >= insertionIndex) {
|
|
386
|
+
activeThinkingIndex += 1;
|
|
387
|
+
}
|
|
388
|
+
} else {
|
|
389
|
+
sealCurrentAnswer();
|
|
390
|
+
appendTimelineEntry("tool", normalized);
|
|
391
|
+
}
|
|
392
|
+
queueRender();
|
|
393
|
+
};
|
|
394
|
+
|
|
326
395
|
const notifyNewAssistantTurn = async () => {
|
|
327
396
|
if (stopped || failed) {
|
|
328
397
|
return;
|
|
@@ -334,7 +403,7 @@ export function createCardDraftController(params: {
|
|
|
334
403
|
}
|
|
335
404
|
if (activeThinkingIndex !== null) {
|
|
336
405
|
removeTimelineEntry(activeThinkingIndex);
|
|
337
|
-
|
|
406
|
+
clearPendingRender();
|
|
338
407
|
}
|
|
339
408
|
};
|
|
340
409
|
|
|
@@ -345,8 +414,18 @@ export function createCardDraftController(params: {
|
|
|
345
414
|
appendThinkingBlock,
|
|
346
415
|
updateTool,
|
|
347
416
|
appendTool: updateTool,
|
|
417
|
+
appendToolBeforeCurrentAnswer,
|
|
348
418
|
notifyNewAssistantTurn,
|
|
349
419
|
startAssistantTurn: notifyNewAssistantTurn,
|
|
420
|
+
sealActiveThinking: async () => {
|
|
421
|
+
if (stopped || failed) {
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
if (activeThinkingIndex !== null) {
|
|
425
|
+
sealLiveThinking();
|
|
426
|
+
await beginBoundaryFlush();
|
|
427
|
+
}
|
|
428
|
+
},
|
|
350
429
|
flush: () => loop.flush(),
|
|
351
430
|
waitForInFlight: () => loop.waitForInFlight(),
|
|
352
431
|
|
package/src/card-service.ts
CHANGED
|
@@ -904,6 +904,117 @@ export async function finishAICard(
|
|
|
904
904
|
}
|
|
905
905
|
}
|
|
906
906
|
|
|
907
|
+
function getCardRecallTarget(card: AICardInstance): {
|
|
908
|
+
isGroup: boolean;
|
|
909
|
+
conversationId?: string;
|
|
910
|
+
} {
|
|
911
|
+
const { targetId, isExplicitUser } = stripTargetPrefix(card.conversationId);
|
|
912
|
+
const resolvedTarget = resolveOriginalPeerId(targetId);
|
|
913
|
+
const isGroup = !isExplicitUser && resolvedTarget.startsWith("cid");
|
|
914
|
+
return {
|
|
915
|
+
isGroup,
|
|
916
|
+
conversationId: resolvedTarget || undefined,
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
function parseRecallFailureEntries(payload: unknown): Array<[string, string]> {
|
|
921
|
+
if (!payload || typeof payload !== "object") {
|
|
922
|
+
return [];
|
|
923
|
+
}
|
|
924
|
+
return Object.entries(payload as Record<string, unknown>)
|
|
925
|
+
.map(([key, value]) => [String(key), String(value ?? "")] as [string, string]);
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
export async function recallAICardMessage(
|
|
929
|
+
card: AICardInstance,
|
|
930
|
+
log?: Logger,
|
|
931
|
+
): Promise<boolean> {
|
|
932
|
+
const config = card.config;
|
|
933
|
+
const processQueryKey = card.processQueryKey?.trim();
|
|
934
|
+
const robotCode = config ? resolveRobotCode(config) : "";
|
|
935
|
+
|
|
936
|
+
if (!config || !processQueryKey || !robotCode) {
|
|
937
|
+
log?.warn?.(
|
|
938
|
+
`[DingTalk][AICard] Skip recall because required metadata is missing: ` +
|
|
939
|
+
`card=${card.cardInstanceId} hasConfig=${Boolean(config)} ` +
|
|
940
|
+
`processQueryKey=${processQueryKey || "(none)"} robotCode=${robotCode || "(none)"}`,
|
|
941
|
+
);
|
|
942
|
+
return false;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
const target = getCardRecallTarget(card);
|
|
946
|
+
if (!target.conversationId) {
|
|
947
|
+
log?.warn?.(
|
|
948
|
+
`[DingTalk][AICard] Skip recall because conversationId is invalid: card=${card.cardInstanceId} conversationId=${card.conversationId}`,
|
|
949
|
+
);
|
|
950
|
+
return false;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
const url = target.isGroup
|
|
954
|
+
? `${DINGTALK_API}/v1.0/robot/groupMessages/recall`
|
|
955
|
+
: `${DINGTALK_API}/v1.0/robot/otoMessages/batchRecall`;
|
|
956
|
+
const body: Record<string, unknown> = {
|
|
957
|
+
robotCode,
|
|
958
|
+
processQueryKeys: [processQueryKey],
|
|
959
|
+
};
|
|
960
|
+
if (target.isGroup) {
|
|
961
|
+
body.openConversationId = target.conversationId;
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
try {
|
|
965
|
+
const token = await getAccessToken(config, log);
|
|
966
|
+
const response = await axios.post(url, body, {
|
|
967
|
+
headers: {
|
|
968
|
+
"x-acs-dingtalk-access-token": token,
|
|
969
|
+
"Content-Type": "application/json",
|
|
970
|
+
},
|
|
971
|
+
...getProxyBypassOption(config),
|
|
972
|
+
});
|
|
973
|
+
const successResults = Array.isArray((response.data as Record<string, unknown> | undefined)?.successResult)
|
|
974
|
+
? ((response.data as Record<string, unknown>).successResult as unknown[])
|
|
975
|
+
.map((item) => String(item))
|
|
976
|
+
: [];
|
|
977
|
+
const failedEntries = parseRecallFailureEntries(
|
|
978
|
+
(response.data as Record<string, unknown> | undefined)?.failedResult,
|
|
979
|
+
);
|
|
980
|
+
if (failedEntries.length > 0) {
|
|
981
|
+
log?.warn?.(
|
|
982
|
+
`[DingTalk][AICard] Recall reported failedResult: card=${card.cardInstanceId} ` +
|
|
983
|
+
`processQueryKey=${processQueryKey} failed=${JSON.stringify(failedEntries)}`,
|
|
984
|
+
);
|
|
985
|
+
return false;
|
|
986
|
+
}
|
|
987
|
+
if (!successResults.includes(processQueryKey)) {
|
|
988
|
+
log?.warn?.(
|
|
989
|
+
`[DingTalk][AICard] Recall response missing successResult for processQueryKey=${processQueryKey} ` +
|
|
990
|
+
`payload=${JSON.stringify(response.data)}`,
|
|
991
|
+
);
|
|
992
|
+
return false;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
card.state = AICardStatus.FINISHED;
|
|
996
|
+
card.lastUpdated = Date.now();
|
|
997
|
+
removePendingCard(card, log);
|
|
998
|
+
log?.info?.(
|
|
999
|
+
`[DingTalk][AICard] Recalled empty card message: card=${card.cardInstanceId} ` +
|
|
1000
|
+
`conversationId=${target.conversationId} processQueryKey=${processQueryKey} mode=${target.isGroup ? "group" : "direct"}`,
|
|
1001
|
+
);
|
|
1002
|
+
return true;
|
|
1003
|
+
} catch (err: any) {
|
|
1004
|
+
log?.warn?.(`[DingTalk][AICard] Recall failed for card=${card.cardInstanceId}: ${err.message}`);
|
|
1005
|
+
if (err.response?.data !== undefined) {
|
|
1006
|
+
log?.warn?.(
|
|
1007
|
+
formatDingTalkErrorPayloadLog(
|
|
1008
|
+
target.isGroup ? "card.groupRecall" : "card.directRecall",
|
|
1009
|
+
err.response.data,
|
|
1010
|
+
"[DingTalk][AICard]",
|
|
1011
|
+
),
|
|
1012
|
+
);
|
|
1013
|
+
}
|
|
1014
|
+
return false;
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
|
|
907
1018
|
export async function finishStoppedAICard(
|
|
908
1019
|
card: AICardInstance,
|
|
909
1020
|
content: string,
|