@soimy/dingtalk 3.5.3 → 3.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +4 -1
  2. package/index.ts +7 -0
  3. package/openclaw.plugin.json +153 -5
  4. package/package.json +1 -1
  5. package/src/auth.ts +5 -2
  6. package/src/card/card-markdown-image-reroute.ts +106 -0
  7. package/src/card/card-run-registry.ts +54 -1
  8. package/src/card/card-stop-handler.ts +10 -20
  9. package/src/card/card-template.ts +14 -3
  10. package/src/card/statusline-renderer.ts +94 -0
  11. package/src/card-draft-controller.ts +245 -52
  12. package/src/card-service.ts +408 -23
  13. package/src/channel.ts +24 -1083
  14. package/src/config-schema.ts +21 -1
  15. package/src/config.ts +139 -66
  16. package/src/device-registration.ts +245 -0
  17. package/src/gateway/channel-gateway.ts +637 -0
  18. package/src/inbound-handler.ts +1276 -975
  19. package/src/media-utils.ts +6 -0
  20. package/src/message-context-store.ts +183 -85
  21. package/src/message-utils.ts +124 -16
  22. package/src/messaging/btw-deliver.ts +85 -0
  23. package/src/messaging/channel-actions.ts +174 -0
  24. package/src/messaging/channel-outbound.ts +158 -0
  25. package/src/onboarding.ts +333 -235
  26. package/src/path-utils.ts +49 -0
  27. package/src/platform/channel-status.ts +81 -0
  28. package/src/reply-strategy-card.ts +373 -64
  29. package/src/reply-strategy-markdown.ts +1 -1
  30. package/src/reply-strategy-types.ts +93 -0
  31. package/src/reply-strategy-with-reaction.ts +1 -1
  32. package/src/reply-strategy.ts +14 -72
  33. package/src/run-usage-store.ts +59 -0
  34. package/src/secret-input.ts +216 -0
  35. package/src/send-service.ts +115 -3
  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 +30 -5
  39. package/src/types.ts +48 -157
@@ -146,3 +146,31 @@ export function resolveAtAgents(
146
146
  hasInvalidAgentNames,
147
147
  };
148
148
  }
