@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.
Files changed (36) hide show
  1. package/README.md +13 -24
  2. package/openclaw.plugin.json +695 -0
  3. package/package.json +12 -7
  4. package/src/ack-reaction-service.ts +1 -1
  5. package/src/auth.ts +1 -1
  6. package/src/card/card-action-handler.ts +1 -1
  7. package/src/card/card-stop-handler.ts +1 -1
  8. package/src/card/card-streaming-mode.ts +30 -0
  9. package/src/card/reasoning-answer-split.ts +162 -0
  10. package/src/card/reasoning-block-assembler.ts +157 -0
  11. package/src/card-callback-service.ts +1 -1
  12. package/src/card-draft-controller.ts +117 -6
  13. package/src/card-service.ts +112 -1
  14. package/src/channel.ts +131 -96
  15. package/src/command/card-stop-command.ts +4 -22
  16. package/src/command/inbound-command-dispatch-service.ts +464 -0
  17. package/src/config-schema.ts +62 -38
  18. package/src/config.ts +25 -3
  19. package/src/docs-service.ts +5 -5
  20. package/src/http-client.ts +20 -0
  21. package/src/inbound-handler.ts +475 -501
  22. package/src/logger-context.ts +16 -2
  23. package/src/media-utils.ts +166 -10
  24. package/src/message-utils.ts +33 -5
  25. package/src/{attachment-text-extractor.ts → messaging/attachment-text-extractor.ts} +1 -1
  26. package/src/{quoted-file-service.ts → messaging/quoted-file-service.ts} +14 -9
  27. package/src/onboarding.ts +29 -0
  28. package/src/plugin-sdk-channel-actions-augment.ts +11 -0
  29. package/src/reply-strategy-card.ts +294 -28
  30. package/src/reply-strategy-markdown.ts +124 -19
  31. package/src/reply-strategy.ts +22 -2
  32. package/src/send-service.ts +178 -7
  33. package/src/targeting/agent-routing.ts +55 -32
  34. package/src/{group-members-store.ts → targeting/group-members-store.ts} +1 -1
  35. package/src/types.ts +60 -4
  36. package/src/utils.ts +190 -0
@@ -1,5 +1,5 @@
1
1
  import * as path from "node:path";
2
- import axios from "axios";
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, () => `![${altText}](${uploadResult.mediaId})`);
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 normalizedText = config.convertMarkdownTables !== false ? convertMarkdownTablesToPlainText(text) : text;
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 = await getVoiceDurationMs(mediaPath, mediaType, log, { preReadBuffer: buffer });
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 = await getVoiceDurationMs(options.mediaPath, options.mediaType, log, { preReadBuffer: buffer });
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 normalizedText = config.convertMarkdownTables !== false ? convertMarkdownTablesToPlainText(text) : text;
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
- * Falls back to framework's resolveAgentRoute if buildAgentSessionKey is unavailable.
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 === "function") {
33
- return (
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
- // Fallback: derive a session key with agentId suffix to ensure isolation.
45
- // resolveAgentRoute routes to the default agent, so we append the target
46
- // agentId to prevent session key collisions between sub-agents.
47
- // @migration-note: When SDK exposes buildAgentSessionKey in type definitions,
48
- // sessions created via this fallback path will become orphaned. Remove this
49
- // fallback and the typeof check once the SDK is updated.
50
- const fallbackRoute = rt.channel.routing.resolveAgentRoute({
51
- cfg,
52
- channel: "dingtalk",
53
- accountId,
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 /learn to avoid false positives
97
- // when the quoted message itself contains a /learn command.
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: any) {
128
- log?.debug?.(`[DingTalk] Failed to send fallback notice: ${err.message}`);
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: `[${sanitizeAgentName(agentMatch.matchedName)}] `,
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: ${error instanceof Error ? error.message : String(error)}`,
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 "./persistence-store";
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
- /** Enable real-time card streaming (default false, true = 300ms throttled per-token updates) */
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
- /** Enable real-time card streaming (default false, true = 300ms throttled per-token updates) */
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 config: DingTalkConfig = {
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
- ...merged,
867
+ ...publicMerged,
812
868
  accountId: id,
813
869
  configured: Boolean(merged.clientId && merged.clientSecret),
814
870
  };