@soimy/dingtalk 3.3.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/README.md +141 -12
- package/package.json +2 -2
- package/src/access-control.ts +65 -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 +17 -4
- package/src/ack-reaction-service.ts +66 -19
- package/src/attachment-text-extractor.ts +2 -1
- package/src/card-service.ts +145 -257
- package/src/channel.ts +60 -28
- package/src/config-schema.ts +28 -6
- package/src/config.ts +29 -5
- package/src/inbound-handler.ts +694 -520
- package/src/message-context-store.ts +787 -0
- package/src/message-utils.ts +221 -42
- package/src/messaging/quoted-context.ts +269 -0
- package/src/messaging/quoted-ref.ts +97 -0
- package/src/onboarding.ts +62 -5
- 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 +140 -52
- 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 +114 -9
- package/src/quote-journal.ts +0 -242
- package/src/quoted-msg-cache.ts +0 -226
package/src/channel.ts
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { DWClient, TOPIC_CARD, TOPIC_ROBOT } from "dingtalk-stream";
|
|
3
|
-
import type {
|
|
4
|
-
ChannelMessageActionAdapter,
|
|
5
|
-
OpenClawConfig,
|
|
6
|
-
} from "openclaw/plugin-sdk";
|
|
3
|
+
import type { ChannelMessageActionAdapter, OpenClawConfig } from "openclaw/plugin-sdk";
|
|
7
4
|
import * as pluginSdk from "openclaw/plugin-sdk";
|
|
8
5
|
import { getAccessToken } from "./auth";
|
|
9
6
|
import { analyzeCardCallback } from "./card-callback-service";
|
|
@@ -18,23 +15,24 @@ import {
|
|
|
18
15
|
getConfig,
|
|
19
16
|
isConfigured,
|
|
20
17
|
mergeAccountWithDefaults,
|
|
18
|
+
resolveGroupConfig,
|
|
21
19
|
resolveRelativePath,
|
|
22
20
|
stripTargetPrefix,
|
|
23
21
|
} from "./config";
|
|
24
22
|
import { DingTalkConfigSchema } from "./config-schema.js";
|
|
25
23
|
import { ConnectionManager } from "./connection-manager";
|
|
26
24
|
import { isMessageProcessed, markMessageProcessed } from "./dedup";
|
|
25
|
+
import {
|
|
26
|
+
isFeedbackLearningAutoApplyEnabled,
|
|
27
|
+
isFeedbackLearningEnabled,
|
|
28
|
+
recordExplicitFeedbackLearning,
|
|
29
|
+
} from "./feedback-learning-service";
|
|
27
30
|
import { handleDingTalkMessage } from "./inbound-handler";
|
|
28
31
|
import { getLogger } from "./logger-context";
|
|
29
32
|
import { prepareMediaInput, resolveOutboundMediaType } from "./media-utils";
|
|
30
33
|
import { dingtalkOnboardingAdapter } from "./onboarding.js";
|
|
31
34
|
import { resolveOriginalPeerId, preloadPeerIdsFromSessions } from "./peer-id-registry";
|
|
32
35
|
import { getDingTalkRuntime } from "./runtime";
|
|
33
|
-
import {
|
|
34
|
-
isFeedbackLearningAutoApplyEnabled,
|
|
35
|
-
isFeedbackLearningEnabled,
|
|
36
|
-
recordExplicitFeedbackLearning,
|
|
37
|
-
} from "./feedback-learning-service";
|
|
38
36
|
import {
|
|
39
37
|
sendMessage,
|
|
40
38
|
sendProactiveMedia,
|
|
@@ -42,6 +40,12 @@ import {
|
|
|
42
40
|
sendBySession,
|
|
43
41
|
uploadMedia,
|
|
44
42
|
} from "./send-service";
|
|
43
|
+
import {
|
|
44
|
+
listDingTalkDirectoryGroups,
|
|
45
|
+
listDingTalkDirectoryUsers,
|
|
46
|
+
normalizeResolvedDingTalkTarget,
|
|
47
|
+
} from "./targeting/target-directory-adapter";
|
|
48
|
+
import { looksLikeDingTalkTargetId, normalizeDingTalkTarget } from "./targeting/target-input";
|
|
45
49
|
import type {
|
|
46
50
|
DingTalkInboundMessage,
|
|
47
51
|
GatewayStartContext,
|
|
@@ -93,7 +97,11 @@ function getInstrumentedEndpoint(client: InstrumentedDWClient): string | undefin
|
|
|
93
97
|
if (typeof endpointConfig === "string") {
|
|
94
98
|
return endpointConfig;
|
|
95
99
|
}
|
|
96
|
-
if (
|
|
100
|
+
if (
|
|
101
|
+
endpointConfig &&
|
|
102
|
+
typeof endpointConfig === "object" &&
|
|
103
|
+
typeof endpointConfig.endpoint === "string"
|
|
104
|
+
) {
|
|
97
105
|
return endpointConfig.endpoint;
|
|
98
106
|
}
|
|
99
107
|
return undefined;
|
|
@@ -101,7 +109,10 @@ function getInstrumentedEndpoint(client: InstrumentedDWClient): string | undefin
|
|
|
101
109
|
|
|
102
110
|
function instrumentConnectionStages(client: DWClient): void {
|
|
103
111
|
const instrumented = client as unknown as InstrumentedDWClient;
|
|
104
|
-
if (
|
|
112
|
+
if (
|
|
113
|
+
typeof instrumented.getEndpoint !== "function" ||
|
|
114
|
+
typeof instrumented._connect !== "function"
|
|
115
|
+
) {
|
|
105
116
|
return;
|
|
106
117
|
}
|
|
107
118
|
|
|
@@ -339,9 +350,7 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
|
|
|
339
350
|
const config = getConfig(cfg);
|
|
340
351
|
const id = accountId || "default";
|
|
341
352
|
const account = config.accounts?.[id];
|
|
342
|
-
const resolvedConfig = account
|
|
343
|
-
? mergeAccountWithDefaults(config, account)
|
|
344
|
-
: config;
|
|
353
|
+
const resolvedConfig = account ? mergeAccountWithDefaults(config, account) : config;
|
|
345
354
|
const configured = Boolean(resolvedConfig.clientId && resolvedConfig.clientSecret);
|
|
346
355
|
return {
|
|
347
356
|
accountId: id,
|
|
@@ -372,7 +381,16 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
|
|
|
372
381
|
}),
|
|
373
382
|
},
|
|
374
383
|
groups: {
|
|
375
|
-
resolveRequireMention: ({ cfg }: any): boolean =>
|
|
384
|
+
resolveRequireMention: ({ cfg, groupId }: any): boolean => {
|
|
385
|
+
const config = getConfig(cfg);
|
|
386
|
+
if (groupId) {
|
|
387
|
+
const groupCfg = resolveGroupConfig(config, groupId);
|
|
388
|
+
if (groupCfg?.requireMention !== undefined) {
|
|
389
|
+
return groupCfg.requireMention;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return config.groupPolicy !== "open";
|
|
393
|
+
},
|
|
376
394
|
resolveGroupIntroHint: ({ groupId, groupChannel }: any): string | undefined => {
|
|
377
395
|
const parts = [`conversationId=${groupId}`];
|
|
378
396
|
if (groupChannel) {
|
|
@@ -382,12 +400,20 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
|
|
|
382
400
|
},
|
|
383
401
|
},
|
|
384
402
|
messaging: {
|
|
385
|
-
normalizeTarget: (raw: string) => (raw ? raw
|
|
403
|
+
normalizeTarget: (raw: string) => (raw ? normalizeDingTalkTarget(raw) : undefined),
|
|
386
404
|
targetResolver: {
|
|
387
|
-
looksLikeId: (
|
|
388
|
-
|
|
405
|
+
looksLikeId: (raw: string, normalized?: string): boolean =>
|
|
406
|
+
looksLikeDingTalkTargetId(raw, normalized),
|
|
407
|
+
hint: "<displayName|conversationId|user:staffId|user:+861...>",
|
|
389
408
|
},
|
|
390
409
|
},
|
|
410
|
+
directory: {
|
|
411
|
+
self: async () => null,
|
|
412
|
+
listGroups: async (params) => listDingTalkDirectoryGroups(params),
|
|
413
|
+
listGroupsLive: async (params) => listDingTalkDirectoryGroups(params),
|
|
414
|
+
listPeers: async (params) => listDingTalkDirectoryUsers(params),
|
|
415
|
+
listPeersLive: async (params) => listDingTalkDirectoryUsers(params),
|
|
416
|
+
},
|
|
391
417
|
actions: dingtalkMessageActions,
|
|
392
418
|
outbound: {
|
|
393
419
|
deliveryMode: "direct" as const,
|
|
@@ -399,9 +425,7 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
|
|
|
399
425
|
error: new Error("DingTalk message requires --to <conversationId>"),
|
|
400
426
|
};
|
|
401
427
|
}
|
|
402
|
-
|
|
403
|
-
const resolved = resolveOriginalPeerId(targetId);
|
|
404
|
-
return { ok: true as const, to: resolved };
|
|
428
|
+
return { ok: true as const, to: normalizeResolvedDingTalkTarget(trimmed) };
|
|
405
429
|
},
|
|
406
430
|
sendText: async ({ cfg, to, text, accountId, log }: any) => {
|
|
407
431
|
const config = getConfig(cfg, accountId);
|
|
@@ -490,12 +514,17 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
|
|
|
490
514
|
preparedMedia = await prepareMediaInput(rawMediaPath, log, config.mediaUrlAllowlist);
|
|
491
515
|
} catch (err: any) {
|
|
492
516
|
if (err?.response?.data !== undefined) {
|
|
493
|
-
log?.error?.(
|
|
517
|
+
log?.error?.(
|
|
518
|
+
formatDingTalkErrorPayloadLog("outbound.sendMedia.prepare", err.response.data),
|
|
519
|
+
);
|
|
494
520
|
}
|
|
495
521
|
const errorCode = typeof err?.code === "string" ? `[${err.code}] ` : "";
|
|
496
|
-
throw new Error(
|
|
497
|
-
|
|
498
|
-
|
|
522
|
+
throw new Error(
|
|
523
|
+
`remote media preparation failed: ${errorCode}${err?.message || "unknown error"}`,
|
|
524
|
+
{
|
|
525
|
+
cause: err,
|
|
526
|
+
},
|
|
527
|
+
);
|
|
499
528
|
}
|
|
500
529
|
|
|
501
530
|
const actualMediaPath = preparedMedia.cleanup
|
|
@@ -521,7 +550,9 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
|
|
|
521
550
|
});
|
|
522
551
|
} catch (err: any) {
|
|
523
552
|
if (err?.response?.data !== undefined) {
|
|
524
|
-
log?.error?.(
|
|
553
|
+
log?.error?.(
|
|
554
|
+
formatDingTalkErrorPayloadLog("outbound.sendMedia.send", err.response.data),
|
|
555
|
+
);
|
|
525
556
|
}
|
|
526
557
|
throw new Error(`proactive media send failed: ${err?.message || "unknown error"}`, {
|
|
527
558
|
cause: err,
|
|
@@ -657,6 +688,7 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
|
|
|
657
688
|
if (!dedupKey) {
|
|
658
689
|
ctx.log?.warn?.(`[${account.accountId}] No message ID available for deduplication`);
|
|
659
690
|
stats.noMessageId += 1;
|
|
691
|
+
acknowledge();
|
|
660
692
|
await handleDingTalkMessage({
|
|
661
693
|
cfg,
|
|
662
694
|
accountId: account.accountId,
|
|
@@ -666,7 +698,6 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
|
|
|
666
698
|
dingtalkConfig: config,
|
|
667
699
|
});
|
|
668
700
|
stats.processed += 1;
|
|
669
|
-
acknowledge();
|
|
670
701
|
if (stats.received % INBOUND_COUNTER_LOG_EVERY === 0) {
|
|
671
702
|
logInboundCounters(ctx.log, account.accountId, "periodic");
|
|
672
703
|
}
|
|
@@ -693,11 +724,13 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
|
|
|
693
724
|
`[${account.accountId}] Skipping in-flight duplicate message: ${dedupKey}`,
|
|
694
725
|
);
|
|
695
726
|
stats.inflightSkipped += 1;
|
|
727
|
+
acknowledge();
|
|
696
728
|
logInboundCounters(ctx.log, account.accountId, "inflight-skipped");
|
|
697
729
|
return;
|
|
698
730
|
}
|
|
699
731
|
}
|
|
700
732
|
|
|
733
|
+
acknowledge();
|
|
701
734
|
processingDedupKeys.set(dedupKey, Date.now());
|
|
702
735
|
try {
|
|
703
736
|
await handleDingTalkMessage({
|
|
@@ -710,7 +743,6 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
|
|
|
710
743
|
});
|
|
711
744
|
stats.processed += 1;
|
|
712
745
|
markMessageProcessed(dedupKey);
|
|
713
|
-
acknowledge();
|
|
714
746
|
if (stats.received % INBOUND_COUNTER_LOG_EVERY === 0) {
|
|
715
747
|
logInboundCounters(ctx.log, account.accountId, "periodic");
|
|
716
748
|
}
|
package/src/config-schema.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
2
|
+
import { DEFAULT_MESSAGE_CONTEXT_TTL_DAYS } from "./message-context-store";
|
|
3
|
+
|
|
4
|
+
const AckReactionSchema = z.union([
|
|
5
|
+
z.literal(""),
|
|
6
|
+
z.enum(["off", "emoji", "kaomoji"]),
|
|
7
|
+
z.string().min(1),
|
|
8
|
+
]);
|
|
3
9
|
|
|
4
10
|
const DingTalkAccountConfigShape = {
|
|
5
11
|
/** Account name (optional display name) */
|
|
@@ -26,18 +32,24 @@ const DingTalkAccountConfigShape = {
|
|
|
26
32
|
/** Direct message policy: open, pairing, or allowlist */
|
|
27
33
|
dmPolicy: z.enum(["open", "pairing", "allowlist"]).optional().default("open"),
|
|
28
34
|
|
|
29
|
-
/** Group message policy: open or
|
|
30
|
-
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"),
|
|
31
37
|
|
|
32
38
|
/** List of allowed user IDs for allowlist policy */
|
|
33
39
|
allowFrom: z.array(z.string()).optional(),
|
|
34
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
|
+
|
|
35
47
|
mediaUrlAllowlist: z.array(z.string()).optional(),
|
|
36
48
|
|
|
37
|
-
/**
|
|
38
|
-
ackReaction:
|
|
49
|
+
/** Native ack reaction mode: off, emoji, or kaomoji */
|
|
50
|
+
ackReaction: AckReactionSchema.optional(),
|
|
39
51
|
|
|
40
|
-
journalTTLDays: z.number().int().min(1).optional().default(
|
|
52
|
+
journalTTLDays: z.number().int().min(1).optional().default(DEFAULT_MESSAGE_CONTEXT_TTL_DAYS),
|
|
41
53
|
/** Enable debug logging */
|
|
42
54
|
debug: z.boolean().optional().default(false),
|
|
43
55
|
|
|
@@ -62,6 +74,8 @@ const DingTalkAccountConfigShape = {
|
|
|
62
74
|
z.string(),
|
|
63
75
|
z.object({
|
|
64
76
|
systemPrompt: z.string().optional(),
|
|
77
|
+
requireMention: z.boolean().optional(),
|
|
78
|
+
groupAllowFrom: z.array(z.string()).optional(),
|
|
65
79
|
}),
|
|
66
80
|
)
|
|
67
81
|
.optional(),
|
|
@@ -128,6 +142,14 @@ const DingTalkAccountConfigShape = {
|
|
|
128
142
|
|
|
129
143
|
/** @deprecated Use learningNoteTtlMs */
|
|
130
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(),
|
|
131
153
|
} as const;
|
|
132
154
|
|
|
133
155
|
const DingTalkAccountConfigSchema = z.object(DingTalkAccountConfigShape);
|
package/src/config.ts
CHANGED
|
@@ -145,7 +145,7 @@ export const resolveUserPath = resolveRelativePath;
|
|
|
145
145
|
export function resolveGroupConfig(
|
|
146
146
|
cfg: DingTalkConfig,
|
|
147
147
|
groupId: string,
|
|
148
|
-
): { systemPrompt?: string } | undefined {
|
|
148
|
+
): { systemPrompt?: string; requireMention?: boolean; groupAllowFrom?: string[] } | undefined {
|
|
149
149
|
// Group config supports exact match first, then wildcard fallback.
|
|
150
150
|
const groups = cfg.groups;
|
|
151
151
|
if (!groups) {
|
|
@@ -169,6 +169,30 @@ function resolveAgentIdentityEmoji(cfg: OpenClawConfig, agentId?: string | null)
|
|
|
169
169
|
return emoji || undefined;
|
|
170
170
|
}
|
|
171
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
|
+
|
|
172
196
|
export function resolveAckReactionSetting(params: {
|
|
173
197
|
cfg: OpenClawConfig;
|
|
174
198
|
accountId?: string | null;
|
|
@@ -182,18 +206,18 @@ export function resolveAckReactionSetting(params: {
|
|
|
182
206
|
: undefined;
|
|
183
207
|
|
|
184
208
|
if (hasOwn(accountConfig, "ackReaction")) {
|
|
185
|
-
return
|
|
209
|
+
return normalizeAckReactionValue(accountConfig.ackReaction);
|
|
186
210
|
}
|
|
187
211
|
if (hasOwn(dingtalk, "ackReaction")) {
|
|
188
|
-
return
|
|
212
|
+
return normalizeAckReactionValue(dingtalk.ackReaction);
|
|
189
213
|
}
|
|
190
214
|
|
|
191
215
|
const messages = (params.cfg as any)?.messages;
|
|
192
216
|
if (hasOwn(messages, "ackReaction")) {
|
|
193
|
-
return
|
|
217
|
+
return normalizeAckReactionValue(messages.ackReaction);
|
|
194
218
|
}
|
|
195
219
|
|
|
196
|
-
return resolveAgentIdentityEmoji(params.cfg, params.agentId);
|
|
220
|
+
return resolveAgentIdentityEmoji(params.cfg, params.agentId) || "👀";
|
|
197
221
|
}
|
|
198
222
|
|
|
199
223
|
/**
|