@soimy/dingtalk 3.4.2 → 3.5.1
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 +86 -1421
- package/package.json +17 -4
- package/src/ack-reaction-service.ts +45 -27
- package/src/card/card-action-handler.ts +62 -0
- package/src/card/card-run-registry.ts +118 -0
- package/src/card/card-stop-handler.ts +94 -0
- package/src/card/card-template.ts +20 -0
- package/src/card-callback-service.ts +90 -8
- package/src/card-draft-controller.ts +270 -52
- package/src/card-service.ts +198 -138
- package/src/channel.ts +21 -7
- package/src/command/card-stop-command.ts +96 -0
- package/src/config-schema.ts +0 -18
- package/src/config.ts +28 -11
- package/src/feedback-learning-service.ts +4 -6
- package/src/inbound-handler.ts +193 -29
- package/src/message-context-store.ts +74 -0
- package/src/message-utils.ts +22 -0
- package/src/onboarding.ts +4 -77
- package/src/reply-strategy-card.ts +46 -35
- package/src/reply-strategy.ts +4 -3
- package/src/send-service.ts +42 -64
- package/src/targeting/agent-routing.ts +12 -6
- package/src/types.ts +10 -28
package/src/onboarding.ts
CHANGED
|
@@ -115,9 +115,6 @@ function applyAccountConfig(params: {
|
|
|
115
115
|
const payload: Partial<DingTalkConfig> = {
|
|
116
116
|
...(input.clientId ? { clientId: input.clientId } : {}),
|
|
117
117
|
...(input.clientSecret ? { clientSecret: input.clientSecret } : {}),
|
|
118
|
-
...(input.robotCode ? { robotCode: input.robotCode } : {}),
|
|
119
|
-
...(input.corpId ? { corpId: input.corpId } : {}),
|
|
120
|
-
...(input.agentId ? { agentId: input.agentId } : {}),
|
|
121
118
|
...(input.dmPolicy ? { dmPolicy: input.dmPolicy } : {}),
|
|
122
119
|
...(input.groupPolicy ? { groupPolicy: input.groupPolicy } : {}),
|
|
123
120
|
...(input.allowFrom && input.allowFrom.length > 0 ? { allowFrom: input.allowFrom } : {}),
|
|
@@ -129,8 +126,6 @@ function applyAccountConfig(params: {
|
|
|
129
126
|
? { mediaUrlAllowlist: input.mediaUrlAllowlist }
|
|
130
127
|
: {}),
|
|
131
128
|
...(input.messageType ? { messageType: input.messageType } : {}),
|
|
132
|
-
...(input.cardTemplateId ? { cardTemplateId: input.cardTemplateId } : {}),
|
|
133
|
-
...(input.cardTemplateKey ? { cardTemplateKey: input.cardTemplateKey } : {}),
|
|
134
129
|
...(typeof input.maxReconnectCycles === "number"
|
|
135
130
|
? { maxReconnectCycles: input.maxReconnectCycles }
|
|
136
131
|
: {}),
|
|
@@ -192,7 +187,6 @@ function applyGenericSetupInput(params: {
|
|
|
192
187
|
clientId: typeof params.input.token === "string" ? params.input.token.trim() : undefined,
|
|
193
188
|
clientSecret:
|
|
194
189
|
typeof params.input.password === "string" ? params.input.password.trim() : undefined,
|
|
195
|
-
robotCode: typeof params.input.code === "string" ? params.input.code.trim() : undefined,
|
|
196
190
|
},
|
|
197
191
|
});
|
|
198
192
|
}
|
|
@@ -233,84 +227,22 @@ async function configureDingTalkAccount(params: {
|
|
|
233
227
|
validate: (value: string) => (String(value ?? "").trim() ? undefined : "Required"),
|
|
234
228
|
});
|
|
235
229
|
|
|
236
|
-
const wantsFullConfig = await prompter.confirm({
|
|
237
|
-
message: "Configure robot code, corp ID, and agent ID? (recommended for full features)",
|
|
238
|
-
initialValue: false,
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
let robotCode: string | undefined;
|
|
242
|
-
let corpId: string | undefined;
|
|
243
|
-
let agentId: string | undefined;
|
|
244
|
-
|
|
245
|
-
if (wantsFullConfig) {
|
|
246
|
-
robotCode =
|
|
247
|
-
String(
|
|
248
|
-
await prompter.text({
|
|
249
|
-
message: "Robot Code",
|
|
250
|
-
placeholder: "dingxxxxxxxx",
|
|
251
|
-
initialValue: resolved.robotCode ?? undefined,
|
|
252
|
-
}),
|
|
253
|
-
).trim() || undefined;
|
|
254
|
-
|
|
255
|
-
corpId =
|
|
256
|
-
String(
|
|
257
|
-
await prompter.text({
|
|
258
|
-
message: "Corp ID",
|
|
259
|
-
placeholder: "dingxxxxxxxx",
|
|
260
|
-
initialValue: resolved.corpId ?? undefined,
|
|
261
|
-
}),
|
|
262
|
-
).trim() || undefined;
|
|
263
|
-
|
|
264
|
-
agentId =
|
|
265
|
-
String(
|
|
266
|
-
await prompter.text({
|
|
267
|
-
message: "Agent ID",
|
|
268
|
-
placeholder: "123456789",
|
|
269
|
-
initialValue: resolved.agentId ? String(resolved.agentId) : undefined,
|
|
270
|
-
}),
|
|
271
|
-
).trim() || undefined;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
230
|
const wantsCardMode = await prompter.confirm({
|
|
275
231
|
message: "Enable AI interactive card mode? (for streaming AI responses)",
|
|
276
232
|
initialValue: resolved.messageType === "card",
|
|
277
233
|
});
|
|
278
234
|
|
|
279
|
-
let cardTemplateId: string | undefined;
|
|
280
|
-
let cardTemplateKey: string | undefined;
|
|
281
235
|
let messageType: "markdown" | "card" = "markdown";
|
|
282
236
|
|
|
283
237
|
if (wantsCardMode) {
|
|
284
238
|
await prompter.note(
|
|
285
239
|
[
|
|
286
|
-
"
|
|
287
|
-
"
|
|
288
|
-
"
|
|
289
|
-
"2. Select 'AI Card' scenario",
|
|
290
|
-
"3. Design your card and publish",
|
|
291
|
-
"4. Copy the Template ID (e.g., xxx.schema)",
|
|
240
|
+
"AI interactive card mode now uses the built-in DingTalk template contract.",
|
|
241
|
+
"No manual Template ID or content field configuration is required.",
|
|
242
|
+
"Legacy cardTemplateId/cardTemplateKey config is deprecated and ignored.",
|
|
292
243
|
].join("\n"),
|
|
293
|
-
"Card Template
|
|
244
|
+
"Built-in AI Card Template",
|
|
294
245
|
);
|
|
295
|
-
|
|
296
|
-
cardTemplateId =
|
|
297
|
-
String(
|
|
298
|
-
await prompter.text({
|
|
299
|
-
message: "Card Template ID",
|
|
300
|
-
placeholder: "xxxxx-xxxxx-xxxxx.schema",
|
|
301
|
-
initialValue: resolved.cardTemplateId ?? undefined,
|
|
302
|
-
}),
|
|
303
|
-
).trim() || undefined;
|
|
304
|
-
|
|
305
|
-
cardTemplateKey =
|
|
306
|
-
String(
|
|
307
|
-
await prompter.text({
|
|
308
|
-
message: "Card Template Key (content field name)",
|
|
309
|
-
placeholder: "content",
|
|
310
|
-
initialValue: resolved.cardTemplateKey ?? "content",
|
|
311
|
-
}),
|
|
312
|
-
).trim() || "content";
|
|
313
|
-
|
|
314
246
|
messageType = "card";
|
|
315
247
|
}
|
|
316
248
|
|
|
@@ -471,9 +403,6 @@ async function configureDingTalkAccount(params: {
|
|
|
471
403
|
input: {
|
|
472
404
|
clientId: String(clientId).trim(),
|
|
473
405
|
clientSecret: String(clientSecret).trim(),
|
|
474
|
-
robotCode,
|
|
475
|
-
corpId,
|
|
476
|
-
agentId,
|
|
477
406
|
dmPolicy: dmPolicyValue as "open" | "allowlist",
|
|
478
407
|
groupPolicy: groupPolicyValue as "open" | "allowlist" | "disabled",
|
|
479
408
|
allowFrom,
|
|
@@ -481,8 +410,6 @@ async function configureDingTalkAccount(params: {
|
|
|
481
410
|
displayNameResolution: displayNameResolutionValue as "disabled" | "all",
|
|
482
411
|
mediaUrlAllowlist,
|
|
483
412
|
messageType,
|
|
484
|
-
cardTemplateId,
|
|
485
|
-
cardTemplateKey,
|
|
486
413
|
maxReconnectCycles,
|
|
487
414
|
mediaMaxMb,
|
|
488
415
|
journalTTLDays,
|
|
@@ -8,23 +8,37 @@
|
|
|
8
8
|
|
|
9
9
|
import {
|
|
10
10
|
finishAICard,
|
|
11
|
-
formatContentForCard,
|
|
12
11
|
isCardInTerminalState,
|
|
13
12
|
} from "./card-service";
|
|
14
13
|
import { createCardDraftController } from "./card-draft-controller";
|
|
14
|
+
import { attachCardRunController } from "./card/card-run-registry";
|
|
15
15
|
import type { DeliverPayload, ReplyOptions, ReplyStrategy, ReplyStrategyContext } from "./reply-strategy";
|
|
16
16
|
import { sendBySession, sendMessage } from "./send-service";
|
|
17
17
|
import type { AICardInstance } from "./types";
|
|
18
18
|
import { AICardStatus } from "./types";
|
|
19
19
|
import { formatDingTalkErrorPayloadLog } from "./utils";
|
|
20
20
|
|
|
21
|
+
const FILE_ONLY_FALLBACK_ANSWER = "附件已发送,请查收。";
|
|
22
|
+
|
|
21
23
|
export function createCardReplyStrategy(
|
|
22
|
-
ctx: ReplyStrategyContext & { card: AICardInstance },
|
|
24
|
+
ctx: ReplyStrategyContext & { card: AICardInstance; isStopRequested?: () => boolean },
|
|
23
25
|
): ReplyStrategy {
|
|
24
|
-
const { card, config, log } = ctx;
|
|
26
|
+
const { card, config, log, isStopRequested } = ctx;
|
|
25
27
|
|
|
26
28
|
const controller = createCardDraftController({ card, log });
|
|
29
|
+
if (card.outTrackId) {
|
|
30
|
+
attachCardRunController(card.outTrackId, controller);
|
|
31
|
+
}
|
|
27
32
|
let finalTextForFallback: string | undefined;
|
|
33
|
+
let sawFinalDelivery = false;
|
|
34
|
+
|
|
35
|
+
const getRenderedTimeline = (options: { preferFinalAnswer?: boolean } = {}): string => {
|
|
36
|
+
const fallbackAnswer = finalTextForFallback || (sawFinalDelivery ? FILE_ONLY_FALLBACK_ANSWER : undefined);
|
|
37
|
+
return controller.getRenderedContent({
|
|
38
|
+
fallbackAnswer,
|
|
39
|
+
overrideAnswer: options.preferFinalAnswer ? finalTextForFallback : undefined,
|
|
40
|
+
});
|
|
41
|
+
};
|
|
28
42
|
|
|
29
43
|
return {
|
|
30
44
|
getReplyOptions(): ReplyOptions {
|
|
@@ -33,21 +47,24 @@ export function createCardReplyStrategy(
|
|
|
33
47
|
// onPartialReply (real-time) or deliver(final) -> finishAICard.
|
|
34
48
|
disableBlockStreaming: true,
|
|
35
49
|
|
|
36
|
-
onAssistantMessageStart: () => {
|
|
37
|
-
|
|
50
|
+
onAssistantMessageStart: async () => {
|
|
51
|
+
if (isStopRequested?.()) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
await controller.notifyNewAssistantTurn();
|
|
38
55
|
},
|
|
39
56
|
|
|
40
57
|
onPartialReply: config.cardRealTimeStream
|
|
41
|
-
? (payload) => {
|
|
42
|
-
if (payload.text) {
|
|
43
|
-
controller.updateAnswer(payload.text);
|
|
58
|
+
? async (payload) => {
|
|
59
|
+
if (payload.text && !isStopRequested?.()) {
|
|
60
|
+
await controller.updateAnswer(payload.text);
|
|
44
61
|
}
|
|
45
62
|
}
|
|
46
63
|
: undefined,
|
|
47
64
|
|
|
48
|
-
onReasoningStream: (payload) => {
|
|
49
|
-
if (payload.text) {
|
|
50
|
-
controller.
|
|
65
|
+
onReasoningStream: async (payload) => {
|
|
66
|
+
if (payload.text && !isStopRequested?.()) {
|
|
67
|
+
await controller.updateThinking(payload.text);
|
|
51
68
|
}
|
|
52
69
|
},
|
|
53
70
|
};
|
|
@@ -65,6 +82,7 @@ export function createCardReplyStrategy(
|
|
|
65
82
|
|
|
66
83
|
// ---- final: defer to finalize, just save text ----
|
|
67
84
|
if (payload.kind === "final") {
|
|
85
|
+
sawFinalDelivery = true;
|
|
68
86
|
log?.info?.(
|
|
69
87
|
`[DingTalk][Finalize] deliver(final) received — cardState=${card.state} ` +
|
|
70
88
|
`textLen=${typeof textToSend === "string" ? textToSend.length : "null"} ` +
|
|
@@ -88,27 +106,10 @@ export function createCardReplyStrategy(
|
|
|
88
106
|
log?.debug?.("[DingTalk] Card failed, skipping tool result (will send full reply on final)");
|
|
89
107
|
return;
|
|
90
108
|
}
|
|
91
|
-
await controller.flush();
|
|
92
|
-
await controller.waitForInFlight();
|
|
93
109
|
log?.info?.(
|
|
94
110
|
`[DingTalk] Tool result received, streaming to AI Card: ${(textToSend ?? "").slice(0, 100)}`,
|
|
95
111
|
);
|
|
96
|
-
|
|
97
|
-
if (toolText) {
|
|
98
|
-
const sendResult = await sendMessage(ctx.config, ctx.to, toolText, {
|
|
99
|
-
sessionWebhook: ctx.sessionWebhook,
|
|
100
|
-
atUserId: !ctx.isDirect ? ctx.senderId : null,
|
|
101
|
-
log,
|
|
102
|
-
card,
|
|
103
|
-
accountId: ctx.accountId,
|
|
104
|
-
storePath: ctx.storePath,
|
|
105
|
-
conversationId: ctx.groupId,
|
|
106
|
-
cardUpdateMode: "append",
|
|
107
|
-
});
|
|
108
|
-
if (!sendResult.ok) {
|
|
109
|
-
throw new Error(sendResult.error || "Tool stream send failed");
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
+
await controller.appendTool(textToSend ?? "");
|
|
112
113
|
return;
|
|
113
114
|
}
|
|
114
115
|
|
|
@@ -128,14 +129,24 @@ export function createCardReplyStrategy(
|
|
|
128
129
|
`lastContent="${(controller.getLastContent() ?? "").slice(0, 80)}"`,
|
|
129
130
|
);
|
|
130
131
|
|
|
132
|
+
if (isStopRequested?.()) {
|
|
133
|
+
log?.info?.("[DingTalk][Finalize] Skipping — card stop was requested");
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
131
137
|
if (card.state === AICardStatus.FINISHED) {
|
|
132
138
|
log?.info?.("[DingTalk][Finalize] Skipping — card already FINISHED");
|
|
133
139
|
return;
|
|
134
140
|
}
|
|
135
141
|
|
|
142
|
+
if (card.state === AICardStatus.STOPPED) {
|
|
143
|
+
log?.info?.("[DingTalk][Finalize] Skipping — card already STOPPED");
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
136
147
|
// Card failed -> markdown fallback (bypass sendMessage to avoid duplicate card).
|
|
137
148
|
if (card.state === AICardStatus.FAILED || controller.isFailed()) {
|
|
138
|
-
const fallbackText =
|
|
149
|
+
const fallbackText = getRenderedTimeline({ preferFinalAnswer: true })
|
|
139
150
|
|| controller.getLastAnswerContent()
|
|
140
151
|
|| controller.getLastContent()
|
|
141
152
|
|| card.lastStreamedContent;
|
|
@@ -164,13 +175,11 @@ export function createCardReplyStrategy(
|
|
|
164
175
|
try {
|
|
165
176
|
await controller.flush();
|
|
166
177
|
await controller.waitForInFlight();
|
|
178
|
+
const finalText = getRenderedTimeline() || "✅ Done";
|
|
167
179
|
controller.stop();
|
|
168
|
-
const finalText = controller.getLastAnswerContent()
|
|
169
|
-
|| finalTextForFallback
|
|
170
|
-
|| "✅ Done";
|
|
171
180
|
log?.info?.(
|
|
172
181
|
`[DingTalk][Finalize] Calling finishAICard — finalTextLen=${finalText.length} ` +
|
|
173
|
-
`source=${controller.
|
|
182
|
+
`source=${controller.getFinalAnswerContent() ? "timeline.answer" : sawFinalDelivery ? "timeline.fileOnly" : "fallbackDone"} ` +
|
|
174
183
|
`preview="${finalText.slice(0, 120)}"`,
|
|
175
184
|
);
|
|
176
185
|
await finishAICard(card, finalText, log, {
|
|
@@ -219,7 +228,9 @@ export function createCardReplyStrategy(
|
|
|
219
228
|
},
|
|
220
229
|
|
|
221
230
|
getFinalText(): string | undefined {
|
|
222
|
-
return controller.
|
|
231
|
+
return controller.getFinalAnswerContent()
|
|
232
|
+
|| finalTextForFallback
|
|
233
|
+
|| (sawFinalDelivery ? FILE_ONLY_FALLBACK_ANSWER : undefined);
|
|
223
234
|
},
|
|
224
235
|
};
|
|
225
236
|
}
|
package/src/reply-strategy.ts
CHANGED
|
@@ -20,9 +20,9 @@ export interface DeliverPayload {
|
|
|
20
20
|
|
|
21
21
|
export interface ReplyOptions {
|
|
22
22
|
disableBlockStreaming: boolean;
|
|
23
|
-
onPartialReply?: (payload: { text?: string }) => void
|
|
24
|
-
onReasoningStream?: (payload: { text?: string }) => void
|
|
25
|
-
onAssistantMessageStart?: () => void
|
|
23
|
+
onPartialReply?: (payload: { text?: string }) => void | Promise<void>;
|
|
24
|
+
onReasoningStream?: (payload: { text?: string }) => void | Promise<void>;
|
|
25
|
+
onAssistantMessageStart?: () => void | Promise<void>;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
export interface ReplyStrategy {
|
|
@@ -55,6 +55,7 @@ export interface ReplyStrategyContext {
|
|
|
55
55
|
log?: Logger;
|
|
56
56
|
replyQuotedRef?: QuotedRef;
|
|
57
57
|
deliverMedia: (urls: string[]) => Promise<void>;
|
|
58
|
+
isStopRequested?: () => boolean;
|
|
58
59
|
}
|
|
59
60
|
|
|
60
61
|
// ---- Factory -----------------------------------------------------
|
package/src/send-service.ts
CHANGED
|
@@ -4,13 +4,17 @@ import { getAccessToken } from "./auth";
|
|
|
4
4
|
import {
|
|
5
5
|
isCardInTerminalState,
|
|
6
6
|
sendProactiveCardText,
|
|
7
|
-
streamAICard,
|
|
8
7
|
} from "./card-service";
|
|
9
|
-
import { stripTargetPrefix } from "./config";
|
|
8
|
+
import { resolveRobotCode, stripTargetPrefix } from "./config";
|
|
10
9
|
import { getLogger } from "./logger-context";
|
|
11
|
-
import { getVoiceDurationMs, uploadMedia as uploadMediaUtil
|
|
10
|
+
import { getVoiceDurationMs, uploadMedia as uploadMediaUtil } from "./media-utils";
|
|
12
11
|
import { convertMarkdownTablesToPlainText, detectMarkdownAndExtractTitle } from "./message-utils";
|
|
13
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
DEFAULT_MESSAGE_CONTEXT_TTL_DAYS,
|
|
14
|
+
DEFAULT_OUTBOUND_SENDER,
|
|
15
|
+
inferConversationChatType,
|
|
16
|
+
upsertOutboundMessageContext,
|
|
17
|
+
} from "./message-context-store";
|
|
14
18
|
import { resolveOriginalPeerId } from "./peer-id-registry";
|
|
15
19
|
import {
|
|
16
20
|
deleteProactiveRiskObservation,
|
|
@@ -18,6 +22,7 @@ import {
|
|
|
18
22
|
recordProactiveRiskObservation,
|
|
19
23
|
} from "./proactive-risk-registry";
|
|
20
24
|
import { formatDingTalkErrorPayloadLog, getProxyBypassOption } from "./utils";
|
|
25
|
+
import type { UploadMediaResult } from "./media-utils";
|
|
21
26
|
import type {
|
|
22
27
|
AICardInstance,
|
|
23
28
|
AxiosResponse,
|
|
@@ -29,7 +34,6 @@ import type {
|
|
|
29
34
|
SendMessageOptions,
|
|
30
35
|
SessionWebhookResponse,
|
|
31
36
|
} from "./types";
|
|
32
|
-
import { AICardStatus } from "./types";
|
|
33
37
|
|
|
34
38
|
export { detectMediaTypeFromExtension } from "./media-utils";
|
|
35
39
|
|
|
@@ -78,6 +82,9 @@ function persistOutboundMessageContext(params: {
|
|
|
78
82
|
createdAt?: number;
|
|
79
83
|
quotedRef?: QuotedRef;
|
|
80
84
|
log?: Logger;
|
|
85
|
+
senderId?: string;
|
|
86
|
+
senderName?: string;
|
|
87
|
+
chatType?: "direct" | "group";
|
|
81
88
|
delivery: {
|
|
82
89
|
messageId?: string;
|
|
83
90
|
processQueryKey?: string;
|
|
@@ -101,6 +108,9 @@ function persistOutboundMessageContext(params: {
|
|
|
101
108
|
createdAt: params.createdAt ?? Date.now(),
|
|
102
109
|
text: params.text,
|
|
103
110
|
messageType: params.messageType,
|
|
111
|
+
senderId: params.senderId,
|
|
112
|
+
senderName: params.senderName,
|
|
113
|
+
chatType: params.chatType,
|
|
104
114
|
ttlMs: DEFAULT_MESSAGE_CONTEXT_TTL_DAYS * 24 * 60 * 60 * 1000,
|
|
105
115
|
topic: null,
|
|
106
116
|
quotedRef: params.quotedRef,
|
|
@@ -118,26 +128,6 @@ function buildPersistedOutboundText(text: string, options: SendMessageOptions):
|
|
|
118
128
|
return text;
|
|
119
129
|
}
|
|
120
130
|
|
|
121
|
-
function composeCardContentForAppend(previous: string | undefined, incoming: string): string {
|
|
122
|
-
const prev = previous ?? "";
|
|
123
|
-
if (!prev) {
|
|
124
|
-
return incoming;
|
|
125
|
-
}
|
|
126
|
-
if (!incoming) {
|
|
127
|
-
return prev;
|
|
128
|
-
}
|
|
129
|
-
if (incoming.startsWith(prev)) {
|
|
130
|
-
return incoming;
|
|
131
|
-
}
|
|
132
|
-
if (prev.endsWith(incoming)) {
|
|
133
|
-
return prev;
|
|
134
|
-
}
|
|
135
|
-
if (prev.endsWith("\n") || incoming.startsWith("\n")) {
|
|
136
|
-
return `${prev}${incoming}`;
|
|
137
|
-
}
|
|
138
|
-
return `${prev}${incoming}`;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
131
|
const DINGTALK_TEXT_CHUNK_LIMIT = 3800;
|
|
142
132
|
|
|
143
133
|
function splitMarkdownChunks(text: string, limit = DINGTALK_TEXT_CHUNK_LIMIT): string[] {
|
|
@@ -203,7 +193,6 @@ function isProactivePermissionOrScopeError(code: string | null): boolean {
|
|
|
203
193
|
|
|
204
194
|
/**
|
|
205
195
|
* Wrapper to upload media with shared getAccessToken binding.
|
|
206
|
-
* Supports sandbox/container paths via mediaLocalRoots option.
|
|
207
196
|
*/
|
|
208
197
|
export async function uploadMedia(
|
|
209
198
|
config: DingTalkConfig,
|
|
@@ -236,7 +225,7 @@ export async function sendProactiveTextOrMarkdown(
|
|
|
236
225
|
|
|
237
226
|
// In card mode, use card API to avoid oToMessages/batchSend permission requirement.
|
|
238
227
|
const messageType = config.messageType || "markdown";
|
|
239
|
-
if (messageType === "card" &&
|
|
228
|
+
if (messageType === "card" && !options.forceMarkdown) {
|
|
240
229
|
log?.debug?.(
|
|
241
230
|
`[DingTalk] Using card API for proactive message to user ${resolvedTarget}${proactiveRiskTag}`,
|
|
242
231
|
);
|
|
@@ -277,7 +266,7 @@ export async function sendProactiveTextOrMarkdown(
|
|
|
277
266
|
: JSON.stringify({ content: normalizedText });
|
|
278
267
|
|
|
279
268
|
const payload: ProactiveMessagePayload = {
|
|
280
|
-
robotCode: config
|
|
269
|
+
robotCode: resolveRobotCode(config),
|
|
281
270
|
msgKey,
|
|
282
271
|
msgParam,
|
|
283
272
|
};
|
|
@@ -343,7 +332,7 @@ export async function sendProactiveMedia(
|
|
|
343
332
|
target: string,
|
|
344
333
|
mediaPath: string,
|
|
345
334
|
mediaType: "image" | "voice" | "video" | "file",
|
|
346
|
-
options: SendMessageOptions & { accountId?: string
|
|
335
|
+
options: SendMessageOptions & { accountId?: string } = {},
|
|
347
336
|
): Promise<{ ok: boolean; error?: string; data?: any; messageId?: string }> {
|
|
348
337
|
const log = options.log || getLogger();
|
|
349
338
|
|
|
@@ -355,7 +344,7 @@ export async function sendProactiveMedia(
|
|
|
355
344
|
if (!uploadResult) {
|
|
356
345
|
return { ok: false, error: "Failed to upload media" };
|
|
357
346
|
}
|
|
358
|
-
const { mediaId, buffer
|
|
347
|
+
const { mediaId, buffer } = uploadResult;
|
|
359
348
|
|
|
360
349
|
const token = await getAccessToken(config, log);
|
|
361
350
|
const { targetId, isExplicitUser } = stripTargetPrefix(target);
|
|
@@ -376,10 +365,7 @@ export async function sendProactiveMedia(
|
|
|
376
365
|
msgParam = JSON.stringify({ photoURL: mediaId });
|
|
377
366
|
} else if (mediaType === "voice") {
|
|
378
367
|
msgKey = "sampleAudio";
|
|
379
|
-
|
|
380
|
-
const durationMs = await getVoiceDurationMs(mediaPath, mediaType, log, {
|
|
381
|
-
preReadBuffer: uploadedBuffer,
|
|
382
|
-
});
|
|
368
|
+
const durationMs = await getVoiceDurationMs(mediaPath, mediaType, log, { preReadBuffer: buffer });
|
|
383
369
|
msgParam = JSON.stringify({ mediaId, duration: String(durationMs) });
|
|
384
370
|
} else {
|
|
385
371
|
// sampleVideo requires picMediaId; fallback to sampleFile for broader compatibility.
|
|
@@ -391,7 +377,7 @@ export async function sendProactiveMedia(
|
|
|
391
377
|
}
|
|
392
378
|
|
|
393
379
|
const payload: ProactiveMessagePayload = {
|
|
394
|
-
robotCode: config
|
|
380
|
+
robotCode: resolveRobotCode(config),
|
|
395
381
|
msgKey,
|
|
396
382
|
msgParam,
|
|
397
383
|
};
|
|
@@ -427,6 +413,8 @@ export async function sendProactiveMedia(
|
|
|
427
413
|
messageType: "outbound-proactive-media",
|
|
428
414
|
quotedRef: options.quotedRef,
|
|
429
415
|
log,
|
|
416
|
+
...DEFAULT_OUTBOUND_SENDER,
|
|
417
|
+
chatType: inferConversationChatType(options.conversationId || resolvedTarget),
|
|
430
418
|
delivery: {
|
|
431
419
|
...delivery,
|
|
432
420
|
kind: "proactive-media",
|
|
@@ -485,6 +473,8 @@ export async function sendProactiveMedia(
|
|
|
485
473
|
messageType: "outbound-proactive-fallback",
|
|
486
474
|
quotedRef: options.quotedRef,
|
|
487
475
|
log,
|
|
476
|
+
...DEFAULT_OUTBOUND_SENDER,
|
|
477
|
+
chatType: inferConversationChatType(options.conversationId || normalizedTarget),
|
|
488
478
|
delivery: {
|
|
489
479
|
...fallbackDelivery,
|
|
490
480
|
kind: isTrackingResult(fallback as ProactiveTextSendResult) ? "proactive-card" : "proactive-text",
|
|
@@ -509,16 +499,13 @@ export async function sendBySession(
|
|
|
509
499
|
mediaLocalRoots: options.mediaLocalRoots,
|
|
510
500
|
});
|
|
511
501
|
if (uploadResult) {
|
|
512
|
-
const { mediaId, buffer
|
|
502
|
+
const { mediaId, buffer } = uploadResult;
|
|
513
503
|
let body: any;
|
|
514
504
|
|
|
515
505
|
if (options.mediaType === "image") {
|
|
516
506
|
body = { msgtype: "image", image: { media_id: mediaId } };
|
|
517
507
|
} else if (options.mediaType === "voice") {
|
|
518
|
-
|
|
519
|
-
const durationMs = await getVoiceDurationMs(options.mediaPath, options.mediaType, log, {
|
|
520
|
-
preReadBuffer: uploadedBuffer,
|
|
521
|
-
});
|
|
508
|
+
const durationMs = await getVoiceDurationMs(options.mediaPath, options.mediaType, log, { preReadBuffer: buffer });
|
|
522
509
|
body = { msgtype: "voice", voice: { media_id: mediaId, duration: String(durationMs) } };
|
|
523
510
|
} else if (options.mediaType === "video") {
|
|
524
511
|
body = { msgtype: "video", video: { media_id: mediaId } };
|
|
@@ -595,31 +582,18 @@ export async function sendMessage(
|
|
|
595
582
|
return { ok: true };
|
|
596
583
|
}
|
|
597
584
|
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
return { ok: false, error: proactiveResult.error || "Card send failed" };
|
|
602
|
-
}
|
|
603
|
-
return {
|
|
604
|
-
ok: true,
|
|
605
|
-
tracking: {
|
|
606
|
-
processQueryKey: proactiveResult.processQueryKey,
|
|
607
|
-
outTrackId: proactiveResult.outTrackId,
|
|
608
|
-
cardInstanceId: proactiveResult.cardInstanceId,
|
|
609
|
-
},
|
|
610
|
-
};
|
|
611
|
-
}
|
|
612
|
-
} else if (options.cardUpdateMode === "append") {
|
|
613
|
-
try {
|
|
614
|
-
const nextContent = composeCardContentForAppend(card.lastStreamedContent, text);
|
|
615
|
-
await streamAICard(card, nextContent, false, log);
|
|
616
|
-
return { ok: true };
|
|
617
|
-
} catch (err: any) {
|
|
618
|
-
log?.warn?.(`[DingTalk] AI Card streaming failed: ${err.message}`);
|
|
619
|
-
card.state = AICardStatus.FAILED;
|
|
620
|
-
card.lastUpdated = Date.now();
|
|
621
|
-
return { ok: false, error: err.message };
|
|
585
|
+
const proactiveResult = await sendProactiveCardText(config, conversationId, text, log);
|
|
586
|
+
if (!proactiveResult.ok) {
|
|
587
|
+
return { ok: false, error: proactiveResult.error || "Card send failed" };
|
|
622
588
|
}
|
|
589
|
+
return {
|
|
590
|
+
ok: true,
|
|
591
|
+
tracking: {
|
|
592
|
+
processQueryKey: proactiveResult.processQueryKey,
|
|
593
|
+
outTrackId: proactiveResult.outTrackId,
|
|
594
|
+
cardInstanceId: proactiveResult.cardInstanceId,
|
|
595
|
+
},
|
|
596
|
+
};
|
|
623
597
|
}
|
|
624
598
|
}
|
|
625
599
|
|
|
@@ -636,6 +610,8 @@ export async function sendMessage(
|
|
|
636
610
|
messageType: options.mediaPath && options.mediaType ? "outbound-media" : "outbound",
|
|
637
611
|
quotedRef: options.quotedRef,
|
|
638
612
|
log,
|
|
613
|
+
...DEFAULT_OUTBOUND_SENDER,
|
|
614
|
+
chatType: inferConversationChatType(options.conversationId || conversationId),
|
|
639
615
|
delivery: {
|
|
640
616
|
...delivery,
|
|
641
617
|
kind: "session",
|
|
@@ -655,6 +631,8 @@ export async function sendMessage(
|
|
|
655
631
|
messageType: "outbound-proactive",
|
|
656
632
|
quotedRef: options.quotedRef,
|
|
657
633
|
log,
|
|
634
|
+
...DEFAULT_OUTBOUND_SENDER,
|
|
635
|
+
chatType: inferConversationChatType(options.conversationId || conversationId),
|
|
658
636
|
delivery: {
|
|
659
637
|
...delivery,
|
|
660
638
|
kind: isTrackingResult(result) ? "proactive-card" : "proactive-text",
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
|
|
11
11
|
import { resolveAtAgents } from "./agent-name-matcher";
|
|
12
|
+
import { resolveRobotCode } from "../config";
|
|
12
13
|
import { parseLearnCommand } from "../learning-command-service";
|
|
13
14
|
import { getDingTalkRuntime } from "../runtime";
|
|
14
15
|
import { sendBySession } from "../send-service";
|
|
@@ -65,7 +66,12 @@ function sanitizeAgentName(name: string): string {
|
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
/**
|
|
68
|
-
* Resolve @mention-based sub-agent routing for a group message.
|
|
69
|
+
* Resolve @mention-based sub-agent routing for a group or direct message.
|
|
70
|
+
*
|
|
71
|
+
* In group chats, @mentions are populated by the DingTalk SDK (atMentions field).
|
|
72
|
+
* In direct messages (DM), the SDK also populates atMentions for text-type messages
|
|
73
|
+
* via extractMessageContent in message-utils.ts, so the same field is reused here.
|
|
74
|
+
* The !isGroup guard is removed to enable sub-agent routing in DM as well.
|
|
69
75
|
*
|
|
70
76
|
* Returns matched agents if any @mentions resolve to configured agents,
|
|
71
77
|
* or null if the message should be handled by the default agent.
|
|
@@ -85,14 +91,14 @@ export async function resolveSubAgentRoute(params: {
|
|
|
85
91
|
const { extractedContent, cfg, isGroup, dingtalkConfig, sessionWebhook, senderId, log } = params;
|
|
86
92
|
|
|
87
93
|
const atMentions = extractedContent.atMentions || [];
|
|
88
|
-
|
|
94
|
+
// DM has no @picker list from DingTalk; only group chats provide atUsers for real-user hints.
|
|
95
|
+
const atUserDingtalkIds = isGroup ? extractedContent.atUserDingtalkIds : undefined;
|
|
89
96
|
// Strip quoted prefix before checking /learn to avoid false positives
|
|
90
97
|
// when the quoted message itself contains a /learn command.
|
|
91
98
|
const textForCommandCheck = extractedContent.text.replace(/^\[引用[^\]]*\]\s*/, "");
|
|
92
99
|
const isLearnCommand = parseLearnCommand(textForCommandCheck).scope !== "unknown";
|
|
93
100
|
|
|
94
101
|
if (
|
|
95
|
-
!isGroup ||
|
|
96
102
|
atMentions.length === 0 ||
|
|
97
103
|
!cfg.agents?.list ||
|
|
98
104
|
cfg.agents.list.length === 0 ||
|
|
@@ -114,9 +120,9 @@ export async function resolveSubAgentRoute(params: {
|
|
|
114
120
|
if (hasInvalidAgentNames) {
|
|
115
121
|
const fallbackReason = `未找到名为"${unmatchedNames.join("、")}"的助手`;
|
|
116
122
|
try {
|
|
123
|
+
const sendOptions = isGroup ? { atUserId: senderId, log } : { log };
|
|
117
124
|
await sendBySession(dingtalkConfig, sessionWebhook, `⚠️ ${fallbackReason}`, {
|
|
118
|
-
|
|
119
|
-
log,
|
|
125
|
+
...sendOptions,
|
|
120
126
|
});
|
|
121
127
|
} catch (err: any) {
|
|
122
128
|
log?.debug?.(`[DingTalk] Failed to send fallback notice: ${err.message}`);
|
|
@@ -149,7 +155,7 @@ export async function dispatchSubAgents(params: {
|
|
|
149
155
|
|
|
150
156
|
// Pre-download media once to avoid duplication across sub-agents
|
|
151
157
|
let preDownloadedMedia: { mediaPath?: string; mediaType?: string } | undefined;
|
|
152
|
-
if (extractedContent.mediaPath && dingtalkConfig
|
|
158
|
+
if (extractedContent.mediaPath && resolveRobotCode(dingtalkConfig)) {
|
|
153
159
|
const media = await download(dingtalkConfig, extractedContent.mediaPath, log);
|
|
154
160
|
if (media) {
|
|
155
161
|
preDownloadedMedia = { mediaPath: media.path, mediaType: media.mimeType };
|