@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.
- package/LICENSE +21 -0
- package/README.md +808 -40
- package/index.ts +62 -0
- package/package.json +4 -2
- package/src/access-control.ts +18 -0
- package/src/ack-reaction-classifier.ts +62 -0
- package/src/ack-reaction-service.ts +135 -0
- package/src/attachment-text-extractor.ts +147 -0
- package/src/card-callback-service.ts +119 -0
- package/src/card-draft-controller.ts +114 -0
- package/src/card-service.ts +794 -62
- package/src/channel.ts +675 -179
- package/src/config-schema.ts +49 -5
- package/src/config.ts +136 -4
- package/src/connection-manager.ts +356 -36
- package/src/dedup.ts +1 -0
- package/src/docs-service.ts +198 -0
- package/src/draft-stream-loop.ts +119 -0
- package/src/feedback-learning-service.ts +643 -0
- package/src/feedback-learning-store.ts +543 -0
- package/src/group-members-store.ts +48 -14
- package/src/inbound-handler.ts +1191 -206
- package/src/learning-command-service.ts +339 -0
- package/src/media-utils.ts +566 -8
- package/src/message-utils.ts +301 -39
- package/src/onboarding.ts +85 -3
- package/src/peer-id-registry.ts +102 -0
- package/src/persistence-store.ts +131 -0
- package/src/quote-journal.ts +242 -0
- package/src/quoted-file-service.ts +385 -0
- package/src/quoted-msg-cache.ts +226 -0
- package/src/send-service.ts +239 -59
- package/src/session-command-service.ts +147 -0
- package/src/session-lock.ts +34 -0
- package/src/session-peer-store.ts +77 -0
- package/src/session-routing.ts +33 -0
- package/src/types.ts +165 -22
- package/src/utils.ts +231 -12
package/src/config-schema.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { DEFAULT_JOURNAL_TTL_DAYS } from "./quote-journal";
|
|
2
3
|
|
|
3
|
-
const
|
|
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
|
-
|
|
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: '
|
|
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
|
-
|
|
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, ...
|
|
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(), ...
|
|
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.
|