@soimy/dingtalk 3.3.0 → 3.4.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.
- package/README.md +141 -12
- package/index.ts +71 -66
- package/package.json +6 -5
- 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 +106 -47
- package/src/config-schema.ts +28 -6
- package/src/config.ts +30 -6
- package/src/connection-manager.ts +16 -5
- package/src/inbound-handler.ts +694 -520
- package/src/media-utils.ts +99 -36
- 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 +381 -269
- 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/runtime.ts +5 -7
- package/src/send-service.ts +164 -62
- package/src/targeting/agent-name-matcher.ts +148 -0
- package/src/targeting/agent-routing.ts +181 -0
- package/src/targeting/target-directory-adapter.ts +152 -0
- package/src/targeting/target-directory-store.ts +396 -0
- package/src/targeting/target-input.ts +62 -0
- package/src/types.ts +124 -21
- package/src/quote-journal.ts +0 -242
- package/src/quoted-msg-cache.ts +0 -226
package/src/channel.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { DWClient, TOPIC_CARD, TOPIC_ROBOT } from "dingtalk-stream";
|
|
3
|
-
import type {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
} from "openclaw/plugin-sdk";
|
|
7
|
-
import
|
|
3
|
+
import type { ChannelMessageActionAdapter } from "openclaw/plugin-sdk/channel-contract";
|
|
4
|
+
import { buildChannelConfigSchema, type OpenClawConfig } from "openclaw/plugin-sdk/core";
|
|
5
|
+
import { jsonResult } from "openclaw/plugin-sdk/telegram-core";
|
|
6
|
+
import { readStringParam } from "openclaw/plugin-sdk/param-readers";
|
|
7
|
+
import { extractToolSend } from "openclaw/plugin-sdk/tool-send";
|
|
8
8
|
import { getAccessToken } from "./auth";
|
|
9
9
|
import { analyzeCardCallback } from "./card-callback-service";
|
|
10
10
|
import {
|
|
@@ -18,23 +18,24 @@ import {
|
|
|
18
18
|
getConfig,
|
|
19
19
|
isConfigured,
|
|
20
20
|
mergeAccountWithDefaults,
|
|
21
|
+
resolveGroupConfig,
|
|
21
22
|
resolveRelativePath,
|
|
22
23
|
stripTargetPrefix,
|
|
23
24
|
} from "./config";
|
|
24
25
|
import { DingTalkConfigSchema } from "./config-schema.js";
|
|
25
26
|
import { ConnectionManager } from "./connection-manager";
|
|
26
27
|
import { isMessageProcessed, markMessageProcessed } from "./dedup";
|
|
27
|
-
import { handleDingTalkMessage } from "./inbound-handler";
|
|
28
|
-
import { getLogger } from "./logger-context";
|
|
29
|
-
import { prepareMediaInput, resolveOutboundMediaType } from "./media-utils";
|
|
30
|
-
import { dingtalkOnboardingAdapter } from "./onboarding.js";
|
|
31
|
-
import { resolveOriginalPeerId, preloadPeerIdsFromSessions } from "./peer-id-registry";
|
|
32
|
-
import { getDingTalkRuntime } from "./runtime";
|
|
33
28
|
import {
|
|
34
29
|
isFeedbackLearningAutoApplyEnabled,
|
|
35
30
|
isFeedbackLearningEnabled,
|
|
36
31
|
recordExplicitFeedbackLearning,
|
|
37
32
|
} from "./feedback-learning-service";
|
|
33
|
+
import { handleDingTalkMessage } from "./inbound-handler";
|
|
34
|
+
import { getLogger } from "./logger-context";
|
|
35
|
+
import { prepareMediaInput, resolveOutboundMediaType } from "./media-utils";
|
|
36
|
+
import { dingtalkSetupAdapter, dingtalkSetupWizard } from "./onboarding.js";
|
|
37
|
+
import { resolveOriginalPeerId, preloadPeerIdsFromSessions } from "./peer-id-registry";
|
|
38
|
+
import { getDingTalkRuntime } from "./runtime";
|
|
38
39
|
import {
|
|
39
40
|
sendMessage,
|
|
40
41
|
sendProactiveMedia,
|
|
@@ -42,6 +43,12 @@ import {
|
|
|
42
43
|
sendBySession,
|
|
43
44
|
uploadMedia,
|
|
44
45
|
} from "./send-service";
|
|
46
|
+
import {
|
|
47
|
+
listDingTalkDirectoryGroups,
|
|
48
|
+
listDingTalkDirectoryUsers,
|
|
49
|
+
normalizeResolvedDingTalkTarget,
|
|
50
|
+
} from "./targeting/target-directory-adapter";
|
|
51
|
+
import { looksLikeDingTalkTargetId, normalizeDingTalkTarget } from "./targeting/target-input";
|
|
45
52
|
import type {
|
|
46
53
|
DingTalkInboundMessage,
|
|
47
54
|
GatewayStartContext,
|
|
@@ -93,7 +100,11 @@ function getInstrumentedEndpoint(client: InstrumentedDWClient): string | undefin
|
|
|
93
100
|
if (typeof endpointConfig === "string") {
|
|
94
101
|
return endpointConfig;
|
|
95
102
|
}
|
|
96
|
-
if (
|
|
103
|
+
if (
|
|
104
|
+
endpointConfig &&
|
|
105
|
+
typeof endpointConfig === "object" &&
|
|
106
|
+
typeof endpointConfig.endpoint === "string"
|
|
107
|
+
) {
|
|
97
108
|
return endpointConfig.endpoint;
|
|
98
109
|
}
|
|
99
110
|
return undefined;
|
|
@@ -101,7 +112,10 @@ function getInstrumentedEndpoint(client: InstrumentedDWClient): string | undefin
|
|
|
101
112
|
|
|
102
113
|
function instrumentConnectionStages(client: DWClient): void {
|
|
103
114
|
const instrumented = client as unknown as InstrumentedDWClient;
|
|
104
|
-
if (
|
|
115
|
+
if (
|
|
116
|
+
typeof instrumented.getEndpoint !== "function" ||
|
|
117
|
+
typeof instrumented._connect !== "function"
|
|
118
|
+
) {
|
|
105
119
|
return;
|
|
106
120
|
}
|
|
107
121
|
|
|
@@ -195,26 +209,46 @@ function readBooleanLikeParam(params: Record<string, unknown>, key: string): boo
|
|
|
195
209
|
return undefined;
|
|
196
210
|
}
|
|
197
211
|
|
|
212
|
+
function describeDingTalkMessageTool(cfg: OpenClawConfig): {
|
|
213
|
+
actions: readonly ["send"] | readonly [];
|
|
214
|
+
capabilities: readonly ["cards"] | readonly [];
|
|
215
|
+
schema: null;
|
|
216
|
+
} {
|
|
217
|
+
const config = getConfig(cfg);
|
|
218
|
+
const configured = Boolean(config.clientId && config.clientSecret);
|
|
219
|
+
if (!configured && !(config.accounts && Object.keys(config.accounts).length > 0)) {
|
|
220
|
+
return { actions: [], capabilities: [], schema: null };
|
|
221
|
+
}
|
|
222
|
+
const hasCardMode =
|
|
223
|
+
config.messageType === "card" ||
|
|
224
|
+
(config.accounts && Object.values(config.accounts).some((a) => a?.messageType === "card"));
|
|
225
|
+
return {
|
|
226
|
+
actions: ["send"] as const,
|
|
227
|
+
capabilities: hasCardMode ? (["cards"] as const) : [],
|
|
228
|
+
schema: null,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
198
232
|
const dingtalkMessageActions: ChannelMessageActionAdapter = {
|
|
199
|
-
|
|
233
|
+
describeMessageTool: ({ cfg }) => describeDingTalkMessageTool(cfg),
|
|
200
234
|
supportsAction: ({ action }) => action === "send",
|
|
201
|
-
extractToolSend: ({ args }) =>
|
|
202
|
-
handleAction: async ({ action, params, cfg, accountId, dryRun }) => {
|
|
235
|
+
extractToolSend: ({ args }) => extractToolSend(args, "sendMessage"),
|
|
236
|
+
handleAction: async ({ action, params, cfg, accountId, dryRun, mediaLocalRoots }) => {
|
|
203
237
|
if (action !== "send") {
|
|
204
238
|
throw new Error(`Action ${action} is not supported for provider dingtalk.`);
|
|
205
239
|
}
|
|
206
240
|
|
|
207
|
-
const to =
|
|
241
|
+
const to = readStringParam(params, "to", { required: true });
|
|
208
242
|
const mediaInput =
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
243
|
+
readStringParam(params, "media", { trim: false }) ??
|
|
244
|
+
readStringParam(params, "path", { trim: false }) ??
|
|
245
|
+
readStringParam(params, "filePath", { trim: false }) ??
|
|
246
|
+
readStringParam(params, "mediaUrl", { trim: false });
|
|
213
247
|
|
|
214
248
|
const hasMedia = Boolean(mediaInput && mediaInput.trim());
|
|
215
|
-
const caption =
|
|
249
|
+
const caption = readStringParam(params, "caption", { allowEmpty: true }) ?? "";
|
|
216
250
|
let message =
|
|
217
|
-
|
|
251
|
+
readStringParam(params, "message", {
|
|
218
252
|
required: !hasMedia,
|
|
219
253
|
allowEmpty: true,
|
|
220
254
|
}) ?? "";
|
|
@@ -224,12 +258,12 @@ const dingtalkMessageActions: ChannelMessageActionAdapter = {
|
|
|
224
258
|
}
|
|
225
259
|
|
|
226
260
|
const asVoice = readBooleanLikeParam(params, "asVoice") === true;
|
|
227
|
-
const requestedMediaType =
|
|
261
|
+
const requestedMediaType = readStringParam(params, "mediaType");
|
|
228
262
|
|
|
229
263
|
const target = resolveOriginalPeerId(stripTargetPrefix(to).targetId);
|
|
230
264
|
|
|
231
265
|
if (dryRun) {
|
|
232
|
-
return
|
|
266
|
+
return jsonResult({
|
|
233
267
|
ok: true,
|
|
234
268
|
dryRun: true,
|
|
235
269
|
to: target,
|
|
@@ -256,13 +290,14 @@ const dingtalkMessageActions: ChannelMessageActionAdapter = {
|
|
|
256
290
|
const result = await sendProactiveMedia(config, target, mediaPath, mediaType, {
|
|
257
291
|
log,
|
|
258
292
|
accountId: accountId ?? undefined,
|
|
293
|
+
mediaLocalRoots: mediaLocalRoots ? [...mediaLocalRoots] : undefined,
|
|
259
294
|
});
|
|
260
295
|
|
|
261
296
|
if (!result.ok) {
|
|
262
297
|
throw new Error(result.error || "send media failed");
|
|
263
298
|
}
|
|
264
299
|
|
|
265
|
-
return
|
|
300
|
+
return jsonResult({
|
|
266
301
|
ok: true,
|
|
267
302
|
to: target,
|
|
268
303
|
mediaType,
|
|
@@ -294,7 +329,7 @@ const dingtalkMessageActions: ChannelMessageActionAdapter = {
|
|
|
294
329
|
}
|
|
295
330
|
|
|
296
331
|
const data = result.data as any;
|
|
297
|
-
return
|
|
332
|
+
return jsonResult({
|
|
298
333
|
ok: true,
|
|
299
334
|
to: target,
|
|
300
335
|
messageId: data?.processQueryKey || data?.messageId || null,
|
|
@@ -311,12 +346,13 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
|
|
|
311
346
|
id: "dingtalk",
|
|
312
347
|
label: "DingTalk",
|
|
313
348
|
selectionLabel: "DingTalk (钉钉)",
|
|
314
|
-
docsPath: "/
|
|
349
|
+
docsPath: "https://github.com/soimy/openclaw-channel-dingtalk",
|
|
315
350
|
blurb: "钉钉企业内部机器人,使用 Stream 模式,无需公网 IP。",
|
|
316
351
|
aliases: ["dd", "ding"],
|
|
317
352
|
},
|
|
318
|
-
configSchema:
|
|
319
|
-
|
|
353
|
+
configSchema: buildChannelConfigSchema(DingTalkConfigSchema),
|
|
354
|
+
setup: dingtalkSetupAdapter,
|
|
355
|
+
setupWizard: dingtalkSetupWizard,
|
|
320
356
|
capabilities: {
|
|
321
357
|
chatTypes: ["direct", "group"] as Array<"direct" | "group">,
|
|
322
358
|
reactions: false,
|
|
@@ -339,9 +375,7 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
|
|
|
339
375
|
const config = getConfig(cfg);
|
|
340
376
|
const id = accountId || "default";
|
|
341
377
|
const account = config.accounts?.[id];
|
|
342
|
-
const resolvedConfig = account
|
|
343
|
-
? mergeAccountWithDefaults(config, account)
|
|
344
|
-
: config;
|
|
378
|
+
const resolvedConfig = account ? mergeAccountWithDefaults(config, account) : config;
|
|
345
379
|
const configured = Boolean(resolvedConfig.clientId && resolvedConfig.clientSecret);
|
|
346
380
|
return {
|
|
347
381
|
accountId: id,
|
|
@@ -372,7 +406,16 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
|
|
|
372
406
|
}),
|
|
373
407
|
},
|
|
374
408
|
groups: {
|
|
375
|
-
resolveRequireMention: ({ cfg }: any): boolean =>
|
|
409
|
+
resolveRequireMention: ({ cfg, groupId }: any): boolean => {
|
|
410
|
+
const config = getConfig(cfg);
|
|
411
|
+
if (groupId) {
|
|
412
|
+
const groupCfg = resolveGroupConfig(config, groupId);
|
|
413
|
+
if (groupCfg?.requireMention !== undefined) {
|
|
414
|
+
return groupCfg.requireMention;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return config.groupPolicy !== "open";
|
|
418
|
+
},
|
|
376
419
|
resolveGroupIntroHint: ({ groupId, groupChannel }: any): string | undefined => {
|
|
377
420
|
const parts = [`conversationId=${groupId}`];
|
|
378
421
|
if (groupChannel) {
|
|
@@ -382,12 +425,20 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
|
|
|
382
425
|
},
|
|
383
426
|
},
|
|
384
427
|
messaging: {
|
|
385
|
-
normalizeTarget: (raw: string) => (raw ? raw
|
|
428
|
+
normalizeTarget: (raw: string) => (raw ? normalizeDingTalkTarget(raw) : undefined),
|
|
386
429
|
targetResolver: {
|
|
387
|
-
looksLikeId: (
|
|
388
|
-
|
|
430
|
+
looksLikeId: (raw: string, normalized?: string): boolean =>
|
|
431
|
+
looksLikeDingTalkTargetId(raw, normalized),
|
|
432
|
+
hint: "<displayName|conversationId|user:staffId|user:+861...>",
|
|
389
433
|
},
|
|
390
434
|
},
|
|
435
|
+
directory: {
|
|
436
|
+
self: async () => null,
|
|
437
|
+
listGroups: async (params) => listDingTalkDirectoryGroups(params),
|
|
438
|
+
listGroupsLive: async (params) => listDingTalkDirectoryGroups(params),
|
|
439
|
+
listPeers: async (params) => listDingTalkDirectoryUsers(params),
|
|
440
|
+
listPeersLive: async (params) => listDingTalkDirectoryUsers(params),
|
|
441
|
+
},
|
|
391
442
|
actions: dingtalkMessageActions,
|
|
392
443
|
outbound: {
|
|
393
444
|
deliveryMode: "direct" as const,
|
|
@@ -399,9 +450,7 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
|
|
|
399
450
|
error: new Error("DingTalk message requires --to <conversationId>"),
|
|
400
451
|
};
|
|
401
452
|
}
|
|
402
|
-
|
|
403
|
-
const resolved = resolveOriginalPeerId(targetId);
|
|
404
|
-
return { ok: true as const, to: resolved };
|
|
453
|
+
return { ok: true as const, to: normalizeResolvedDingTalkTarget(trimmed) };
|
|
405
454
|
},
|
|
406
455
|
sendText: async ({ cfg, to, text, accountId, log }: any) => {
|
|
407
456
|
const config = getConfig(cfg, accountId);
|
|
@@ -455,6 +504,7 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
|
|
|
455
504
|
mediaType: providedMediaType,
|
|
456
505
|
asVoice,
|
|
457
506
|
accountId,
|
|
507
|
+
mediaLocalRoots,
|
|
458
508
|
log,
|
|
459
509
|
}: any) => {
|
|
460
510
|
const config = getConfig(cfg, accountId);
|
|
@@ -490,12 +540,17 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
|
|
|
490
540
|
preparedMedia = await prepareMediaInput(rawMediaPath, log, config.mediaUrlAllowlist);
|
|
491
541
|
} catch (err: any) {
|
|
492
542
|
if (err?.response?.data !== undefined) {
|
|
493
|
-
log?.error?.(
|
|
543
|
+
log?.error?.(
|
|
544
|
+
formatDingTalkErrorPayloadLog("outbound.sendMedia.prepare", err.response.data),
|
|
545
|
+
);
|
|
494
546
|
}
|
|
495
547
|
const errorCode = typeof err?.code === "string" ? `[${err.code}] ` : "";
|
|
496
|
-
throw new Error(
|
|
497
|
-
|
|
498
|
-
|
|
548
|
+
throw new Error(
|
|
549
|
+
`remote media preparation failed: ${errorCode}${err?.message || "unknown error"}`,
|
|
550
|
+
{
|
|
551
|
+
cause: err,
|
|
552
|
+
},
|
|
553
|
+
);
|
|
499
554
|
}
|
|
500
555
|
|
|
501
556
|
const actualMediaPath = preparedMedia.cleanup
|
|
@@ -518,10 +573,13 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
|
|
|
518
573
|
accountId,
|
|
519
574
|
storePath,
|
|
520
575
|
conversationId: to,
|
|
576
|
+
mediaLocalRoots,
|
|
521
577
|
});
|
|
522
578
|
} catch (err: any) {
|
|
523
579
|
if (err?.response?.data !== undefined) {
|
|
524
|
-
log?.error?.(
|
|
580
|
+
log?.error?.(
|
|
581
|
+
formatDingTalkErrorPayloadLog("outbound.sendMedia.send", err.response.data),
|
|
582
|
+
);
|
|
525
583
|
}
|
|
526
584
|
throw new Error(`proactive media send failed: ${err?.message || "unknown error"}`, {
|
|
527
585
|
cause: err,
|
|
@@ -657,6 +715,7 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
|
|
|
657
715
|
if (!dedupKey) {
|
|
658
716
|
ctx.log?.warn?.(`[${account.accountId}] No message ID available for deduplication`);
|
|
659
717
|
stats.noMessageId += 1;
|
|
718
|
+
acknowledge();
|
|
660
719
|
await handleDingTalkMessage({
|
|
661
720
|
cfg,
|
|
662
721
|
accountId: account.accountId,
|
|
@@ -666,7 +725,6 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
|
|
|
666
725
|
dingtalkConfig: config,
|
|
667
726
|
});
|
|
668
727
|
stats.processed += 1;
|
|
669
|
-
acknowledge();
|
|
670
728
|
if (stats.received % INBOUND_COUNTER_LOG_EVERY === 0) {
|
|
671
729
|
logInboundCounters(ctx.log, account.accountId, "periodic");
|
|
672
730
|
}
|
|
@@ -693,11 +751,13 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
|
|
|
693
751
|
`[${account.accountId}] Skipping in-flight duplicate message: ${dedupKey}`,
|
|
694
752
|
);
|
|
695
753
|
stats.inflightSkipped += 1;
|
|
754
|
+
acknowledge();
|
|
696
755
|
logInboundCounters(ctx.log, account.accountId, "inflight-skipped");
|
|
697
756
|
return;
|
|
698
757
|
}
|
|
699
758
|
}
|
|
700
759
|
|
|
760
|
+
acknowledge();
|
|
701
761
|
processingDedupKeys.set(dedupKey, Date.now());
|
|
702
762
|
try {
|
|
703
763
|
await handleDingTalkMessage({
|
|
@@ -710,7 +770,6 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
|
|
|
710
770
|
});
|
|
711
771
|
stats.processed += 1;
|
|
712
772
|
markMessageProcessed(dedupKey);
|
|
713
|
-
acknowledge();
|
|
714
773
|
if (stats.received % INBOUND_COUNTER_LOG_EVERY === 0) {
|
|
715
774
|
logInboundCounters(ctx.log, account.accountId, "periodic");
|
|
716
775
|
}
|
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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as os from "node:os";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
|
3
|
+
import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
|
|
4
4
|
import type { DingTalkConfig } from "./types";
|
|
5
5
|
|
|
6
6
|
const WINDOWS_ROOT_DIRECTORIES = new Set([
|
|
@@ -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
|
/**
|
|
@@ -112,6 +112,18 @@ export class ConnectionManager {
|
|
|
112
112
|
}
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
/**
|
|
116
|
+
* Resolve all pending waitForStop() callers without performing full stop().
|
|
117
|
+
* Called by stop() and terminal FAILED states where the manager will never
|
|
118
|
+
* reconnect again, so startAccount must be allowed to exit.
|
|
119
|
+
*/
|
|
120
|
+
private resolveStopWaiters(): void {
|
|
121
|
+
for (const resolve of this.stopPromiseResolvers) {
|
|
122
|
+
resolve();
|
|
123
|
+
}
|
|
124
|
+
this.stopPromiseResolvers = [];
|
|
125
|
+
}
|
|
126
|
+
|
|
115
127
|
private logRuntimeCounters(reason: string): void {
|
|
116
128
|
const c = this.runtimeCounters;
|
|
117
129
|
this.log?.info?.(
|
|
@@ -722,6 +734,7 @@ export class ConnectionManager {
|
|
|
722
734
|
this.notifyStateChange(
|
|
723
735
|
`Max consecutive deadline timeouts (${ConnectionManager.MAX_CONSECUTIVE_DEADLINE_TIMEOUTS}) reached`,
|
|
724
736
|
);
|
|
737
|
+
this.resolveStopWaiters();
|
|
725
738
|
return;
|
|
726
739
|
}
|
|
727
740
|
|
|
@@ -755,6 +768,7 @@ export class ConnectionManager {
|
|
|
755
768
|
this.consecutiveUnhealthyChecks = 0;
|
|
756
769
|
this.reconnectDeadline = undefined;
|
|
757
770
|
this.notifyStateChange(`Max runtime reconnect cycles (${maxCycles}) reached`);
|
|
771
|
+
this.resolveStopWaiters();
|
|
758
772
|
return;
|
|
759
773
|
}
|
|
760
774
|
|
|
@@ -822,10 +836,7 @@ export class ConnectionManager {
|
|
|
822
836
|
this.log?.info?.(`[${this.accountId}] Connection manager stopped`);
|
|
823
837
|
|
|
824
838
|
// Resolve all pending waitForStop() promises
|
|
825
|
-
|
|
826
|
-
resolve();
|
|
827
|
-
}
|
|
828
|
-
this.stopPromiseResolvers = [];
|
|
839
|
+
this.resolveStopWaiters();
|
|
829
840
|
}
|
|
830
841
|
|
|
831
842
|
/**
|
|
@@ -835,7 +846,7 @@ export class ConnectionManager {
|
|
|
835
846
|
* Safe to call concurrently; all pending callers are resolved when stop() is called.
|
|
836
847
|
*/
|
|
837
848
|
public waitForStop(): Promise<void> {
|
|
838
|
-
if (this.stopped) {
|
|
849
|
+
if (this.stopped || this.state === ConnectionStateEnum.FAILED) {
|
|
839
850
|
return Promise.resolve();
|
|
840
851
|
}
|
|
841
852
|
return new Promise<void>((resolve) => {
|