@soimy/dingtalk 3.5.1 → 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 +13 -24
- package/openclaw.plugin.json +695 -0
- package/package.json +12 -7
- package/src/ack-reaction-service.ts +1 -1
- package/src/auth.ts +1 -1
- package/src/card/card-action-handler.ts +1 -1
- package/src/card/card-stop-handler.ts +1 -1
- package/src/card/card-streaming-mode.ts +30 -0
- package/src/card/reasoning-answer-split.ts +162 -0
- package/src/card/reasoning-block-assembler.ts +157 -0
- package/src/card-callback-service.ts +1 -1
- package/src/card-draft-controller.ts +117 -6
- package/src/card-service.ts +112 -1
- package/src/channel.ts +131 -96
- package/src/command/card-stop-command.ts +4 -22
- package/src/command/inbound-command-dispatch-service.ts +464 -0
- package/src/config-schema.ts +62 -38
- package/src/config.ts +25 -3
- package/src/docs-service.ts +5 -5
- package/src/http-client.ts +20 -0
- package/src/inbound-handler.ts +475 -501
- package/src/logger-context.ts +16 -2
- package/src/media-utils.ts +166 -10
- package/src/message-utils.ts +33 -5
- package/src/{attachment-text-extractor.ts → messaging/attachment-text-extractor.ts} +1 -1
- package/src/{quoted-file-service.ts → messaging/quoted-file-service.ts} +14 -9
- package/src/onboarding.ts +29 -0
- package/src/plugin-sdk-channel-actions-augment.ts +11 -0
- package/src/reply-strategy-card.ts +294 -28
- package/src/reply-strategy-markdown.ts +124 -19
- package/src/reply-strategy.ts +22 -2
- package/src/send-service.ts +178 -7
- package/src/targeting/agent-routing.ts +55 -32
- package/src/{group-members-store.ts → targeting/group-members-store.ts} +1 -1
- package/src/types.ts +60 -4
- package/src/utils.ts +190 -0
package/src/send-service.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
|
-
import axios from "
|
|
2
|
+
import axios from "./http-client";
|
|
3
3
|
import { getAccessToken } from "./auth";
|
|
4
4
|
import {
|
|
5
5
|
isCardInTerminalState,
|
|
@@ -37,6 +37,53 @@ import type {
|
|
|
37
37
|
|
|
38
38
|
export { detectMediaTypeFromExtension } from "./media-utils";
|
|
39
39
|
|
|
40
|
+
const MARKDOWN_LOCAL_IMAGE_RE =
|
|
41
|
+
/!\[([^\]]*)\]\((file:\/\/\/[^)]+|\/(?:tmp|var|private|Users|home|root)[^)]+|[A-Za-z]:[\\/][^)]+)\)/g;
|
|
42
|
+
|
|
43
|
+
function decodeMarkdownLocalImagePath(rawPath: string): string {
|
|
44
|
+
const unescapedPath = rawPath.replace(/\\ /g, " ");
|
|
45
|
+
if (unescapedPath.startsWith("file://")) {
|
|
46
|
+
try {
|
|
47
|
+
return decodeURIComponent(unescapedPath.replace("file://", ""));
|
|
48
|
+
} catch {
|
|
49
|
+
return unescapedPath.replace("file://", "");
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return unescapedPath;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function replaceMarkdownLocalImages(params: {
|
|
56
|
+
config: DingTalkConfig;
|
|
57
|
+
text: string;
|
|
58
|
+
log?: Logger;
|
|
59
|
+
mediaLocalRoots?: string[];
|
|
60
|
+
}): Promise<string> {
|
|
61
|
+
const matches = [...params.text.matchAll(MARKDOWN_LOCAL_IMAGE_RE)];
|
|
62
|
+
if (matches.length === 0) {
|
|
63
|
+
return params.text;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let result = params.text;
|
|
67
|
+
for (const match of matches) {
|
|
68
|
+
const [fullMatch, altText, rawPath] = match;
|
|
69
|
+
const mediaPath = decodeMarkdownLocalImagePath(rawPath);
|
|
70
|
+
const uploadResult = await uploadMedia(params.config, mediaPath, "image", params.log, {
|
|
71
|
+
mediaLocalRoots: params.mediaLocalRoots,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
if (!uploadResult?.mediaId) {
|
|
75
|
+
params.log?.warn?.(
|
|
76
|
+
`[DingTalk] Markdown local image upload failed, keep original reference: ${mediaPath}`,
|
|
77
|
+
);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
result = result.replace(fullMatch, () => ``);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
|
|
40
87
|
type ProactiveTextSendResult = AxiosResponse | { tracking: DingTalkTrackingMetadata };
|
|
41
88
|
|
|
42
89
|
function isTrackingResult(result: ProactiveTextSendResult): result is { tracking: DingTalkTrackingMetadata } {
|
|
@@ -165,6 +212,14 @@ function extractErrorCodeFromResponseData(data: unknown): string | null {
|
|
|
165
212
|
}
|
|
166
213
|
|
|
167
214
|
const payload = data as Record<string, unknown>;
|
|
215
|
+
const errcode = payload.errcode;
|
|
216
|
+
if (typeof errcode === "number" && Number.isFinite(errcode)) {
|
|
217
|
+
return String(errcode);
|
|
218
|
+
}
|
|
219
|
+
if (typeof errcode === "string" && errcode.trim()) {
|
|
220
|
+
return errcode.trim();
|
|
221
|
+
}
|
|
222
|
+
|
|
168
223
|
const code = payload.code;
|
|
169
224
|
if (typeof code === "string" && code.trim()) {
|
|
170
225
|
return code.trim();
|
|
@@ -178,6 +233,62 @@ function extractErrorCodeFromResponseData(data: unknown): string | null {
|
|
|
178
233
|
return null;
|
|
179
234
|
}
|
|
180
235
|
|
|
236
|
+
function summarizeSessionWebhookResponse(data: unknown): string {
|
|
237
|
+
if (!data || typeof data !== "object") {
|
|
238
|
+
return `type=${typeof data}`;
|
|
239
|
+
}
|
|
240
|
+
const payload = data as Record<string, unknown>;
|
|
241
|
+
const code = extractErrorCodeFromResponseData(payload) || "(none)";
|
|
242
|
+
const message = firstTrimmedString(
|
|
243
|
+
payload.message,
|
|
244
|
+
payload.errmsg,
|
|
245
|
+
payload.msg,
|
|
246
|
+
payload.errorMessage,
|
|
247
|
+
) || "(none)";
|
|
248
|
+
const success =
|
|
249
|
+
typeof payload.success === "boolean"
|
|
250
|
+
? String(payload.success)
|
|
251
|
+
: typeof payload.result === "boolean"
|
|
252
|
+
? String(payload.result)
|
|
253
|
+
: "(none)";
|
|
254
|
+
const delivery = extractOutboundDeliveryMetadata(payload);
|
|
255
|
+
return (
|
|
256
|
+
`success=${success} code=${code} message=${message} ` +
|
|
257
|
+
`messageId=${delivery.messageId || "(none)"} ` +
|
|
258
|
+
`processQueryKey=${delivery.processQueryKey || "(none)"} ` +
|
|
259
|
+
`outTrackId=${delivery.outTrackId || "(none)"}`
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function ensureSessionWebhookBusinessSuccess(
|
|
264
|
+
data: unknown,
|
|
265
|
+
context: { msgtype: string },
|
|
266
|
+
): void {
|
|
267
|
+
if (!data || typeof data !== "object") {
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
const payload = data as Record<string, unknown>;
|
|
271
|
+
const code = extractErrorCodeFromResponseData(payload);
|
|
272
|
+
const message = firstTrimmedString(
|
|
273
|
+
payload.message,
|
|
274
|
+
payload.errmsg,
|
|
275
|
+
payload.msg,
|
|
276
|
+
payload.errorMessage,
|
|
277
|
+
) || "unknown error";
|
|
278
|
+
|
|
279
|
+
const hasFailureSuccessFlag = payload.success === false || payload.result === false;
|
|
280
|
+
const hasFailureCode = typeof code === "string" && code !== "" && code !== "0";
|
|
281
|
+
if (!hasFailureSuccessFlag && !hasFailureCode) {
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const reason = [
|
|
286
|
+
code && code !== "0" ? `code=${code}` : "",
|
|
287
|
+
message !== "unknown error" ? `message=${message}` : "",
|
|
288
|
+
].filter(Boolean).join(" ");
|
|
289
|
+
throw new Error(`Session webhook ${context.msgtype} send failed${reason ? `: ${reason}` : ""}`);
|
|
290
|
+
}
|
|
291
|
+
|
|
181
292
|
function isProactivePermissionOrScopeError(code: string | null): boolean {
|
|
182
293
|
if (!code) {
|
|
183
294
|
return false;
|
|
@@ -252,7 +363,16 @@ export async function sendProactiveTextOrMarkdown(
|
|
|
252
363
|
? "https://api.dingtalk.com/v1.0/robot/groupMessages/send"
|
|
253
364
|
: "https://api.dingtalk.com/v1.0/robot/oToMessages/batchSend";
|
|
254
365
|
|
|
255
|
-
const
|
|
366
|
+
const textWithUploadedLocalImages = await replaceMarkdownLocalImages({
|
|
367
|
+
config,
|
|
368
|
+
text,
|
|
369
|
+
log,
|
|
370
|
+
mediaLocalRoots: options.mediaLocalRoots,
|
|
371
|
+
});
|
|
372
|
+
const normalizedText =
|
|
373
|
+
config.convertMarkdownTables !== false
|
|
374
|
+
? convertMarkdownTablesToPlainText(textWithUploadedLocalImages)
|
|
375
|
+
: textWithUploadedLocalImages;
|
|
256
376
|
const { useMarkdown, title } = detectMarkdownAndExtractTitle(normalizedText, options, "OpenClaw 提醒");
|
|
257
377
|
|
|
258
378
|
log?.debug?.(
|
|
@@ -344,7 +464,7 @@ export async function sendProactiveMedia(
|
|
|
344
464
|
if (!uploadResult) {
|
|
345
465
|
return { ok: false, error: "Failed to upload media" };
|
|
346
466
|
}
|
|
347
|
-
const { mediaId, buffer } = uploadResult;
|
|
467
|
+
const { mediaId, buffer, durationMs: uploadedDurationMs } = uploadResult;
|
|
348
468
|
|
|
349
469
|
const token = await getAccessToken(config, log);
|
|
350
470
|
const { targetId, isExplicitUser } = stripTargetPrefix(target);
|
|
@@ -365,7 +485,8 @@ export async function sendProactiveMedia(
|
|
|
365
485
|
msgParam = JSON.stringify({ photoURL: mediaId });
|
|
366
486
|
} else if (mediaType === "voice") {
|
|
367
487
|
msgKey = "sampleAudio";
|
|
368
|
-
const durationMs =
|
|
488
|
+
const durationMs = uploadedDurationMs
|
|
489
|
+
?? await getVoiceDurationMs(mediaPath, mediaType, log, { preReadBuffer: buffer });
|
|
369
490
|
msgParam = JSON.stringify({ mediaId, duration: String(durationMs) });
|
|
370
491
|
} else {
|
|
371
492
|
// sampleVideo requires picMediaId; fallback to sampleFile for broader compatibility.
|
|
@@ -499,14 +620,18 @@ export async function sendBySession(
|
|
|
499
620
|
mediaLocalRoots: options.mediaLocalRoots,
|
|
500
621
|
});
|
|
501
622
|
if (uploadResult) {
|
|
502
|
-
const { mediaId, buffer } = uploadResult;
|
|
623
|
+
const { mediaId, buffer, durationMs: uploadedDurationMs } = uploadResult;
|
|
503
624
|
let body: any;
|
|
504
625
|
|
|
505
626
|
if (options.mediaType === "image") {
|
|
506
627
|
body = { msgtype: "image", image: { media_id: mediaId } };
|
|
507
628
|
} else if (options.mediaType === "voice") {
|
|
508
|
-
const durationMs =
|
|
629
|
+
const durationMs = uploadedDurationMs
|
|
630
|
+
?? await getVoiceDurationMs(options.mediaPath, options.mediaType, log, { preReadBuffer: buffer });
|
|
509
631
|
body = { msgtype: "voice", voice: { media_id: mediaId, duration: String(durationMs) } };
|
|
632
|
+
log?.debug?.(
|
|
633
|
+
`[DingTalk] Sending session voice message mediaId=${mediaId} durationMs=${durationMs}`,
|
|
634
|
+
);
|
|
510
635
|
} else if (options.mediaType === "video") {
|
|
511
636
|
body = { msgtype: "video", video: { media_id: mediaId } };
|
|
512
637
|
} else if (options.mediaType === "file") {
|
|
@@ -521,6 +646,17 @@ export async function sendBySession(
|
|
|
521
646
|
headers: { "x-acs-dingtalk-access-token": token, "Content-Type": "application/json" },
|
|
522
647
|
...getProxyBypassOption(config),
|
|
523
648
|
});
|
|
649
|
+
log?.debug?.(
|
|
650
|
+
`[DingTalk] Session webhook response msgtype=${body.msgtype} ${summarizeSessionWebhookResponse(result.data)}`,
|
|
651
|
+
);
|
|
652
|
+
ensureSessionWebhookBusinessSuccess(result.data, { msgtype: body.msgtype });
|
|
653
|
+
const delivery = extractOutboundDeliveryMetadata(result.data);
|
|
654
|
+
if (!delivery.messageId && !delivery.processQueryKey && !delivery.outTrackId) {
|
|
655
|
+
log?.warn?.(
|
|
656
|
+
`[DingTalk] Session webhook ${body.msgtype} response missing delivery metadata; ` +
|
|
657
|
+
summarizeSessionWebhookResponse(result.data),
|
|
658
|
+
);
|
|
659
|
+
}
|
|
524
660
|
return result.data;
|
|
525
661
|
}
|
|
526
662
|
} else {
|
|
@@ -531,7 +667,16 @@ export async function sendBySession(
|
|
|
531
667
|
}
|
|
532
668
|
|
|
533
669
|
// Fallback to text/markdown reply payload.
|
|
534
|
-
const
|
|
670
|
+
const textWithUploadedLocalImages = await replaceMarkdownLocalImages({
|
|
671
|
+
config,
|
|
672
|
+
text,
|
|
673
|
+
log,
|
|
674
|
+
mediaLocalRoots: options.mediaLocalRoots,
|
|
675
|
+
});
|
|
676
|
+
const normalizedText =
|
|
677
|
+
config.convertMarkdownTables !== false
|
|
678
|
+
? convertMarkdownTablesToPlainText(textWithUploadedLocalImages)
|
|
679
|
+
: textWithUploadedLocalImages;
|
|
535
680
|
const { useMarkdown, title } = detectMarkdownAndExtractTitle(normalizedText, options, "Clawdbot 消息");
|
|
536
681
|
const chunks = splitMarkdownChunks(normalizedText, DINGTALK_TEXT_CHUNK_LIMIT);
|
|
537
682
|
|
|
@@ -559,6 +704,10 @@ export async function sendBySession(
|
|
|
559
704
|
headers: { "x-acs-dingtalk-access-token": token, "Content-Type": "application/json" },
|
|
560
705
|
...getProxyBypassOption(config),
|
|
561
706
|
});
|
|
707
|
+
log?.debug?.(
|
|
708
|
+
`[DingTalk] Session webhook response msgtype=${body.msgtype} ${summarizeSessionWebhookResponse(result.data)}`,
|
|
709
|
+
);
|
|
710
|
+
ensureSessionWebhookBusinessSuccess(result.data, { msgtype: body.msgtype });
|
|
562
711
|
lastResult = result.data;
|
|
563
712
|
}
|
|
564
713
|
return lastResult;
|
|
@@ -597,6 +746,28 @@ export async function sendMessage(
|
|
|
597
746
|
}
|
|
598
747
|
}
|
|
599
748
|
|
|
749
|
+
if (options.sessionWebhook && options.mediaPath && options.mediaType === "voice") {
|
|
750
|
+
log?.debug?.(
|
|
751
|
+
"[DingTalk] Session webhook does not support voice replies reliably; " +
|
|
752
|
+
"using proactive media API for this voice response",
|
|
753
|
+
);
|
|
754
|
+
const proactiveVoiceResult = await sendProactiveMedia(
|
|
755
|
+
config,
|
|
756
|
+
conversationId,
|
|
757
|
+
options.mediaPath,
|
|
758
|
+
options.mediaType,
|
|
759
|
+
options,
|
|
760
|
+
);
|
|
761
|
+
if (!proactiveVoiceResult.ok) {
|
|
762
|
+
return { ok: false, error: proactiveVoiceResult.error || "Voice reply send failed" };
|
|
763
|
+
}
|
|
764
|
+
return {
|
|
765
|
+
ok: true,
|
|
766
|
+
data: proactiveVoiceResult.data,
|
|
767
|
+
messageId: proactiveVoiceResult.messageId,
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
|
|
600
771
|
if (options.sessionWebhook) {
|
|
601
772
|
const data = await sendBySession(config, options.sessionWebhook, text, options);
|
|
602
773
|
const delivery = extractOutboundDeliveryMetadata(data);
|
|
@@ -8,16 +8,26 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
|
|
11
|
+
import { maybeResolveTextAlias } from "openclaw/plugin-sdk/command-auth";
|
|
11
12
|
import { resolveAtAgents } from "./agent-name-matcher";
|
|
12
13
|
import { resolveRobotCode } from "../config";
|
|
13
14
|
import { parseLearnCommand } from "../learning-command-service";
|
|
14
15
|
import { getDingTalkRuntime } from "../runtime";
|
|
15
16
|
import { sendBySession } from "../send-service";
|
|
17
|
+
import { getErrorMessage } from "../utils";
|
|
16
18
|
import type { AgentNameMatch, DingTalkConfig, DingTalkInboundMessage, HandleDingTalkMessageParams, Logger, MessageContent } from "../types";
|
|
17
19
|
|
|
20
|
+
export class HostRoutingHelperUnavailableError extends Error {
|
|
21
|
+
constructor(message = "DingTalk sub-agent routing requires runtime.channel.routing.buildAgentSessionKey from the host runtime.") {
|
|
22
|
+
super(message);
|
|
23
|
+
this.name = "HostRoutingHelperUnavailableError";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
18
27
|
/**
|
|
19
28
|
* Build a session key for a specific agent using the runtime API.
|
|
20
|
-
*
|
|
29
|
+
* On supported host versions, sub-agent routing must use the shared helper
|
|
30
|
+
* instead of synthesizing plugin-local fallback keys.
|
|
21
31
|
*/
|
|
22
32
|
export function buildAgentSessionKey(params: {
|
|
23
33
|
rt: ReturnType<typeof getDingTalkRuntime>;
|
|
@@ -29,31 +39,19 @@ export function buildAgentSessionKey(params: {
|
|
|
29
39
|
}): string {
|
|
30
40
|
const { rt, cfg, accountId, agentId, peerKind, peerId } = params;
|
|
31
41
|
const routing = rt.channel.routing as Record<string, unknown>;
|
|
32
|
-
if (typeof routing.buildAgentSessionKey
|
|
33
|
-
|
|
34
|
-
(routing.buildAgentSessionKey as (p: unknown) => string)({
|
|
35
|
-
agentId,
|
|
36
|
-
channel: "dingtalk",
|
|
37
|
-
accountId,
|
|
38
|
-
peer: { kind: peerKind, id: peerId },
|
|
39
|
-
dmScope: cfg.session?.dmScope,
|
|
40
|
-
identityLinks: cfg.session?.identityLinks,
|
|
41
|
-
})
|
|
42
|
-
).toLowerCase();
|
|
42
|
+
if (typeof routing.buildAgentSessionKey !== "function") {
|
|
43
|
+
throw new HostRoutingHelperUnavailableError();
|
|
43
44
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
peer: { kind: peerKind, id: peerId },
|
|
55
|
-
});
|
|
56
|
-
return `${fallbackRoute.sessionKey}:subagent:${agentId}`;
|
|
45
|
+
return (
|
|
46
|
+
(routing.buildAgentSessionKey as (p: unknown) => string)({
|
|
47
|
+
agentId,
|
|
48
|
+
channel: "dingtalk",
|
|
49
|
+
accountId,
|
|
50
|
+
peer: { kind: peerKind, id: peerId },
|
|
51
|
+
dmScope: cfg.session?.dmScope,
|
|
52
|
+
identityLinks: cfg.session?.identityLinks,
|
|
53
|
+
})
|
|
54
|
+
).toLowerCase();
|
|
57
55
|
}
|
|
58
56
|
|
|
59
57
|
/**
|
|
@@ -93,16 +91,22 @@ export async function resolveSubAgentRoute(params: {
|
|
|
93
91
|
const atMentions = extractedContent.atMentions || [];
|
|
94
92
|
// DM has no @picker list from DingTalk; only group chats provide atUsers for real-user hints.
|
|
95
93
|
const atUserDingtalkIds = isGroup ? extractedContent.atUserDingtalkIds : undefined;
|
|
96
|
-
// Strip quoted prefix before checking
|
|
97
|
-
// when the quoted message itself contains a
|
|
94
|
+
// Strip quoted prefix before checking commands to avoid false positives
|
|
95
|
+
// when the quoted message itself contains a command.
|
|
98
96
|
const textForCommandCheck = extractedContent.text.replace(/^\[引用[^\]]*\]\s*/, "");
|
|
99
97
|
const isLearnCommand = parseLearnCommand(textForCommandCheck).scope !== "unknown";
|
|
98
|
+
// Slash commands like /new, /stop, /reasoning etc. must bypass sub-agent
|
|
99
|
+
// routing so they reach the framework's own command handling layer.
|
|
100
|
+
// Strip leading @mention tokens first since DM text may look like "@Agent /new".
|
|
101
|
+
const textWithoutMentions = textForCommandCheck.replace(/^(?:@\S+\s+)*/u, "").trim();
|
|
102
|
+
const isSlashCommand = maybeResolveTextAlias(textWithoutMentions, cfg) !== null;
|
|
100
103
|
|
|
101
104
|
if (
|
|
102
105
|
atMentions.length === 0 ||
|
|
103
106
|
!cfg.agents?.list ||
|
|
104
107
|
cfg.agents.list.length === 0 ||
|
|
105
|
-
isLearnCommand
|
|
108
|
+
isLearnCommand ||
|
|
109
|
+
isSlashCommand
|
|
106
110
|
) {
|
|
107
111
|
return null;
|
|
108
112
|
}
|
|
@@ -124,8 +128,8 @@ export async function resolveSubAgentRoute(params: {
|
|
|
124
128
|
await sendBySession(dingtalkConfig, sessionWebhook, `⚠️ ${fallbackReason}`, {
|
|
125
129
|
...sendOptions,
|
|
126
130
|
});
|
|
127
|
-
} catch (err:
|
|
128
|
-
log?.debug?.(`[DingTalk] Failed to send fallback notice: ${err
|
|
131
|
+
} catch (err: unknown) {
|
|
132
|
+
log?.debug?.(`[DingTalk] Failed to send fallback notice: ${getErrorMessage(err)}`);
|
|
129
133
|
}
|
|
130
134
|
}
|
|
131
135
|
|
|
@@ -161,6 +165,7 @@ export async function dispatchSubAgents(params: {
|
|
|
161
165
|
preDownloadedMedia = { mediaPath: media.path, mediaType: media.mimeType };
|
|
162
166
|
}
|
|
163
167
|
}
|
|
168
|
+
let helperMissingWarningSent = false;
|
|
164
169
|
|
|
165
170
|
for (const agentMatch of matchedAgents) {
|
|
166
171
|
try {
|
|
@@ -173,15 +178,33 @@ export async function dispatchSubAgents(params: {
|
|
|
173
178
|
dingtalkConfig,
|
|
174
179
|
subAgentOptions: {
|
|
175
180
|
agentId: agentMatch.agentId,
|
|
176
|
-
responsePrefix:
|
|
181
|
+
responsePrefix: `> 🤖 **${sanitizeAgentName(agentMatch.matchedName)}**:\n\n`,
|
|
177
182
|
matchedName: agentMatch.matchedName,
|
|
178
183
|
},
|
|
179
184
|
preDownloadedMedia,
|
|
180
185
|
});
|
|
181
186
|
} catch (error) {
|
|
187
|
+
const message = getErrorMessage(error);
|
|
182
188
|
log?.error?.(
|
|
183
|
-
`[DingTalk] Sub-agent ${agentMatch.agentId} failed: ${
|
|
189
|
+
`[DingTalk] Sub-agent ${agentMatch.agentId} failed: ${message}`,
|
|
184
190
|
);
|
|
191
|
+
if (error instanceof HostRoutingHelperUnavailableError && !helperMissingWarningSent) {
|
|
192
|
+
helperMissingWarningSent = true;
|
|
193
|
+
try {
|
|
194
|
+
const isGroup = data.conversationType !== "1";
|
|
195
|
+
const sendOptions = isGroup ? { atUserId: data.senderId, log } : { log };
|
|
196
|
+
await sendBySession(
|
|
197
|
+
dingtalkConfig,
|
|
198
|
+
sessionWebhook,
|
|
199
|
+
"⚠️ 当前宿主版本不支持 DingTalk 子助手路由所需的 session helper,请升级 OpenClaw 后重试。",
|
|
200
|
+
sendOptions,
|
|
201
|
+
);
|
|
202
|
+
} catch (notifyError: unknown) {
|
|
203
|
+
log?.debug?.(
|
|
204
|
+
`[DingTalk] Failed to send sub-agent helper-missing notice: ${getErrorMessage(notifyError)}`,
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
185
208
|
}
|
|
186
209
|
}
|
|
187
210
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import { readNamespaceJson, writeNamespaceJsonAtomic } from "
|
|
3
|
+
import { readNamespaceJson, writeNamespaceJsonAtomic } from "../persistence-store";
|
|
4
4
|
|
|
5
5
|
const GROUP_MEMBERS_NAMESPACE = "members.group-roster";
|
|
6
6
|
|
package/src/types.ts
CHANGED
|
@@ -25,6 +25,8 @@ export type AckReactionMode = "off" | "emoji" | "kaomoji";
|
|
|
25
25
|
// Accept arbitrary strings for backward compatibility; the recommended
|
|
26
26
|
// explicit modes remain: "off" | "emoji" | "kaomoji".
|
|
27
27
|
export type AckReactionConfigValue = string;
|
|
28
|
+
export type CardStreamingMode = "off" | "answer" | "all";
|
|
29
|
+
export type ContextVisibilityMode = "all" | "allowlist" | "allowlist_quote";
|
|
28
30
|
|
|
29
31
|
/**
|
|
30
32
|
* DingTalk channel configuration (extends base OpenClaw config)
|
|
@@ -39,6 +41,7 @@ export interface DingTalkConfig extends OpenClawConfig {
|
|
|
39
41
|
allowFrom?: string[];
|
|
40
42
|
groupAllowFrom?: string[];
|
|
41
43
|
displayNameResolution?: "disabled" | "all";
|
|
44
|
+
contextVisibility?: ContextVisibilityMode;
|
|
42
45
|
mediaUrlAllowlist?: string[];
|
|
43
46
|
journalTTLDays?: number;
|
|
44
47
|
ackReaction?: AckReactionConfigValue;
|
|
@@ -71,8 +74,15 @@ export interface DingTalkConfig extends OpenClawConfig {
|
|
|
71
74
|
enabled?: boolean;
|
|
72
75
|
cooldownHours?: number;
|
|
73
76
|
};
|
|
74
|
-
/**
|
|
77
|
+
/** Card streaming mode.
|
|
78
|
+
* - off: disable incremental streaming
|
|
79
|
+
* - answer: stream answer text
|
|
80
|
+
* - all: stream answer + reasoning text */
|
|
81
|
+
cardStreamingMode?: CardStreamingMode;
|
|
82
|
+
/** @deprecated Use `cardStreamingMode` instead. */
|
|
75
83
|
cardRealTimeStream?: boolean;
|
|
84
|
+
/** Throttle interval in ms for card stream updates (default 1000) */
|
|
85
|
+
cardStreamInterval?: number;
|
|
76
86
|
/** AICard degrade duration in milliseconds after trigger errors (default 30m) */
|
|
77
87
|
aicardDegradeMs?: number;
|
|
78
88
|
/** Enable local learning loop (events/reflections/session notes/global rules) */
|
|
@@ -100,6 +110,7 @@ export interface DingTalkChannelConfig {
|
|
|
100
110
|
allowFrom?: string[];
|
|
101
111
|
groupAllowFrom?: string[];
|
|
102
112
|
displayNameResolution?: "disabled" | "all";
|
|
113
|
+
contextVisibility?: ContextVisibilityMode;
|
|
103
114
|
mediaUrlAllowlist?: string[];
|
|
104
115
|
journalTTLDays?: number;
|
|
105
116
|
ackReaction?: AckReactionConfigValue;
|
|
@@ -131,8 +142,15 @@ export interface DingTalkChannelConfig {
|
|
|
131
142
|
enabled?: boolean;
|
|
132
143
|
cooldownHours?: number;
|
|
133
144
|
};
|
|
134
|
-
/**
|
|
145
|
+
/** Card streaming mode.
|
|
146
|
+
* - off: disable incremental streaming
|
|
147
|
+
* - answer: stream answer text
|
|
148
|
+
* - all: stream answer + reasoning text */
|
|
149
|
+
cardStreamingMode?: CardStreamingMode;
|
|
150
|
+
/** @deprecated Use `cardStreamingMode` instead. */
|
|
135
151
|
cardRealTimeStream?: boolean;
|
|
152
|
+
/** Throttle interval in ms for card stream updates (default 1000) */
|
|
153
|
+
cardStreamInterval?: number;
|
|
136
154
|
/** AICard degrade duration in milliseconds after trigger errors (default 30m) */
|
|
137
155
|
aicardDegradeMs?: number;
|
|
138
156
|
/** Enable local learning loop (events/reflections/session notes/global rules) */
|
|
@@ -234,6 +252,10 @@ export interface DingTalkInboundMessage {
|
|
|
234
252
|
atName?: string;
|
|
235
253
|
downloadCode?: string;
|
|
236
254
|
}>;
|
|
255
|
+
/** chatRecord 消息摘要 */
|
|
256
|
+
summary?: string;
|
|
257
|
+
/** chatRecord 消息标题 */
|
|
258
|
+
title?: string;
|
|
237
259
|
};
|
|
238
260
|
};
|
|
239
261
|
};
|
|
@@ -243,6 +265,8 @@ export interface DingTalkInboundMessage {
|
|
|
243
265
|
recognition?: string;
|
|
244
266
|
spaceId?: string;
|
|
245
267
|
fileId?: string;
|
|
268
|
+
text?: string;
|
|
269
|
+
title?: string;
|
|
246
270
|
biz_custom_action_url?: string;
|
|
247
271
|
richText?: Array<{
|
|
248
272
|
type: string;
|
|
@@ -712,6 +736,33 @@ export interface ConnectionAttemptResult {
|
|
|
712
736
|
|
|
713
737
|
const DEFAULT_ACCOUNT_ID = "default";
|
|
714
738
|
|
|
739
|
+
function stripRemovedLegacyFieldsFromPublicAccount(
|
|
740
|
+
config: DingTalkConfig,
|
|
741
|
+
): DingTalkConfig {
|
|
742
|
+
const {
|
|
743
|
+
cardStreamReasoning: _cardStreamReasoning,
|
|
744
|
+
verboseRealtimeStream: _verboseRealtimeStream,
|
|
745
|
+
accounts,
|
|
746
|
+
...rest
|
|
747
|
+
} = config as DingTalkConfig & {
|
|
748
|
+
cardStreamReasoning?: unknown;
|
|
749
|
+
verboseRealtimeStream?: unknown;
|
|
750
|
+
accounts?: Record<string, DingTalkConfig | undefined>;
|
|
751
|
+
};
|
|
752
|
+
const sanitizedAccounts = accounts
|
|
753
|
+
? Object.fromEntries(
|
|
754
|
+
Object.entries(accounts).map(([accountId, accountConfig]) => [
|
|
755
|
+
accountId,
|
|
756
|
+
accountConfig ? stripRemovedLegacyFieldsFromPublicAccount(accountConfig) : accountConfig,
|
|
757
|
+
]),
|
|
758
|
+
)
|
|
759
|
+
: undefined;
|
|
760
|
+
if (sanitizedAccounts) {
|
|
761
|
+
return { ...rest, accounts: sanitizedAccounts } as DingTalkConfig;
|
|
762
|
+
}
|
|
763
|
+
return rest as DingTalkConfig;
|
|
764
|
+
}
|
|
765
|
+
|
|
715
766
|
/**
|
|
716
767
|
* List all DingTalk account IDs from config
|
|
717
768
|
*/
|
|
@@ -756,7 +807,7 @@ export function resolveDingTalkAccount(
|
|
|
756
807
|
|
|
757
808
|
// If default account, return top-level config
|
|
758
809
|
if (id === DEFAULT_ACCOUNT_ID) {
|
|
759
|
-
const
|
|
810
|
+
const rawConfig: DingTalkConfig = {
|
|
760
811
|
clientId: dingtalk?.clientId ?? "",
|
|
761
812
|
clientSecret: dingtalk?.clientSecret ?? "",
|
|
762
813
|
name: dingtalk?.name,
|
|
@@ -766,6 +817,7 @@ export function resolveDingTalkAccount(
|
|
|
766
817
|
allowFrom: dingtalk?.allowFrom,
|
|
767
818
|
groupAllowFrom: dingtalk?.groupAllowFrom,
|
|
768
819
|
displayNameResolution: dingtalk?.displayNameResolution,
|
|
820
|
+
contextVisibility: dingtalk?.contextVisibility,
|
|
769
821
|
journalTTLDays: dingtalk?.journalTTLDays,
|
|
770
822
|
ackReaction: dingtalk?.ackReaction,
|
|
771
823
|
debug: dingtalk?.debug,
|
|
@@ -785,7 +837,9 @@ export function resolveDingTalkAccount(
|
|
|
785
837
|
keepAlive: dingtalk?.keepAlive,
|
|
786
838
|
bypassProxyForSend: dingtalk?.bypassProxyForSend,
|
|
787
839
|
proactivePermissionHint: dingtalk?.proactivePermissionHint,
|
|
840
|
+
cardStreamingMode: dingtalk?.cardStreamingMode,
|
|
788
841
|
cardRealTimeStream: dingtalk?.cardRealTimeStream,
|
|
842
|
+
cardStreamInterval: dingtalk?.cardStreamInterval,
|
|
789
843
|
aicardDegradeMs: dingtalk?.aicardDegradeMs,
|
|
790
844
|
learningEnabled: dingtalk?.learningEnabled,
|
|
791
845
|
learningAutoApply: dingtalk?.learningAutoApply,
|
|
@@ -793,6 +847,7 @@ export function resolveDingTalkAccount(
|
|
|
793
847
|
convertMarkdownTables: dingtalk?.convertMarkdownTables,
|
|
794
848
|
cardAtSender: dingtalk?.cardAtSender,
|
|
795
849
|
};
|
|
850
|
+
const config = stripRemovedLegacyFieldsFromPublicAccount(rawConfig);
|
|
796
851
|
return {
|
|
797
852
|
...config,
|
|
798
853
|
accountId: id,
|
|
@@ -807,8 +862,9 @@ export function resolveDingTalkAccount(
|
|
|
807
862
|
dingtalk as DingTalkConfig,
|
|
808
863
|
accountConfig,
|
|
809
864
|
);
|
|
865
|
+
const publicMerged = stripRemovedLegacyFieldsFromPublicAccount(merged);
|
|
810
866
|
return {
|
|
811
|
-
...
|
|
867
|
+
...publicMerged,
|
|
812
868
|
accountId: id,
|
|
813
869
|
configured: Boolean(merged.clientId && merged.clientSecret),
|
|
814
870
|
};
|