@soimy/dingtalk 3.5.2 → 3.6.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.
Files changed (40) hide show
  1. package/README.md +6 -23
  2. package/index.ts +7 -0
  3. package/openclaw.plugin.json +799 -0
  4. package/package.json +5 -5
  5. package/src/card/card-markdown-image-reroute.ts +106 -0
  6. package/src/card/card-run-registry.ts +54 -1
  7. package/src/card/card-stop-handler.ts +10 -20
  8. package/src/card/card-streaming-mode.ts +30 -0
  9. package/src/card/card-template.ts +14 -3
  10. package/src/card/reasoning-answer-split.ts +162 -0
  11. package/src/card/statusline-renderer.ts +94 -0
  12. package/src/card-draft-controller.ts +326 -54
  13. package/src/card-service.ts +479 -8
  14. package/src/channel.ts +19 -1062
  15. package/src/config-schema.ts +81 -38
  16. package/src/config.ts +142 -4
  17. package/src/device-registration.ts +245 -0
  18. package/src/gateway/channel-gateway.ts +636 -0
  19. package/src/inbound-handler.ts +489 -49
  20. package/src/media-utils.ts +169 -7
  21. package/src/message-utils.ts +153 -17
  22. package/src/messaging/btw-deliver.ts +85 -0
  23. package/src/messaging/channel-actions.ts +173 -0
  24. package/src/messaging/channel-outbound.ts +158 -0
  25. package/src/messaging/quoted-file-service.ts +9 -4
  26. package/src/onboarding.ts +323 -205
  27. package/src/platform/channel-status.ts +81 -0
  28. package/src/plugin-sdk-channel-actions-augment.ts +11 -0
  29. package/src/reply-strategy-card.ts +568 -44
  30. package/src/reply-strategy-markdown.ts +2 -2
  31. package/src/reply-strategy-types.ts +93 -0
  32. package/src/reply-strategy-with-reaction.ts +1 -1
  33. package/src/reply-strategy.ts +14 -56
  34. package/src/run-usage-store.ts +59 -0
  35. package/src/send-service.ts +225 -7
  36. package/src/session-state.ts +62 -0
  37. package/src/targeting/agent-name-matcher.ts +28 -0
  38. package/src/targeting/agent-routing.ts +44 -28
  39. package/src/types.ts +49 -117
  40. package/src/utils.ts +25 -0
@@ -14,11 +14,20 @@ import { resolveRobotCode } from "../config";
14
14
  import { parseLearnCommand } from "../learning-command-service";
15
15
  import { getDingTalkRuntime } from "../runtime";
16
16
  import { sendBySession } from "../send-service";
17
+ import { getErrorMessage } from "../utils";
17
18
  import type { AgentNameMatch, DingTalkConfig, DingTalkInboundMessage, HandleDingTalkMessageParams, Logger, MessageContent } from "../types";
18
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
+
19
27
  /**
20
28
  * Build a session key for a specific agent using the runtime API.
21
- * 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.
22
31
  */
