@soimy/dingtalk 3.4.2 → 3.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +86 -1421
- package/package.json +11 -4
- package/src/ack-reaction-service.ts +45 -27
- package/src/card-draft-controller.ts +270 -52
- package/src/card-service.ts +10 -11
- package/src/channel.ts +8 -7
- 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 +152 -28
- package/src/message-context-store.ts +74 -0
- package/src/message-utils.ts +22 -0
- package/src/onboarding.ts +0 -45
- package/src/reply-strategy-card.ts +25 -31
- package/src/reply-strategy.ts +3 -3
- package/src/send-service.ts +30 -50
- package/src/targeting/agent-routing.ts +12 -6
- package/src/types.ts +4 -27
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 } : {}),
|
|
@@ -192,7 +189,6 @@ function applyGenericSetupInput(params: {
|
|
|
192
189
|
clientId: typeof params.input.token === "string" ? params.input.token.trim() : undefined,
|
|
193
190
|
clientSecret:
|
|
194
191
|
typeof params.input.password === "string" ? params.input.password.trim() : undefined,
|
|
195
|
-
robotCode: typeof params.input.code === "string" ? params.input.code.trim() : undefined,
|
|
196
192
|
},
|
|
197
193
|
});
|
|
198
194
|
}
|
|
@@ -233,44 +229,6 @@ async function configureDingTalkAccount(params: {
|
|
|
233
229
|
validate: (value: string) => (String(value ?? "").trim() ? undefined : "Required"),
|
|
234
230
|
});
|
|
235
231
|
|
|
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
232
|
const wantsCardMode = await prompter.confirm({
|
|
275
233
|
message: "Enable AI interactive card mode? (for streaming AI responses)",
|
|
276
234
|
initialValue: resolved.messageType === "card",
|
|
@@ -471,9 +429,6 @@ async function configureDingTalkAccount(params: {
|
|
|
471
429
|
input: {
|
|
472
430
|
clientId: String(clientId).trim(),
|
|
473
431
|
clientSecret: String(clientSecret).trim(),
|
|
474
|
-
robotCode,
|
|
475
|
-
corpId,
|
|
476
|
-
agentId,
|
|
477
432
|
dmPolicy: dmPolicyValue as "open" | "allowlist",
|
|
478
433
|
groupPolicy: groupPolicyValue as "open" | "allowlist" | "disabled",
|
|
479
434
|
allowFrom,
|
|
@@ -8,7 +8,6 @@
|
|
|
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";
|
|
@@ -18,6 +17,8 @@ import type { AICardInstance } from "./types";
|
|
|
18
17
|
import { AICardStatus } from "./types";
|
|
19
18
|
import { formatDingTalkErrorPayloadLog } from "./utils";
|
|
20
19
|
|
|
20
|
+
const FILE_ONLY_FALLBACK_ANSWER = "附件已发送,请查收。";
|
|
21
|
+
|
|
21
22
|
export function createCardReplyStrategy(
|
|
22
23
|
ctx: ReplyStrategyContext & { card: AICardInstance },
|
|
23
24
|
): ReplyStrategy {
|
|
@@ -25,6 +26,15 @@ export function createCardReplyStrategy(
|
|
|
25
26
|
|
|
26
27
|
const controller = createCardDraftController({ card, log });
|
|
27
28
|
let finalTextForFallback: string | undefined;
|
|
29
|
+
let sawFinalDelivery = false;
|
|
30
|
+
|
|
31
|
+
const getRenderedTimeline = (options: { preferFinalAnswer?: boolean } = {}): string => {
|
|
32
|
+
const fallbackAnswer = finalTextForFallback || (sawFinalDelivery ? FILE_ONLY_FALLBACK_ANSWER : undefined);
|
|
33
|
+
return controller.getRenderedContent({
|
|
34
|
+
fallbackAnswer,
|
|
35
|
+
overrideAnswer: options.preferFinalAnswer ? finalTextForFallback : undefined,
|
|
36
|
+
});
|
|
37
|
+
};
|
|
28
38
|
|
|
29
39
|
return {
|
|
30
40
|
getReplyOptions(): ReplyOptions {
|
|
@@ -33,21 +43,21 @@ export function createCardReplyStrategy(
|
|
|
33
43
|
// onPartialReply (real-time) or deliver(final) -> finishAICard.
|
|
34
44
|
disableBlockStreaming: true,
|
|
35
45
|
|
|
36
|
-
onAssistantMessageStart: () => {
|
|
37
|
-
controller.notifyNewAssistantTurn();
|
|
46
|
+
onAssistantMessageStart: async () => {
|
|
47
|
+
await controller.notifyNewAssistantTurn();
|
|
38
48
|
},
|
|
39
49
|
|
|
40
50
|
onPartialReply: config.cardRealTimeStream
|
|
41
|
-
? (payload) => {
|
|
51
|
+
? async (payload) => {
|
|
42
52
|
if (payload.text) {
|
|
43
|
-
controller.updateAnswer(payload.text);
|
|
53
|
+
await controller.updateAnswer(payload.text);
|
|
44
54
|
}
|
|
45
55
|
}
|
|
46
56
|
: undefined,
|
|
47
57
|
|
|
48
|
-
onReasoningStream: (payload) => {
|
|
58
|
+
onReasoningStream: async (payload) => {
|
|
49
59
|
if (payload.text) {
|
|
50
|
-
controller.
|
|
60
|
+
await controller.updateThinking(payload.text);
|
|
51
61
|
}
|
|
52
62
|
},
|
|
53
63
|
};
|
|
@@ -65,6 +75,7 @@ export function createCardReplyStrategy(
|
|
|
65
75
|
|
|
66
76
|
// ---- final: defer to finalize, just save text ----
|
|
67
77
|
if (payload.kind === "final") {
|
|
78
|
+
sawFinalDelivery = true;
|
|
68
79
|
log?.info?.(
|
|
69
80
|
`[DingTalk][Finalize] deliver(final) received — cardState=${card.state} ` +
|
|
70
81
|
`textLen=${typeof textToSend === "string" ? textToSend.length : "null"} ` +
|
|
@@ -88,27 +99,10 @@ export function createCardReplyStrategy(
|
|
|
88
99
|
log?.debug?.("[DingTalk] Card failed, skipping tool result (will send full reply on final)");
|
|
89
100
|
return;
|
|
90
101
|
}
|
|
91
|
-
await controller.flush();
|
|
92
|
-
await controller.waitForInFlight();
|
|
93
102
|
log?.info?.(
|
|
94
103
|
`[DingTalk] Tool result received, streaming to AI Card: ${(textToSend ?? "").slice(0, 100)}`,
|
|
95
104
|
);
|
|
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
|
-
}
|
|
105
|
+
await controller.appendTool(textToSend ?? "");
|
|
112
106
|
return;
|
|
113
107
|
}
|
|
114
108
|
|
|
@@ -135,7 +129,7 @@ export function createCardReplyStrategy(
|
|
|
135
129
|
|
|
136
130
|
// Card failed -> markdown fallback (bypass sendMessage to avoid duplicate card).
|
|
137
131
|
if (card.state === AICardStatus.FAILED || controller.isFailed()) {
|
|
138
|
-
const fallbackText =
|
|
132
|
+
const fallbackText = getRenderedTimeline({ preferFinalAnswer: true })
|
|
139
133
|
|| controller.getLastAnswerContent()
|
|
140
134
|
|| controller.getLastContent()
|
|
141
135
|
|| card.lastStreamedContent;
|
|
@@ -164,13 +158,11 @@ export function createCardReplyStrategy(
|
|
|
164
158
|
try {
|
|
165
159
|
await controller.flush();
|
|
166
160
|
await controller.waitForInFlight();
|
|
161
|
+
const finalText = getRenderedTimeline() || "✅ Done";
|
|
167
162
|
controller.stop();
|
|
168
|
-
const finalText = controller.getLastAnswerContent()
|
|
169
|
-
|| finalTextForFallback
|
|
170
|
-
|| "✅ Done";
|
|
171
163
|
log?.info?.(
|
|
172
164
|
`[DingTalk][Finalize] Calling finishAICard — finalTextLen=${finalText.length} ` +
|
|
173
|
-
`source=${controller.
|
|
165
|
+
`source=${controller.getFinalAnswerContent() ? "timeline.answer" : sawFinalDelivery ? "timeline.fileOnly" : "fallbackDone"} ` +
|
|
174
166
|
`preview="${finalText.slice(0, 120)}"`,
|
|
175
167
|
);
|
|
176
168
|
await finishAICard(card, finalText, log, {
|
|
@@ -219,7 +211,9 @@ export function createCardReplyStrategy(
|
|
|
219
211
|
},
|
|
220
212
|
|
|
221
213
|
getFinalText(): string | undefined {
|
|
222
|
-
return controller.
|
|
214
|
+
return controller.getFinalAnswerContent()
|
|
215
|
+
|| finalTextForFallback
|
|
216
|
+
|| (sawFinalDelivery ? FILE_ONLY_FALLBACK_ANSWER : undefined);
|
|
223
217
|
},
|
|
224
218
|
};
|
|
225
219
|
}
|
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 {
|
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,
|
|
@@ -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 } };
|
|
@@ -609,17 +596,6 @@ export async function sendMessage(
|
|
|
609
596
|
},
|
|
610
597
|
};
|
|
611
598
|
}
|
|
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 };
|
|
622
|
-
}
|
|
623
599
|
}
|
|
624
600
|
}
|
|
625
601
|
|
|
@@ -636,6 +612,8 @@ export async function sendMessage(
|
|
|
636
612
|
messageType: options.mediaPath && options.mediaType ? "outbound-media" : "outbound",
|
|
637
613
|
quotedRef: options.quotedRef,
|
|
638
614
|
log,
|
|
615
|
+
...DEFAULT_OUTBOUND_SENDER,
|
|
616
|
+
chatType: inferConversationChatType(options.conversationId || conversationId),
|
|
639
617
|
delivery: {
|
|
640
618
|
...delivery,
|
|
641
619
|
kind: "session",
|
|
@@ -655,6 +633,8 @@ export async function sendMessage(
|
|
|
655
633
|
messageType: "outbound-proactive",
|
|
656
634
|
quotedRef: options.quotedRef,
|
|
657
635
|
log,
|
|
636
|
+
...DEFAULT_OUTBOUND_SENDER,
|
|
637
|
+
chatType: inferConversationChatType(options.conversationId || conversationId),
|
|
658
638
|
delivery: {
|
|
659
639
|
...delivery,
|
|
660
640
|
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 };
|
package/src/types.ts
CHANGED
|
@@ -32,9 +32,6 @@ export type AckReactionConfigValue = string;
|
|
|
32
32
|
export interface DingTalkConfig extends OpenClawConfig {
|
|
33
33
|
clientId: string;
|
|
34
34
|
clientSecret: string;
|
|
35
|
-
robotCode?: string;
|
|
36
|
-
corpId?: string;
|
|
37
|
-
agentId?: string;
|
|
38
35
|
name?: string;
|
|
39
36
|
enabled?: boolean;
|
|
40
37
|
dmPolicy?: "open" | "pairing" | "allowlist";
|
|
@@ -82,12 +79,6 @@ export interface DingTalkConfig extends OpenClawConfig {
|
|
|
82
79
|
learningAutoApply?: boolean;
|
|
83
80
|
/** Session learning note TTL in milliseconds (default 6h) */
|
|
84
81
|
learningNoteTtlMs?: number;
|
|
85
|
-
/** @deprecated Use learningEnabled */
|
|
86
|
-
feedbackLearningEnabled?: boolean;
|
|
87
|
-
/** @deprecated Use learningAutoApply */
|
|
88
|
-
feedbackLearningAutoApply?: boolean;
|
|
89
|
-
/** @deprecated Use learningNoteTtlMs */
|
|
90
|
-
feedbackLearningNoteTtlMs?: number;
|
|
91
82
|
/** Whether to convert markdown tables to plain text for better rendering on some clients (default: true) */
|
|
92
83
|
convertMarkdownTables?: boolean;
|
|
93
84
|
/** @mention the sender after card finalization in group chats; value is the message text */
|
|
@@ -101,9 +92,6 @@ export interface DingTalkChannelConfig {
|
|
|
101
92
|
enabled?: boolean;
|
|
102
93
|
clientId: string;
|
|
103
94
|
clientSecret: string;
|
|
104
|
-
robotCode?: string;
|
|
105
|
-
corpId?: string;
|
|
106
|
-
agentId?: string;
|
|
107
95
|
name?: string;
|
|
108
96
|
dmPolicy?: "open" | "pairing" | "allowlist";
|
|
109
97
|
groupPolicy?: "open" | "allowlist" | "disabled";
|
|
@@ -149,12 +137,6 @@ export interface DingTalkChannelConfig {
|
|
|
149
137
|
learningAutoApply?: boolean;
|
|
150
138
|
/** Session learning note TTL in milliseconds (default 6h) */
|
|
151
139
|
learningNoteTtlMs?: number;
|
|
152
|
-
/** @deprecated Use learningEnabled */
|
|
153
|
-
feedbackLearningEnabled?: boolean;
|
|
154
|
-
/** @deprecated Use learningAutoApply */
|
|
155
|
-
feedbackLearningAutoApply?: boolean;
|
|
156
|
-
/** @deprecated Use learningNoteTtlMs */
|
|
157
|
-
feedbackLearningNoteTtlMs?: number;
|
|
158
140
|
/** Whether to convert markdown tables to plain text for better rendering on some clients (default: true) */
|
|
159
141
|
convertMarkdownTables?: boolean;
|
|
160
142
|
/** @mention the sender after card finalization in group chats; value is the message text */
|
|
@@ -313,6 +295,7 @@ export interface QuotedInfo {
|
|
|
313
295
|
cardCreatedAt?: number;
|
|
314
296
|
processQueryKey?: string;
|
|
315
297
|
fileCreatedAt?: number;
|
|
298
|
+
fileDownloadCode?: string;
|
|
316
299
|
msgId?: string;
|
|
317
300
|
previewText?: string;
|
|
318
301
|
previewMessageType?: string;
|
|
@@ -771,9 +754,6 @@ export function resolveDingTalkAccount(
|
|
|
771
754
|
const config: DingTalkConfig = {
|
|
772
755
|
clientId: dingtalk?.clientId ?? "",
|
|
773
756
|
clientSecret: dingtalk?.clientSecret ?? "",
|
|
774
|
-
robotCode: dingtalk?.robotCode,
|
|
775
|
-
corpId: dingtalk?.corpId,
|
|
776
|
-
agentId: dingtalk?.agentId,
|
|
777
757
|
name: dingtalk?.name,
|
|
778
758
|
enabled: dingtalk?.enabled,
|
|
779
759
|
dmPolicy: dingtalk?.dmPolicy,
|
|
@@ -802,12 +782,9 @@ export function resolveDingTalkAccount(
|
|
|
802
782
|
proactivePermissionHint: dingtalk?.proactivePermissionHint,
|
|
803
783
|
cardRealTimeStream: dingtalk?.cardRealTimeStream,
|
|
804
784
|
aicardDegradeMs: dingtalk?.aicardDegradeMs,
|
|
805
|
-
learningEnabled: dingtalk?.learningEnabled
|
|
806
|
-
learningAutoApply: dingtalk?.learningAutoApply
|
|
807
|
-
learningNoteTtlMs: dingtalk?.learningNoteTtlMs
|
|
808
|
-
feedbackLearningEnabled: dingtalk?.feedbackLearningEnabled,
|
|
809
|
-
feedbackLearningAutoApply: dingtalk?.feedbackLearningAutoApply,
|
|
810
|
-
feedbackLearningNoteTtlMs: dingtalk?.feedbackLearningNoteTtlMs,
|
|
785
|
+
learningEnabled: dingtalk?.learningEnabled,
|
|
786
|
+
learningAutoApply: dingtalk?.learningAutoApply,
|
|
787
|
+
learningNoteTtlMs: dingtalk?.learningNoteTtlMs,
|
|
811
788
|
convertMarkdownTables: dingtalk?.convertMarkdownTables,
|
|
812
789
|
cardAtSender: dingtalk?.cardAtSender,
|
|
813
790
|
};
|