149
+
150
+ /**
151
+ * Get agent display name for statusLine in card template.
152
+ *
153
+ * Priority:
154
+ * 1. subAgentOptions.matchedName - user-friendly name from @mention (e.g., "代码专家")
155
+ * 2. agents.list lookup - name field from agent config
156
+ * 3. agentId fallback - technical identifier
157
+ */
158
+ export function getAgentDisplayName(params: {
159
+ subAgentOptions?: { matchedName?: string };
160
+ agentId: string;
161
+ agentsList?: Array<{ id: string; name?: string }>;
162
+ }): string {
163
+ // Priority 1: sub-agent matchedName (user-friendly)
164
+ if (params.subAgentOptions?.matchedName) {
165
+ return params.subAgentOptions.matchedName;
166
+ }
167
+ // Priority 2: lookup from agents.list
168
+ if (params.agentsList) {
169
+ const agent = params.agentsList.find((a) => a.id === params.agentId);
170
+ if (agent?.name) {
171
+ return agent.name;
172
+ }
173
+ }
174
+ // Priority 3: fallback to agentId
175
+ return params.agentId;
176
+ }
@@ -158,11 +158,36 @@ export async function dispatchSubAgents(params: {
158
158
  const { matchedAgents, cfg, accountId, data, dingtalkConfig, sessionWebhook, extractedContent, handleMessage, downloadMedia: download, log } = params;
159
159
 
160
160
  // Pre-download media once to avoid duplication across sub-agents
161
- let preDownloadedMedia: { mediaPath?: string; mediaType?: string } | undefined;
162
- if (extractedContent.mediaPath && resolveRobotCode(dingtalkConfig)) {
163
- const media = await download(dingtalkConfig, extractedContent.mediaPath, log);
164
- if (media) {
165
- preDownloadedMedia = { mediaPath: media.path, mediaType: media.mimeType };
161
+ let preDownloadedMedia: {
162
+ mediaPath?: string;
163
+ mediaType?: string;
164
+ mediaPaths?: string[];
165
+ mediaTypes?: string[];
166
+ } | undefined;
167
+ const robotCode = resolveRobotCode(dingtalkConfig);
168
+ if (robotCode) {
169
+ const downloadCodes =
170
+ extractedContent.mediaPaths && extractedContent.mediaPaths.length > 0
171
+ ? extractedContent.mediaPaths
172
+ : extractedContent.mediaPath
173
+ ? [extractedContent.mediaPath]
174
+ : [];
175
+ const mediaPaths: string[] = [];
176
+ const mediaTypes: string[] = [];
177
+ for (const downloadCode of downloadCodes) {
178
+ const media = await download(dingtalkConfig, downloadCode, log);
179
+ if (media) {
180
+ mediaPaths.push(media.path);
181
+ mediaTypes.push(media.mimeType);
182
+ }
183
+ }
184
+ if (mediaPaths.length > 0) {
185
+ preDownloadedMedia = {
186
+ mediaPath: mediaPaths[0],
187
+ mediaType: mediaTypes[0],
188
+ mediaPaths,
189
+ mediaTypes,
190
+ };
166
191
  }
167
192
  }
168
193
  let helperMissingWarningSent = false;
package/src/types.ts CHANGED
@@ -9,17 +9,14 @@
9
9
  * - Session and token management
10
10
  */
11
11
 
12
- import type {
13
- ChannelPlugin as SDKChannelPlugin,
14
- OpenClawConfig,
15
- } from "openclaw/plugin-sdk/core";
16
12
  import type {
17
13
  ChannelAccountSnapshot as SDKChannelAccountSnapshot,
18
14
  ChannelGatewayContext as SDKChannelGatewayContext,
19
15
  ChannelLogSink as SDKChannelLogSink,
20
16
  } from "openclaw/plugin-sdk/channel-runtime";
17
+ import type { ChannelPlugin as SDKChannelPlugin, OpenClawConfig } from "openclaw/plugin-sdk/core";
21
18
  import type { ChannelSetupWizard } from "openclaw/plugin-sdk/setup";
22
- import { mergeAccountWithDefaults } from "./config";
19
+ import type { SecretInput } from "./secret-input";
23
20
 
24
21
  export type AckReactionMode = "off" | "emoji" | "kaomoji";
25
22
  // Accept arbitrary strings for backward compatibility; the recommended
@@ -33,7 +30,7 @@ export type ContextVisibilityMode = "all" | "allowlist" | "allowlist_quote";
33
30
  */
34
31
  export interface DingTalkConfig extends OpenClawConfig {
35
32
  clientId: string;
36
- clientSecret: string;
33
+ clientSecret: SecretInput;
37
34
  name?: string;
38
35
  enabled?: boolean;
39
36
  dmPolicy?: "open" | "pairing" | "allowlist";
@@ -51,7 +48,10 @@ export interface DingTalkConfig extends OpenClawConfig {
51
48
  cardTemplateId?: string;
52
49
  /** @deprecated 已固定使用内置模板契约 */
53
50
  cardTemplateKey?: string;
54
- groups?: Record<string, { systemPrompt?: string; requireMention?: boolean; groupAllowFrom?: string[] }>;
51
+ groups?: Record<
52
+ string,
53
+ { systemPrompt?: string; requireMention?: boolean; groupAllowFrom?: string[] }
54
+ >;
55
55
  accounts?: Record<string, DingTalkConfig>;
56
56
  // Connection robustness configuration
57
57
  maxConnectionAttempts?: number;
@@ -95,6 +95,15 @@ export interface DingTalkConfig extends OpenClawConfig {
95
95
  convertMarkdownTables?: boolean;
96
96
  /** @mention the sender after card finalization in group chats; value is the message text */
97
97
  cardAtSender?: string;
98
+ /** Status line visibility toggles for the AI card footer */
99
+ cardStatusLine?: {
100
+ model?: boolean;
101
+ effort?: boolean;
102
+ agent?: boolean;
103
+ taskTime?: boolean;
104
+ tokens?: boolean;
105
+ dapiUsage?: boolean;
106
+ };
98
107
  }
99
108
 
100
109
  /**
@@ -103,7 +112,7 @@ export interface DingTalkConfig extends OpenClawConfig {
103
112
  export interface DingTalkChannelConfig {
104
113
  enabled?: boolean;
105
114
  clientId: string;
106
- clientSecret: string;
115
+ clientSecret: SecretInput;
107
116
  name?: string;
108
117
  dmPolicy?: "open" | "pairing" | "allowlist";
109
118
  groupPolicy?: "open" | "allowlist" | "disabled";
@@ -120,7 +129,10 @@ export interface DingTalkChannelConfig {
120
129
  cardTemplateId?: string;
121
130
  /** @deprecated 已固定使用内置模板契约 */
122
131
  cardTemplateKey?: string;
123
- groups?: Record<string, { systemPrompt?: string; requireMention?: boolean; groupAllowFrom?: string[] }>;
132
+ groups?: Record<
133
+ string,
134
+ { systemPrompt?: string; requireMention?: boolean; groupAllowFrom?: string[] }
135
+ >;
124
136
  accounts?: Record<string, DingTalkConfig>;
125
137
  maxConnectionAttempts?: number;
126
138
  initialReconnectDelay?: number;
@@ -298,7 +310,12 @@ export interface DingTalkInboundMessage {
298
310
  sessionWebhook: string;
299
311
  }
300
312
 
301
- export type QuotedRefKey = "msgId" | "processQueryKey" | "messageId" | "outTrackId" | "cardInstanceId";
313
+ export type QuotedRefKey =
314
+ | "msgId"
315
+ | "processQueryKey"
316
+ | "messageId"
317
+ | "outTrackId"
318
+ | "cardInstanceId";
302
319
 
303
320
  export type AttachmentTextSource = "text" | "html" | "pdf" | "docx";
304
321
 
@@ -460,6 +477,8 @@ export interface HandleDingTalkMessageParams {
460
477
  preDownloadedMedia?: {
461
478
  mediaPath?: string;
462
479
  mediaType?: string;
480
+ mediaPaths?: string[];
481
+ mediaTypes?: string[];
463
482
  };
464
483
  }
465
484
 
@@ -636,7 +655,9 @@ export interface DingTalkOutboundHandler {
636
655
  deliveryMode: "direct" | "queued" | "batch";
637
656
  resolveTarget: (params: ResolveTargetParams) => TargetResolutionResult;
638
657
  sendText: (params: SendTextParams) => Promise<{ ok: boolean; data?: unknown; error?: unknown }>;
639
- sendMedia?: (params: SendMediaParams) => Promise<{ ok: boolean; data?: unknown; error?: unknown }>;
658
+ sendMedia?: (
659
+ params: SendMediaParams,
660
+ ) => Promise<{ ok: boolean; data?: unknown; error?: unknown }>;
640
661
  }
641
662
 
642
663
  /**
@@ -670,8 +691,13 @@ export interface AICardInstance {
670
691
  lastUpdated: number;
671
692
  state: AICardState; // Current card state: PROCESSING, INPUTING, FINISHED, STOPPED, FAILED
672
693
  config?: DingTalkConfig; // Store config reference for token refresh
673
- lastStreamedContent?: string;
694
+ lastStreamedContent?: string; // Latest copy/content text shown to user
695
+ lastBlockListJson?: string; // Latest rendered CardBlock[] JSON for V2 instances API
674
696
  outTrackId?: string;
697
+ /** Cumulative DingTalk API call count for this card instance. */
698
+ dapiUsage?: number;
699
+ /** True after this card has successfully opened DingTalk's streaming lifecycle. */
700
+ streamLifecycleOpened?: boolean;
675
701
  }
676
702
 
677
703
  /**
@@ -731,150 +757,15 @@ export interface ConnectionAttemptResult {
731
757
  error?: Error;
732
758
  nextDelay?: number;
733
759
  }
734
-
735
- // ============ Onboarding Helper Functions ============
736
-
737
- const DEFAULT_ACCOUNT_ID = "default";
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
-
766
- /**
767
- * List all DingTalk account IDs from config
768
- */
769
- export function listDingTalkAccountIds(cfg: OpenClawConfig): string[] {
770
- const dingtalk = cfg.channels?.dingtalk as DingTalkChannelConfig | undefined;
771
- if (!dingtalk) {
772
- return [];
773
- }
774
-
775
- const accountIds: string[] = [];
776
-
777
- // Check for direct configuration (default account)
778
- if (dingtalk.clientId || dingtalk.clientSecret) {
779
- accountIds.push(DEFAULT_ACCOUNT_ID);
780
- }
781
-
782
- // Check accounts object
783
- if (dingtalk.accounts) {
784
- accountIds.push(...Object.keys(dingtalk.accounts));
785
- }
786
-
787
- return accountIds;
788
- }
789
-
790
760
  /**
791
- * Resolved DingTalk account with configuration status
761
+ * CardBlock types for AI Card v2 blockList.
762
+ * - 0 = answer (markdown text)
763
+ * - 1 = thinking process
764
+ * - 2 = tool result
765
+ * - 3 = image (uploaded mediaId)
792
766
  */
793
- export interface ResolvedDingTalkAccount extends DingTalkConfig {
794
- accountId: string;
795
- configured: boolean;
796
- }
797
-
798
- /**
799
- * Resolve a specific DingTalk account configuration
800
- */
801
- export function resolveDingTalkAccount(
802
- cfg: OpenClawConfig,
803
- accountId?: string | null,
804
- ): ResolvedDingTalkAccount {
805
- const id = accountId || DEFAULT_ACCOUNT_ID;
806
- const dingtalk = cfg.channels?.dingtalk as DingTalkChannelConfig | undefined;
807
-
808
- // If default account, return top-level config
809
- if (id === DEFAULT_ACCOUNT_ID) {
810
- const rawConfig: DingTalkConfig = {
811
- clientId: dingtalk?.clientId ?? "",
812
- clientSecret: dingtalk?.clientSecret ?? "",
813
- name: dingtalk?.name,
814
- enabled: dingtalk?.enabled,
815
- dmPolicy: dingtalk?.dmPolicy,
816
- groupPolicy: dingtalk?.groupPolicy,
817
- allowFrom: dingtalk?.allowFrom,
818
- groupAllowFrom: dingtalk?.groupAllowFrom,
819
- displayNameResolution: dingtalk?.displayNameResolution,
820
- contextVisibility: dingtalk?.contextVisibility,
821
- journalTTLDays: dingtalk?.journalTTLDays,
822
- ackReaction: dingtalk?.ackReaction,
823
- debug: dingtalk?.debug,
824
- messageType: dingtalk?.messageType,
825
- cardTemplateId: dingtalk?.cardTemplateId,
826
- cardTemplateKey: dingtalk?.cardTemplateKey,
827
- groups: dingtalk?.groups,
828
- accounts: dingtalk?.accounts,
829
- maxConnectionAttempts: dingtalk?.maxConnectionAttempts,
830
- initialReconnectDelay: dingtalk?.initialReconnectDelay,
831
- maxReconnectDelay: dingtalk?.maxReconnectDelay,
832
- reconnectJitter: dingtalk?.reconnectJitter,
833
- maxReconnectCycles: dingtalk?.maxReconnectCycles,
834
- reconnectDeadlineMs: dingtalk?.reconnectDeadlineMs,
835
- useConnectionManager: dingtalk?.useConnectionManager,
836
- mediaMaxMb: dingtalk?.mediaMaxMb,
837
- keepAlive: dingtalk?.keepAlive,
838
- bypassProxyForSend: dingtalk?.bypassProxyForSend,
839
- proactivePermissionHint: dingtalk?.proactivePermissionHint,
840
- cardStreamingMode: dingtalk?.cardStreamingMode,
841
- cardRealTimeStream: dingtalk?.cardRealTimeStream,
842
- cardStreamInterval: dingtalk?.cardStreamInterval,
843
- aicardDegradeMs: dingtalk?.aicardDegradeMs,
844
- learningEnabled: dingtalk?.learningEnabled,
845
- learningAutoApply: dingtalk?.learningAutoApply,
846
- learningNoteTtlMs: dingtalk?.learningNoteTtlMs,
847
- convertMarkdownTables: dingtalk?.convertMarkdownTables,
848
- cardAtSender: dingtalk?.cardAtSender,
849
- };
850
- const config = stripRemovedLegacyFieldsFromPublicAccount(rawConfig);
851
- return {
852
- ...config,
853
- accountId: id,
854
- configured: Boolean(config.clientId && config.clientSecret),
855
- };
856
- }
857
-
858
- // If named account, merge channel-level defaults with account-level overrides
859
- const accountConfig = dingtalk?.accounts?.[id];
860
- if (accountConfig) {
861
- const merged = mergeAccountWithDefaults(
862
- dingtalk as DingTalkConfig,
863
- accountConfig,
864
- );
865
- const publicMerged = stripRemovedLegacyFieldsFromPublicAccount(merged);
866
- return {
867
- ...publicMerged,
868
- accountId: id,
869
- configured: Boolean(merged.clientId && merged.clientSecret),
870
- };
871
- }
872
-
873
- // Account doesn't exist, return empty config
874
- return {
875
- clientId: "",
876
- clientSecret: "",
877
- accountId: id,
878
- configured: false,
879
- };
880
- }
767
+ export type CardBlock =
768
+ | { type: 0; markdown: string }
769
+ | { type: 1; markdown: string }
770
+ | { type: 2; markdown: string }
771
+ | { type: 3; mediaId: string; text?: string };