23
32
  export function buildAgentSessionKey(params: {
24
33
  rt: ReturnType<typeof getDingTalkRuntime>;
@@ -30,31 +39,19 @@ export function buildAgentSessionKey(params: {
30
39
  }): string {
31
40
  const { rt, cfg, accountId, agentId, peerKind, peerId } = params;
32
41
  const routing = rt.channel.routing as Record<string, unknown>;
33
- if (typeof routing.buildAgentSessionKey === "function") {
34
- return (
35
- (routing.buildAgentSessionKey as (p: unknown) => string)({
36
- agentId,
37
- channel: "dingtalk",
38
- accountId,
39
- peer: { kind: peerKind, id: peerId },
40
- dmScope: cfg.session?.dmScope,
41
- identityLinks: cfg.session?.identityLinks,
42
- })
43
- ).toLowerCase();
42
+ if (typeof routing.buildAgentSessionKey !== "function") {
43
+ throw new HostRoutingHelperUnavailableError();
44
44
  }
45
- // Fallback: derive a session key with agentId suffix to ensure isolation.
46
- // resolveAgentRoute routes to the default agent, so we append the target
47
- // agentId to prevent session key collisions between sub-agents.
48
- // @migration-note: When SDK exposes buildAgentSessionKey in type definitions,
49
- // sessions created via this fallback path will become orphaned. Remove this
50
- // fallback and the typeof check once the SDK is updated.
51
- const fallbackRoute = rt.channel.routing.resolveAgentRoute({
52
- cfg,
53
- channel: "dingtalk",
54
- accountId,
55
- peer: { kind: peerKind, id: peerId },
56
- });
57
- 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();
58
55
  }
59
56
 
60
57
  /**
@@ -131,8 +128,8 @@ export async function resolveSubAgentRoute(params: {
131
128
  await sendBySession(dingtalkConfig, sessionWebhook, `⚠️ ${fallbackReason}`, {
132
129
  ...sendOptions,
133
130
  });
134
- } catch (err: any) {
135
- 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)}`);
136
133
  }
137
134
  }
138
135
 
@@ -168,6 +165,7 @@ export async function dispatchSubAgents(params: {
168
165
  preDownloadedMedia = { mediaPath: media.path, mediaType: media.mimeType };
169
166
  }
170
167
  }
168
+ let helperMissingWarningSent = false;
171
169
 
172
170
  for (const agentMatch of matchedAgents) {
173
171
  try {
@@ -186,9 +184,27 @@ export async function dispatchSubAgents(params: {
186
184
  preDownloadedMedia,
187
185
  });
188
186
  } catch (error) {
187
+ const message = getErrorMessage(error);
189
188
  log?.error?.(
190
- `[DingTalk] Sub-agent ${agentMatch.agentId} failed: ${error instanceof Error ? error.message : String(error)}`,
189
+ `[DingTalk] Sub-agent ${agentMatch.agentId} failed: ${message}`,
191
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
+ }
192
208
  }
193
209
  }
194
210
  }
package/src/types.ts CHANGED
@@ -19,12 +19,13 @@ import type {
19
19
  ChannelLogSink as SDKChannelLogSink,
20
20
  } from "openclaw/plugin-sdk/channel-runtime";
21
21
  import type { ChannelSetupWizard } from "openclaw/plugin-sdk/setup";
22
- import { mergeAccountWithDefaults } from "./config";
23
22
 
24
23
  export type AckReactionMode = "off" | "emoji" | "kaomoji";
25
24
  // Accept arbitrary strings for backward compatibility; the recommended
26
25
  // explicit modes remain: "off" | "emoji" | "kaomoji".
27
26
  export type AckReactionConfigValue = string;
27
+ export type CardStreamingMode = "off" | "answer" | "all";
28
+ export type ContextVisibilityMode = "all" | "allowlist" | "allowlist_quote";
28
29
 
29
30
  /**
30
31
  * DingTalk channel configuration (extends base OpenClaw config)
@@ -39,6 +40,7 @@ export interface DingTalkConfig extends OpenClawConfig {
39
40
  allowFrom?: string[];
40
41
  groupAllowFrom?: string[];
41
42
  displayNameResolution?: "disabled" | "all";
43
+ contextVisibility?: ContextVisibilityMode;
42
44
  mediaUrlAllowlist?: string[];
43
45
  journalTTLDays?: number;
44
46
  ackReaction?: AckReactionConfigValue;
@@ -71,8 +73,15 @@ export interface DingTalkConfig extends OpenClawConfig {
71
73
  enabled?: boolean;
72
74
  cooldownHours?: number;
73
75
  };
74
- /** Enable real-time card streaming (default false, true = 300ms throttled per-token updates) */
76
+ /** Card streaming mode.
77
+ * - off: disable incremental streaming
78
+ * - answer: stream answer text
79
+ * - all: stream answer + reasoning text */
80
+ cardStreamingMode?: CardStreamingMode;
81
+ /** @deprecated Use `cardStreamingMode` instead. */
75
82
  cardRealTimeStream?: boolean;
83
+ /** Throttle interval in ms for card stream updates (default 1000) */
84
+ cardStreamInterval?: number;
76
85
  /** AICard degrade duration in milliseconds after trigger errors (default 30m) */
77
86
  aicardDegradeMs?: number;
78
87
  /** Enable local learning loop (events/reflections/session notes/global rules) */
@@ -85,6 +94,15 @@ export interface DingTalkConfig extends OpenClawConfig {
85
94
  convertMarkdownTables?: boolean;
86
95
  /** @mention the sender after card finalization in group chats; value is the message text */
87
96
  cardAtSender?: string;
97
+ /** Status line visibility toggles for the AI card footer */
98
+ cardStatusLine?: {
99
+ model?: boolean;
100
+ effort?: boolean;
101
+ agent?: boolean;
102
+ taskTime?: boolean;
103
+ tokens?: boolean;
104
+ dapiUsage?: boolean;
105
+ };
88
106
  }
89
107
 
90
108
  /**
@@ -100,6 +118,7 @@ export interface DingTalkChannelConfig {
100
118
  allowFrom?: string[];
101
119
  groupAllowFrom?: string[];
102
120
  displayNameResolution?: "disabled" | "all";
121
+ contextVisibility?: ContextVisibilityMode;
103
122
  mediaUrlAllowlist?: string[];
104
123
  journalTTLDays?: number;
105
124
  ackReaction?: AckReactionConfigValue;
@@ -131,8 +150,15 @@ export interface DingTalkChannelConfig {
131
150
  enabled?: boolean;
132
151
  cooldownHours?: number;
133
152
  };
134
- /** Enable real-time card streaming (default false, true = 300ms throttled per-token updates) */
153
+ /** Card streaming mode.
154
+ * - off: disable incremental streaming
155
+ * - answer: stream answer text
156
+ * - all: stream answer + reasoning text */
157
+ cardStreamingMode?: CardStreamingMode;
158
+ /** @deprecated Use `cardStreamingMode` instead. */
135
159
  cardRealTimeStream?: boolean;
160
+ /** Throttle interval in ms for card stream updates (default 1000) */
161
+ cardStreamInterval?: number;
136
162
  /** AICard degrade duration in milliseconds after trigger errors (default 30m) */
137
163
  aicardDegradeMs?: number;
138
164
  /** Enable local learning loop (events/reflections/session notes/global rules) */
@@ -234,6 +260,10 @@ export interface DingTalkInboundMessage {
234
260
  atName?: string;
235
261
  downloadCode?: string;
236
262
  }>;
263
+ /** chatRecord 消息摘要 */
264
+ summary?: string;
265
+ /** chatRecord 消息标题 */
266
+ title?: string;
237
267
  };
238
268
  };
239
269
  };
