@soimy/dingtalk 3.2.0 → 3.4.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 +796 -42
- package/index.ts +62 -0
- package/package.json +4 -2
- package/src/access-control.ts +83 -0
- package/src/ack-reaction/dynamic-ack-reaction-controller.ts +271 -0
- package/src/ack-reaction/dynamic-ack-reaction-events.ts +123 -0
- package/src/ack-reaction/dynamic-ack-reaction-progress.ts +59 -0
- package/src/ack-reaction-classifier.ts +75 -0
- package/src/ack-reaction-service.ts +182 -0
- package/src/attachment-text-extractor.ts +148 -0
- package/src/card-callback-service.ts +119 -0
- package/src/card-draft-controller.ts +114 -0
- package/src/card-service.ts +666 -26
- package/src/channel.ts +455 -150
- package/src/config-schema.ts +64 -6
- package/src/config.ts +161 -5
- package/src/connection-manager.ts +354 -47
- 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 +1374 -259
- package/src/learning-command-service.ts +339 -0
- package/src/media-utils.ts +94 -50
- package/src/message-context-store.ts +787 -0
- package/src/message-utils.ts +487 -46
- package/src/messaging/quoted-context.ts +269 -0
- package/src/messaging/quoted-ref.ts +97 -0
- package/src/onboarding.ts +96 -1
- package/src/peer-id-registry.ts +102 -0
- package/src/persistence-store.ts +131 -0
- package/src/quoted-file-service.ts +385 -0
- package/src/reply-strategy-card.ts +225 -0
- package/src/reply-strategy-markdown.ts +55 -0
- package/src/reply-strategy-with-reaction.ts +190 -0
- package/src/reply-strategy.ts +72 -0
- package/src/send-service.ts +267 -45
- package/src/session-command-service.ts +147 -0
- package/src/session-lock.ts +2 -0
- package/src/session-peer-store.ts +77 -0
- package/src/session-routing.ts +33 -0
- package/src/targeting/agent-name-matcher.ts +148 -0
- package/src/targeting/agent-routing.ts +181 -0
- package/src/targeting/target-directory-adapter.ts +151 -0
- package/src/targeting/target-directory-store.ts +396 -0
- package/src/targeting/target-input.ts +62 -0
- package/src/types.ts +261 -28
- package/src/utils.ts +231 -12
package/src/config-schema.ts
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { DEFAULT_MESSAGE_CONTEXT_TTL_DAYS } from "./message-context-store";
|
|
2
3
|
|
|
3
|
-
const
|
|
4
|
+
const AckReactionSchema = z.union([
|
|
5
|
+
z.literal(""),
|
|
6
|
+
z.enum(["off", "emoji", "kaomoji"]),
|
|
7
|
+
z.string().min(1),
|
|
8
|
+
]);
|
|
9
|
+
|
|
10
|
+
const DingTalkAccountConfigShape = {
|
|
4
11
|
/** Account name (optional display name) */
|
|
5
12
|
name: z.string().optional(),
|
|
6
13
|
|
|
@@ -25,17 +32,24 @@ const DingTalkAccountConfigSchema = z.object({
|
|
|
25
32
|
/** Direct message policy: open, pairing, or allowlist */
|
|
26
33
|
dmPolicy: z.enum(["open", "pairing", "allowlist"]).optional().default("open"),
|
|
27
34
|
|
|
28
|
-
/** Group message policy: open or
|
|
29
|
-
groupPolicy: z.enum(["open", "allowlist"]).optional().default("open"),
|
|
35
|
+
/** Group message policy: open, allowlist, or disabled */
|
|
36
|
+
groupPolicy: z.enum(["open", "allowlist", "disabled"]).optional().default("open"),
|
|
30
37
|
|
|
31
38
|
/** List of allowed user IDs for allowlist policy */
|
|
32
39
|
allowFrom: z.array(z.string()).optional(),
|
|
33
40
|
|
|
41
|
+
/** List of allowed user IDs for group allowlist policy */
|
|
42
|
+
groupAllowFrom: z.array(z.string()).optional(),
|
|
43
|
+
|
|
44
|
+
/** Default disabled. Enabling "all" allows learned displayName lookup but may misroute on stale/duplicate names and is available to all callers until upstream exposes requester authz context. */
|
|
45
|
+
displayNameResolution: z.enum(["disabled", "all"]).optional().default("disabled"),
|
|
46
|
+
|
|
34
47
|
mediaUrlAllowlist: z.array(z.string()).optional(),
|
|
35
48
|
|
|
36
|
-
/**
|
|
37
|
-
|
|
49
|
+
/** Native ack reaction mode: off, emoji, or kaomoji */
|
|
50
|
+
ackReaction: AckReactionSchema.optional(),
|
|
38
51
|
|
|
52
|
+
journalTTLDays: z.number().int().min(1).optional().default(DEFAULT_MESSAGE_CONTEXT_TTL_DAYS),
|
|
39
53
|
/** Enable debug logging */
|
|
40
54
|
debug: z.boolean().optional().default(false),
|
|
41
55
|
|
|
@@ -60,6 +74,8 @@ const DingTalkAccountConfigSchema = z.object({
|
|
|
60
74
|
z.string(),
|
|
61
75
|
z.object({
|
|
62
76
|
systemPrompt: z.string().optional(),
|
|
77
|
+
requireMention: z.boolean().optional(),
|
|
78
|
+
groupAllowFrom: z.array(z.string()).optional(),
|
|
63
79
|
}),
|
|
64
80
|
)
|
|
65
81
|
.optional(),
|
|
@@ -81,12 +97,19 @@ const DingTalkAccountConfigSchema = z.object({
|
|
|
81
97
|
/** Maximum number of runtime reconnect cycles before giving up (default: 10) */
|
|
82
98
|
maxReconnectCycles: z.number().int().min(1).optional().default(10),
|
|
83
99
|
|
|
100
|
+
/** Maximum time (ms) for a single reconnect cycle before starting a new cycle (default: 50000) */
|
|
101
|
+
reconnectDeadlineMs: z.number().int().min(5000).optional().default(50000),
|
|
102
|
+
|
|
84
103
|
/** Whether to use ConnectionManager (default: true). When false, rely on DWClient native keepAlive+autoReconnect. */
|
|
85
104
|
useConnectionManager: z.boolean().optional().default(true),
|
|
86
105
|
|
|
87
106
|
/** Maximum inbound media file size in MB (overrides runtime default when set) */
|
|
88
107
|
mediaMaxMb: z.number().int().min(1).optional(),
|
|
89
108
|
|
|
109
|
+
/** Whether to enable underlying stream keepAlive heartbeat; defaults to !useConnectionManager when omitted */
|
|
110
|
+
keepAlive: z.boolean().optional(),
|
|
111
|
+
/** Bypass system/global HTTP(S) proxy for DingTalk outbound send/card/upload APIs */
|
|
112
|
+
bypassProxyForSend: z.boolean().optional().default(false),
|
|
90
113
|
proactivePermissionHint: z
|
|
91
114
|
.object({
|
|
92
115
|
enabled: z.boolean().optional().default(true),
|
|
@@ -94,7 +117,42 @@ const DingTalkAccountConfigSchema = z.object({
|
|
|
94
117
|
})
|
|
95
118
|
.optional()
|
|
96
119
|
.default({ enabled: true, cooldownHours: 24 }),
|
|
97
|
-
|
|
120
|
+
|
|
121
|
+
/** Enable real-time card streaming (default: false).
|
|
122
|
+
* When true, card updates are streamed per-token with 300ms throttle for a smoother experience, at the cost of more API calls. */
|
|
123
|
+
cardRealTimeStream: z.boolean().optional().default(false),
|
|
124
|
+
|
|
125
|
+
/** AICard degrade duration in milliseconds after trigger errors (default: 30 minutes) */
|
|
126
|
+
aicardDegradeMs: z.number().int().min(60_000).optional().default(30 * 60 * 1000),
|
|
127
|
+
|
|
128
|
+
/** Enable local learning loop (default: false) */
|
|
129
|
+
learningEnabled: z.boolean().optional(),
|
|
130
|
+
|
|
131
|
+
/** Auto-apply generated reflections into session notes/global rules (default: false) */
|
|
132
|
+
learningAutoApply: z.boolean().optional(),
|
|
133
|
+
|
|
134
|
+
/** Session learning note TTL in milliseconds (default: 6 hours) */
|
|
135
|
+
learningNoteTtlMs: z.number().int().min(60_000).optional(),
|
|
136
|
+
|
|
137
|
+
/** @deprecated Use learningEnabled */
|
|
138
|
+
feedbackLearningEnabled: z.boolean().optional(),
|
|
139
|
+
|
|
140
|
+
/** @deprecated Use learningAutoApply */
|
|
141
|
+
feedbackLearningAutoApply: z.boolean().optional(),
|
|
142
|
+
|
|
143
|
+
/** @deprecated Use learningNoteTtlMs */
|
|
144
|
+
feedbackLearningNoteTtlMs: z.number().int().min(60_000).optional(),
|
|
145
|
+
|
|
146
|
+
/** Whether to convert markdown tables to plain text for better rendering on some clients (default: true) */
|
|
147
|
+
convertMarkdownTables: z.boolean().optional().default(true),
|
|
148
|
+
|
|
149
|
+
/** @mention the sender after card finalization in group chats.
|
|
150
|
+
* Set to a non-empty string (e.g. "✅ 回复完成") to enable — the value is used as the message text.
|
|
151
|
+
* Leave empty or omit to disable. */
|
|
152
|
+
cardAtSender: z.string().optional(),
|
|
153
|
+
} as const;
|
|
154
|
+
|
|
155
|
+
const DingTalkAccountConfigSchema = z.object(DingTalkAccountConfigShape);
|
|
98
156
|
|
|
99
157
|
/**
|
|
100
158
|
* 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]);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (accountId) {
|
|
74
|
+
return normalizeLearningConfig(dingtalkCfg, { applyDefaults: true });
|
|
18
75
|
}
|
|
19
76
|
|
|
20
|
-
|
|
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;
|
|
@@ -55,7 +145,7 @@ export const resolveUserPath = resolveRelativePath;
|
|
|
55
145
|
export function resolveGroupConfig(
|
|
56
146
|
cfg: DingTalkConfig,
|
|
57
147
|
groupId: string,
|
|
58
|
-
): { systemPrompt?: string } | undefined {
|
|
148
|
+
): { systemPrompt?: string; requireMention?: boolean; groupAllowFrom?: string[] } | undefined {
|
|
59
149
|
// Group config supports exact match first, then wildcard fallback.
|
|
60
150
|
const groups = cfg.groups;
|
|
61
151
|
if (!groups) {
|
|
@@ -64,6 +154,72 @@ 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
|
+
function normalizeAckReactionValue(value: unknown): string | undefined {
|
|
173
|
+
if (typeof value !== "string") {
|
|
174
|
+
return undefined;
|
|
175
|
+
}
|
|
176
|
+
const trimmed = value.trim();
|
|
177
|
+
if (!trimmed) {
|
|
178
|
+
return "";
|
|
179
|
+
}
|
|
180
|
+
const normalized = trimmed.toLowerCase();
|
|
181
|
+
if (normalized === "off") {
|
|
182
|
+
return "off";
|
|
183
|
+
}
|
|
184
|
+
if (normalized === "emoji") {
|
|
185
|
+
return "emoji";
|
|
186
|
+
}
|
|
187
|
+
if (normalized === "kaomoji") {
|
|
188
|
+
return "kaomoji";
|
|
189
|
+
}
|
|
190
|
+
if (trimmed === "🤔思考中") {
|
|
191
|
+
return "emoji";
|
|
192
|
+
}
|
|
193
|
+
return trimmed;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function resolveAckReactionSetting(params: {
|
|
197
|
+
cfg: OpenClawConfig;
|
|
198
|
+
accountId?: string | null;
|
|
199
|
+
agentId?: string | null;
|
|
200
|
+
}): string | undefined {
|
|
201
|
+
const dingtalk = (params.cfg?.channels as any)?.dingtalk;
|
|
202
|
+
const accountId = String(params.accountId || "").trim();
|
|
203
|
+
const accountConfig =
|
|
204
|
+
accountId && dingtalk?.accounts && typeof dingtalk.accounts === "object"
|
|
205
|
+
? dingtalk.accounts[accountId]
|
|
206
|
+
: undefined;
|
|
207
|
+
|
|
208
|
+
if (hasOwn(accountConfig, "ackReaction")) {
|
|
209
|
+
return normalizeAckReactionValue(accountConfig.ackReaction);
|
|
210
|
+
}
|
|
211
|
+
if (hasOwn(dingtalk, "ackReaction")) {
|
|
212
|
+
return normalizeAckReactionValue(dingtalk.ackReaction);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const messages = (params.cfg as any)?.messages;
|
|
216
|
+
if (hasOwn(messages, "ackReaction")) {
|
|
217
|
+
return normalizeAckReactionValue(messages.ackReaction);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return resolveAgentIdentityEmoji(params.cfg, params.agentId) || "👀";
|
|
221
|
+
}
|
|
222
|
+
|
|
67
223
|
/**
|
|
68
224
|
* Strip group/user prefixes used by CLI targeting.
|
|
69
225
|
* Returns raw DingTalk target ID and whether caller explicitly requested a user target.
|