@soimy/dingtalk 3.1.4 → 3.3.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.
@@ -1,6 +1,7 @@
1
1
  import { z } from "zod";
2
+ import { DEFAULT_JOURNAL_TTL_DAYS } from "./quote-journal";
2
3
 
3
- const DingTalkAccountConfigSchema = z.object({
4
+ const DingTalkAccountConfigShape = {
4
5
  /** Account name (optional display name) */
5
6
  name: z.string().optional(),
6
7
 
@@ -31,9 +32,12 @@ const DingTalkAccountConfigSchema = z.object({
31
32
  /** List of allowed user IDs for allowlist policy */
32
33
  allowFrom: z.array(z.string()).optional(),
33
34
 
34
- /** Show thinking indicator while processing */
35
- showThinking: z.boolean().optional().default(true),
35
+ mediaUrlAllowlist: z.array(z.string()).optional(),
36
36
 
37
+ /** Official OpenClaw ackReaction entry for processing feedback; empty string disables it */
38
+ ackReaction: z.string().optional(),
39
+
40
+ journalTTLDays: z.number().int().min(1).optional().default(DEFAULT_JOURNAL_TTL_DAYS),
37
41
  /** Enable debug logging */
38
42
  debug: z.boolean().optional().default(false),
39
43
 
@@ -47,7 +51,7 @@ const DingTalkAccountConfigSchema = z.object({
47
51
  cardTemplateId: z.string().optional(),
48
52
 
49
53
  /** Card template key for streaming updates
50
- * Default: 'msgContent' - maps to the content field in the card template
54
+ * Default: 'content' - maps to the content field in the card template
51
55
  * This key is used in the streaming API to update specific fields in the card.
52
56
  */
53
57
  cardTemplateKey: z.string().optional().default("content"),
@@ -79,6 +83,19 @@ const DingTalkAccountConfigSchema = z.object({
79
83
  /** Maximum number of runtime reconnect cycles before giving up (default: 10) */
80
84
  maxReconnectCycles: z.number().int().min(1).optional().default(10),
81
85
 
86
+ /** Maximum time (ms) for a single reconnect cycle before starting a new cycle (default: 50000) */
87
+ reconnectDeadlineMs: z.number().int().min(5000).optional().default(50000),
88
+
89
+ /** Whether to use ConnectionManager (default: true). When false, rely on DWClient native keepAlive+autoReconnect. */
90
+ useConnectionManager: z.boolean().optional().default(true),
91
+
92
+ /** Maximum inbound media file size in MB (overrides runtime default when set) */
93
+ mediaMaxMb: z.number().int().min(1).optional(),
94
+
95
+ /** Whether to enable underlying stream keepAlive heartbeat; defaults to !useConnectionManager when omitted */
96
+ keepAlive: z.boolean().optional(),
97
+ /** Bypass system/global HTTP(S) proxy for DingTalk outbound send/card/upload APIs */
98
+ bypassProxyForSend: z.boolean().optional().default(false),
82
99
  proactivePermissionHint: z
83
100
  .object({
84
101
  enabled: z.boolean().optional().default(true),
@@ -86,7 +103,34 @@ const DingTalkAccountConfigSchema = z.object({
86
103
  })
87
104
  .optional()
88
105
  .default({ enabled: true, cooldownHours: 24 }),
89
- });
106
+
107
+ /** Enable real-time card streaming (default: false).
108
+ * When true, card updates are streamed per-token with 300ms throttle for a smoother experience, at the cost of more API calls. */
109
+ cardRealTimeStream: z.boolean().optional().default(false),
110
+
111
+ /** AICard degrade duration in milliseconds after trigger errors (default: 30 minutes) */
112
+ aicardDegradeMs: z.number().int().min(60_000).optional().default(30 * 60 * 1000),
113
+
114
+ /** Enable local learning loop (default: false) */
115
+ learningEnabled: z.boolean().optional(),
116
+
117
+ /** Auto-apply generated reflections into session notes/global rules (default: false) */
118
+ learningAutoApply: z.boolean().optional(),
119
+
120
+ /** Session learning note TTL in milliseconds (default: 6 hours) */
121
+ learningNoteTtlMs: z.number().int().min(60_000).optional(),
122
+
123
+ /** @deprecated Use learningEnabled */
124
+ feedbackLearningEnabled: z.boolean().optional(),
125
+
126
+ /** @deprecated Use learningAutoApply */
127
+ feedbackLearningAutoApply: z.boolean().optional(),
128
+
129
+ /** @deprecated Use learningNoteTtlMs */
130
+ feedbackLearningNoteTtlMs: z.number().int().min(60_000).optional(),
131
+ } as const;
132
+
133
+ const DingTalkAccountConfigSchema = z.object(DingTalkAccountConfigShape);
90
134
 
91
135
  /**
92
136
  * DingTalk configuration schema using Zod
package/src/config.ts CHANGED
@@ -3,8 +3,61 @@ import * as path from "node:path";
3
3
  import type { OpenClawConfig } from "openclaw/plugin-sdk";
4
4
  import type { DingTalkConfig } from "./types";
5
5
 
6
+ const WINDOWS_ROOT_DIRECTORIES = new Set([
7
+ "Users",
8
+ "Program Files",
9
+ "Program Files (x86)",
10
+ "ProgramData",
11
+ "Windows",
12
+ "Documents and Settings",
13
+ ]);
14
+ const DEFAULT_LEARNING_NOTE_TTL_MS = 6 * 60 * 60 * 1000;
15
+
16
+ function normalizeLearningConfig(
17
+ config: DingTalkConfig,
18
+ options: { applyDefaults: boolean },
19
+ ): DingTalkConfig {
20
+ const learningEnabled = config.learningEnabled ?? config.feedbackLearningEnabled;
21
+ const learningAutoApply = config.learningAutoApply ?? config.feedbackLearningAutoApply;
22
+ const learningNoteTtlMs = config.learningNoteTtlMs ?? config.feedbackLearningNoteTtlMs;
23
+ return {
24
+ ...config,
25
+ learningEnabled: options.applyDefaults ? learningEnabled ?? false : learningEnabled,
26
+ learningAutoApply: options.applyDefaults ? learningAutoApply ?? false : learningAutoApply,
27
+ learningNoteTtlMs: options.applyDefaults
28
+ ? learningNoteTtlMs ?? DEFAULT_LEARNING_NOTE_TTL_MS
29
+ : learningNoteTtlMs,
30
+ };
31
+ }
32
+
33
+ /**
34
+ * Merge channel-level defaults into an account-specific config.
35
+ * Account-level values take precedence; `accounts` key is excluded to avoid recursion.
36
+ */
37
+ export function mergeAccountWithDefaults(
38
+ channelCfg: DingTalkConfig,
39
+ accountCfg: DingTalkConfig,
40
+ ): DingTalkConfig {
41
+ const { accounts: _accounts, ...defaults } = channelCfg;
42
+ const normalizedAccountCfg = normalizeLearningConfig(accountCfg, { applyDefaults: false });
43
+ const overrides: Partial<DingTalkConfig> = {};
44
+ for (const [key, value] of Object.entries(normalizedAccountCfg)) {
45
+ if (value !== undefined) {
46
+ Object.assign(overrides, { [key]: value });
47
+ }
48
+ }
49
+ return normalizeLearningConfig(
50
+ {
51
+ ...defaults,
52
+ ...overrides,
53
+ },
54
+ { applyDefaults: true },
55
+ );
56
+ }
57
+
6
58
  /**
7
59
  * Resolve DingTalk config for an account.
60
+ * Named accounts inherit channel-level defaults with account-level overrides.
8
61
  * Falls back to top-level config for single-account setups.
9
62
  */
10
63
  export function getConfig(cfg: OpenClawConfig, accountId?: string): DingTalkConfig {
@@ -14,10 +67,18 @@ export function getConfig(cfg: OpenClawConfig, accountId?: string): DingTalkConf
14
67
  }
15
68
 
16
69
  if (accountId && dingtalkCfg.accounts?.[accountId]) {
17
- return dingtalkCfg.accounts[accountId];
70
+ return mergeAccountWithDefaults(dingtalkCfg, dingtalkCfg.accounts[accountId]);
18
71
  }
19
72
 
20
- return dingtalkCfg;
73
+ if (accountId) {
74
+ return normalizeLearningConfig(dingtalkCfg, { applyDefaults: true });
75
+ }
76
+
77
+ if (dingtalkCfg.accounts && Object.keys(dingtalkCfg.accounts).length > 0) {
78
+ return dingtalkCfg;
79
+ }
80
+
81
+ return normalizeLearningConfig(dingtalkCfg, { applyDefaults: true });
21
82
  }
22
83
 
23
84
  export function isConfigured(cfg: OpenClawConfig, accountId?: string): boolean {
@@ -25,6 +86,19 @@ export function isConfigured(cfg: OpenClawConfig, accountId?: string): boolean {
25
86
  return Boolean(config.clientId && config.clientSecret);
26
87
  }
27
88
 
89
+ /**
90
+ * Resolve relative paths against a base directory, with intelligent platform-specific handling.
91
+ *
92
+ * Supports:
93
+ * - ~ and ~/ expansion to home directory
94
+ * - Absolute paths (Unix: /path, Windows: \path or C:\path)
95
+ * - Relative paths resolved against cwd
96
+ * - Windows absolute paths without drive letters (e.g., Users\name\.openclaw\file.txt)
97
+ * - Mixed path separators (/ and \)
98
+ *
99
+ * @param input - The path string to resolve
100
+ * @returns The resolved absolute path
101
+ */
28
102
  export function resolveRelativePath(input: string): string {
29
103
  const trimmed = input.trim();
30
104
  if (!trimmed) {
@@ -32,6 +106,8 @@ export function resolveRelativePath(input: string): string {
32
106
  }
33
107
 
34
108
  const segments = (value: string): string[] => value.split(/[\\/]+/).filter(Boolean);
109
+ const pathSegments = segments(trimmed);
110
+ const firstSegment = pathSegments[0];
35
111
 
36
112
  // Expand bare "~" and "~/" or "~\\" prefixes into the user home directory.
37
113
  if (trimmed === "~") {
@@ -41,13 +117,27 @@ export function resolveRelativePath(input: string): string {
41
117
  return path.resolve(os.homedir(), ...segments(trimmed.slice(2)));
42
118
  }
43
119
 
120
+ if (process.platform === "win32") {
121
+ // On Windows, OpenClaw may drop the leading "\" from root-based paths like
122
+ // "Users\name\.openclaw\workspace\file.xlsx". Only recover paths that start
123
+ // with well-known root directories to avoid misclassifying ordinary relative paths.
124
+ if (/^[a-zA-Z]:[\\/]/.test(trimmed)) {
125
+ return path.win32.normalize(trimmed);
126
+ }
127
+ if (firstSegment && /^[a-zA-Z]:$/.test(firstSegment)) {
128
+ return path.win32.resolve(`${firstSegment}\\`, ...pathSegments.slice(1));
129
+ }
130
+ if (firstSegment && WINDOWS_ROOT_DIRECTORIES.has(firstSegment)) {
131
+ return path.win32.resolve("\\", ...pathSegments);
132
+ }
133
+ }
44
134
  // Treat both "/" and "\\" as absolute root prefixes for cross-platform input.
45
135
  if (/^[\\/]/.test(trimmed)) {
46
- return path.resolve(path.sep, ...segments(trimmed));
136
+ return path.resolve(path.sep, ...pathSegments);
47
137
  }
48
138
 
49
139
  // Resolve relative path against cwd; supports mixed separators and "..\\..".
50
- return path.resolve(process.cwd(), ...segments(trimmed));
140
+ return path.resolve(process.cwd(), ...pathSegments);
51
141
  }
52
142
 
53
143
  export const resolveUserPath = resolveRelativePath;
@@ -64,6 +154,48 @@ export function resolveGroupConfig(
64
154
  return groups[groupId] || groups["*"] || undefined;
65
155
  }
66
156
 
157
+ function hasOwn(obj: unknown, key: string): boolean {
158
+ return typeof obj === "object" && obj !== null && Object.prototype.hasOwnProperty.call(obj, key);
159
+ }
160
+
161
+ function resolveAgentIdentityEmoji(cfg: OpenClawConfig, agentId?: string | null): string | undefined {
162
+ const targetAgentId = String(agentId || "").trim();
163
+ if (!targetAgentId) {
164
+ return undefined;
165
+ }
166
+ const agents = Array.isArray((cfg as any)?.agents?.list) ? (cfg as any).agents.list : [];
167
+ const agent = agents.find((entry: any) => String(entry?.id || "").trim() === targetAgentId);
168
+ const emoji = typeof agent?.identity?.emoji === "string" ? agent.identity.emoji.trim() : "";
169
+ return emoji || undefined;
170
+ }
171
+
172
+ export function resolveAckReactionSetting(params: {
173
+ cfg: OpenClawConfig;
174
+ accountId?: string | null;
175
+ agentId?: string | null;
176
+ }): string | undefined {
177
+ const dingtalk = (params.cfg?.channels as any)?.dingtalk;
178
+ const accountId = String(params.accountId || "").trim();
179
+ const accountConfig =
180
+ accountId && dingtalk?.accounts && typeof dingtalk.accounts === "object"
181
+ ? dingtalk.accounts[accountId]
182
+ : undefined;
183
+
184
+ if (hasOwn(accountConfig, "ackReaction")) {
185
+ return typeof accountConfig.ackReaction === "string" ? accountConfig.ackReaction.trim() : "";
186
+ }
187
+ if (hasOwn(dingtalk, "ackReaction")) {
188
+ return typeof dingtalk.ackReaction === "string" ? dingtalk.ackReaction.trim() : "";
189
+ }
190
+
191
+ const messages = (params.cfg as any)?.messages;
192
+ if (hasOwn(messages, "ackReaction")) {
193
+ return typeof messages.ackReaction === "string" ? messages.ackReaction.trim() : "";
194
+ }
195
+
196
+ return resolveAgentIdentityEmoji(params.cfg, params.agentId);
197
+ }
198
+
67
199
  /**
68
200
  * Strip group/user prefixes used by CLI targeting.
69
201
  * Returns raw DingTalk target ID and whether caller explicitly requested a user target.