@@ -243,6 +273,8 @@ export interface DingTalkInboundMessage {
243
273
  recognition?: string;
244
274
  spaceId?: string;
245
275
  fileId?: string;
276
+ text?: string;
277
+ title?: string;
246
278
  biz_custom_action_url?: string;
247
279
  richText?: Array<{
248
280
  type: string;
@@ -646,8 +678,11 @@ export interface AICardInstance {
646
678
  lastUpdated: number;
647
679
  state: AICardState; // Current card state: PROCESSING, INPUTING, FINISHED, STOPPED, FAILED
648
680
  config?: DingTalkConfig; // Store config reference for token refresh
649
- lastStreamedContent?: string;
681
+ lastStreamedContent?: string; // Latest copy/content text shown to user
682
+ lastBlockListJson?: string; // Latest rendered CardBlock[] JSON for V2 instances API
650
683
  outTrackId?: string;
684
+ /** Cumulative DingTalk API call count for this card instance. */
685
+ dapiUsage?: number;
651
686
  }
652
687
 
653
688
  /**
@@ -707,118 +742,15 @@ export interface ConnectionAttemptResult {
707
742
  error?: Error;
708
743
  nextDelay?: number;
709
744
  }
710
-
711
- // ============ Onboarding Helper Functions ============
712
-
713
- const DEFAULT_ACCOUNT_ID = "default";
714
-
715
- /**
716
- * List all DingTalk account IDs from config
717
- */
718
- export function listDingTalkAccountIds(cfg: OpenClawConfig): string[] {
719
- const dingtalk = cfg.channels?.dingtalk as DingTalkChannelConfig | undefined;
720
- if (!dingtalk) {
721
- return [];
722
- }
723
-
724
- const accountIds: string[] = [];
725
-
726
- // Check for direct configuration (default account)
727
- if (dingtalk.clientId || dingtalk.clientSecret) {
728
- accountIds.push(DEFAULT_ACCOUNT_ID);
729
- }
730
-
731
- // Check accounts object
732
- if (dingtalk.accounts) {
733
- accountIds.push(...Object.keys(dingtalk.accounts));
734
- }
735
-
736
- return accountIds;
737
- }
738
-
739
745
  /**
740
- * Resolved DingTalk account with configuration status
746
+ * CardBlock types for AI Card v2 blockList.
747
+ * - 0 = answer (markdown text)
748
+ * - 1 = thinking process
749
+ * - 2 = tool result
750
+ * - 3 = image (uploaded mediaId)
741
751
  */
742
- export interface ResolvedDingTalkAccount extends DingTalkConfig {
743
- accountId: string;
744
- configured: boolean;
745
- }
746
-
747
- /**
748
- * Resolve a specific DingTalk account configuration
749
- */
750
- export function resolveDingTalkAccount(
751
- cfg: OpenClawConfig,
752
- accountId?: string | null,
753
- ): ResolvedDingTalkAccount {
754
- const id = accountId || DEFAULT_ACCOUNT_ID;
755
- const dingtalk = cfg.channels?.dingtalk as DingTalkChannelConfig | undefined;
756
-
757
- // If default account, return top-level config
758
- if (id === DEFAULT_ACCOUNT_ID) {
759
- const config: DingTalkConfig = {
760
- clientId: dingtalk?.clientId ?? "",
761
- clientSecret: dingtalk?.clientSecret ?? "",
762
- name: dingtalk?.name,
763
- enabled: dingtalk?.enabled,
764
- dmPolicy: dingtalk?.dmPolicy,
765
- groupPolicy: dingtalk?.groupPolicy,
766
- allowFrom: dingtalk?.allowFrom,
767
- groupAllowFrom: dingtalk?.groupAllowFrom,
768
- displayNameResolution: dingtalk?.displayNameResolution,
769
- journalTTLDays: dingtalk?.journalTTLDays,
770
- ackReaction: dingtalk?.ackReaction,
771
- debug: dingtalk?.debug,
772
- messageType: dingtalk?.messageType,
773
- cardTemplateId: dingtalk?.cardTemplateId,
774
- cardTemplateKey: dingtalk?.cardTemplateKey,
775
- groups: dingtalk?.groups,
776
- accounts: dingtalk?.accounts,
777
- maxConnectionAttempts: dingtalk?.maxConnectionAttempts,
778
- initialReconnectDelay: dingtalk?.initialReconnectDelay,
779
- maxReconnectDelay: dingtalk?.maxReconnectDelay,
780
- reconnectJitter: dingtalk?.reconnectJitter,
781
- maxReconnectCycles: dingtalk?.maxReconnectCycles,
782
- reconnectDeadlineMs: dingtalk?.reconnectDeadlineMs,
783
- useConnectionManager: dingtalk?.useConnectionManager,
784
- mediaMaxMb: dingtalk?.mediaMaxMb,
785
- keepAlive: dingtalk?.keepAlive,
786
- bypassProxyForSend: dingtalk?.bypassProxyForSend,
787
- proactivePermissionHint: dingtalk?.proactivePermissionHint,
788
- cardRealTimeStream: dingtalk?.cardRealTimeStream,
789
- aicardDegradeMs: dingtalk?.aicardDegradeMs,
790
- learningEnabled: dingtalk?.learningEnabled,
791
- learningAutoApply: dingtalk?.learningAutoApply,
792
- learningNoteTtlMs: dingtalk?.learningNoteTtlMs,
793
- convertMarkdownTables: dingtalk?.convertMarkdownTables,
794
- cardAtSender: dingtalk?.cardAtSender,
795
- };
796
- return {
797
- ...config,
798
- accountId: id,
799
- configured: Boolean(config.clientId && config.clientSecret),
800
- };
801
- }
802
-
803
- // If named account, merge channel-level defaults with account-level overrides
804
- const accountConfig = dingtalk?.accounts?.[id];
805
- if (accountConfig) {
806
- const merged = mergeAccountWithDefaults(
807
- dingtalk as DingTalkConfig,
808
- accountConfig,
809
- );
810
- return {
811
- ...merged,
812
- accountId: id,
813
- configured: Boolean(merged.clientId && merged.clientSecret),
814
- };
815
- }
816
-
817
- // Account doesn't exist, return empty config
818
- return {
819
- clientId: "",
820
- clientSecret: "",
821
- accountId: id,
822
- configured: false,
823
- };
824
- }
752
+ export type CardBlock =
753
+ | { type: 0; markdown: string }
754
+ | { type: 1; markdown: string }
755
+ | { type: 2; markdown: string }
756
+ | { type: 3; mediaId: string; text?: string };
package/src/utils.ts CHANGED
@@ -224,6 +224,31 @@ export function stringifyUnknown(value: unknown): string {
224
224
  }
225
225
  }
226
226
 
227
+ export function parseBooleanLike(value: unknown): boolean | undefined {
228
+ if (typeof value === "boolean") {
229
+ return value;
230
+ }
231
+ if (typeof value === "number") {
232
+ if (value === 1) {
233
+ return true;
234
+ }
235
+ if (value === 0) {
236
+ return false;
237
+ }
238
+ return undefined;
239
+ }
240
+ if (typeof value === "string") {
241
+ const normalized = value.trim().toLowerCase();
242
+ if (["1", "true", "yes", "y", "on"].includes(normalized)) {
243
+ return true;
244
+ }
245
+ if (["0", "false", "no", "n", "off"].includes(normalized)) {
246
+ return false;
247
+ }
248
+ }
249
+ return undefined;
250
+ }
251
+
227
252
  export function getErrorMessage(err: unknown): string {
228
253
  if (err instanceof Error && err.message) {
229
254
  return err.message;