@soimy/dingtalk 3.5.1 → 3.5.3

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.
Files changed (36) hide show
  1. package/README.md +13 -24
  2. package/openclaw.plugin.json +695 -0
  3. package/package.json +12 -7
  4. package/src/ack-reaction-service.ts +1 -1
  5. package/src/auth.ts +1 -1
  6. package/src/card/card-action-handler.ts +1 -1
  7. package/src/card/card-stop-handler.ts +1 -1
  8. package/src/card/card-streaming-mode.ts +30 -0
  9. package/src/card/reasoning-answer-split.ts +162 -0
  10. package/src/card/reasoning-block-assembler.ts +157 -0
  11. package/src/card-callback-service.ts +1 -1
  12. package/src/card-draft-controller.ts +117 -6
  13. package/src/card-service.ts +112 -1
  14. package/src/channel.ts +131 -96
  15. package/src/command/card-stop-command.ts +4 -22
  16. package/src/command/inbound-command-dispatch-service.ts +464 -0
  17. package/src/config-schema.ts +62 -38
  18. package/src/config.ts +25 -3
  19. package/src/docs-service.ts +5 -5
  20. package/src/http-client.ts +20 -0
  21. package/src/inbound-handler.ts +475 -501
  22. package/src/logger-context.ts +16 -2
  23. package/src/media-utils.ts +166 -10
  24. package/src/message-utils.ts +33 -5
  25. package/src/{attachment-text-extractor.ts → messaging/attachment-text-extractor.ts} +1 -1
  26. package/src/{quoted-file-service.ts → messaging/quoted-file-service.ts} +14 -9
  27. package/src/onboarding.ts +29 -0
  28. package/src/plugin-sdk-channel-actions-augment.ts +11 -0
  29. package/src/reply-strategy-card.ts +294 -28
  30. package/src/reply-strategy-markdown.ts +124 -19
  31. package/src/reply-strategy.ts +22 -2
  32. package/src/send-service.ts +178 -7
  33. package/src/targeting/agent-routing.ts +55 -32
  34. package/src/{group-members-store.ts → targeting/group-members-store.ts} +1 -1
  35. package/src/types.ts +60 -4
  36. package/src/utils.ts +190 -0
@@ -0,0 +1,464 @@
1
+ import {
2
+ applyManualGlobalLearningRule,
3
+ applyManualSessionLearningNote,
4
+ applyManualTargetLearningRule,
5
+ applyManualTargetsLearningRule,
6
+ applyTargetSetLearningRule,
7
+ createOrUpdateTargetSet,
8
+ deleteManualRule,
9
+ disableManualRule,
10
+ listLearningTargetSets,
11
+ listScopedLearningRules,
12
+ resolveManualForcedReply,
13
+ } from "../feedback-learning-service";
14
+ import {
15
+ formatLearnAppliedReply,
16
+ formatLearnCommandHelp,
17
+ formatLearnDeletedReply,
18
+ formatLearnDisabledReply,
19
+ formatLearnListReply,
20
+ formatOwnerOnlyDeniedReply,
21
+ formatOwnerStatusReply,
22
+ formatTargetSetSavedReply,
23
+ formatWhereAmIReply,
24
+ formatWhoAmIReply,
25
+ isLearningOwner,
26
+ parseLearnCommand,
27
+ } from "../learning-command-service";
28
+ import {
29
+ formatSessionAliasBoundReply,
30
+ formatSessionAliasClearedReply,
31
+ formatSessionAliasReply,
32
+ formatSessionAliasSetReply,
33
+ formatSessionAliasUnboundReply,
34
+ formatSessionAliasValidationErrorReply,
35
+ parseSessionCommand,
36
+ validateSessionAlias,
37
+ } from "../session-command-service";
38
+ import type { SessionPeerSourceKind } from "../session-peer-store";
39
+ import type { DingTalkConfig, HandleDingTalkMessageParams, MessageContent } from "../types";
40
+
41
+ type InboundCommandDispatchParams = {
42
+ cfg: HandleDingTalkMessageParams["cfg"];
43
+ accountId: string;
44
+ dingtalkConfig: DingTalkConfig;
45
+ senderId: string;
46
+ isDirect: boolean;
47
+ extractedText: string;
48
+ messageType: MessageContent["messageType"];
49
+ data: {
50
+ conversationId: string;
51
+ senderId?: string;
52
+ senderStaffId?: string;
53
+ };
54
+ accountStorePath: string;
55
+ currentSessionSourceKind: SessionPeerSourceKind;
56
+ currentSessionSourceId: string;
57
+ peerIdOverride?: string;
58
+ sessionPeer: {
59
+ peerId: string;
60
+ };
61
+ sendReply: (text: string) => Promise<void>;
62
+ clearSessionPeerOverride: (params: {
63
+ storePath: string;
64
+ accountId: string;
65
+ sourceKind: SessionPeerSourceKind;
66
+ sourceId: string;
67
+ }) => boolean;
68
+ setSessionPeerOverride: (params: {
69
+ storePath: string;
70
+ accountId: string;
71
+ sourceKind: SessionPeerSourceKind;
72
+ sourceId: string;
73
+ peerId: string;
74
+ }) => void;
75
+ };
76
+
77
+ export async function handleInboundCommandDispatch(
78
+ params: InboundCommandDispatchParams,
79
+ ): Promise<boolean> {
80
+ const parsedLearnCommand = parseLearnCommand(params.extractedText);
81
+ const parsedSessionCommand = parseSessionCommand(params.extractedText);
82
+ const isOwner = isLearningOwner({
83
+ cfg: params.cfg,
84
+ config: params.dingtalkConfig,
85
+ senderId: params.senderId,
86
+ rawSenderId: params.data.senderId,
87
+ });
88
+
89
+ if (params.isDirect && parsedLearnCommand.scope === "whoami") {
90
+ await params.sendReply(
91
+ formatWhoAmIReply({
92
+ senderId: params.senderId,
93
+ rawSenderId: params.data.senderId,
94
+ senderStaffId: params.data.senderStaffId,
95
+ isOwner,
96
+ }),
97
+ );
98
+ return true;
99
+ }
100
+
101
+ if (parsedLearnCommand.scope === "whereami") {
102
+ await params.sendReply(
103
+ formatWhereAmIReply({
104
+ conversationId: params.data.conversationId,
105
+ conversationType: params.isDirect ? "dm" : "group",
106
+ peerId: params.sessionPeer.peerId,
107
+ }),
108
+ );
109
+ return true;
110
+ }
111
+
112
+ if (params.isDirect && parsedLearnCommand.scope === "owner-status") {
113
+ await params.sendReply(
114
+ formatOwnerStatusReply({
115
+ senderId: params.senderId,
116
+ rawSenderId: params.data.senderId,
117
+ isOwner,
118
+ }),
119
+ );
120
+ return true;
121
+ }
122
+
123
+ if (parsedLearnCommand.scope === "help") {
124
+ await params.sendReply(formatLearnCommandHelp());
125
+ return true;
126
+ }
127
+
128
+ if (
129
+ (parsedLearnCommand.scope === "global" ||
130
+ parsedLearnCommand.scope === "session" ||
131
+ parsedLearnCommand.scope === "here" ||
132
+ parsedLearnCommand.scope === "target" ||
133
+ parsedLearnCommand.scope === "targets" ||
134
+ parsedLearnCommand.scope === "list" ||
135
+ parsedLearnCommand.scope === "disable" ||
136
+ parsedLearnCommand.scope === "delete" ||
137
+ parsedLearnCommand.scope === "target-set-create" ||
138
+ parsedLearnCommand.scope === "target-set-apply" ||
139
+ parsedSessionCommand.scope === "session-alias-show" ||
140
+ parsedSessionCommand.scope === "session-alias-set" ||
141
+ parsedSessionCommand.scope === "session-alias-clear" ||
142
+ parsedSessionCommand.scope === "session-alias-bind" ||
143
+ parsedSessionCommand.scope === "session-alias-unbind") &&
144
+ !isOwner
145
+ ) {
146
+ await params.sendReply(formatOwnerOnlyDeniedReply());
147
+ return true;
148
+ }
149
+
150
+ if (isOwner) {
151
+ if (parsedSessionCommand.scope === "session-alias-show") {
152
+ await params.sendReply(
153
+ formatSessionAliasReply({
154
+ sourceKind: params.currentSessionSourceKind,
155
+ sourceId: params.currentSessionSourceId,
156
+ peerId: params.sessionPeer.peerId,
157
+ aliasSource: params.peerIdOverride ? "override" : "default",
158
+ }),
159
+ );
160
+ return true;
161
+ }
162
+
163
+ if (parsedSessionCommand.scope === "session-alias-set" && parsedSessionCommand.peerId) {
164
+ const aliasValidationError = validateSessionAlias(parsedSessionCommand.peerId);
165
+ if (aliasValidationError) {
166
+ await params.sendReply(formatSessionAliasValidationErrorReply(aliasValidationError));
167
+ return true;
168
+ }
169
+ params.setSessionPeerOverride({
170
+ storePath: params.accountStorePath,
171
+ accountId: params.accountId,
172
+ sourceKind: params.currentSessionSourceKind,
173
+ sourceId: params.currentSessionSourceId,
174
+ peerId: parsedSessionCommand.peerId,
175
+ });
176
+ await params.sendReply(
177
+ formatSessionAliasSetReply({
178
+ sourceKind: params.currentSessionSourceKind,
179
+ sourceId: params.currentSessionSourceId,
180
+ peerId: parsedSessionCommand.peerId,
181
+ }),
182
+ );
183
+ return true;
184
+ }
185
+
186
+ if (parsedSessionCommand.scope === "session-alias-clear") {
187
+ params.clearSessionPeerOverride({
188
+ storePath: params.accountStorePath,
189
+ accountId: params.accountId,
190
+ sourceKind: params.currentSessionSourceKind,
191
+ sourceId: params.currentSessionSourceId,
192
+ });
193
+ await params.sendReply(
194
+ formatSessionAliasClearedReply({
195
+ sourceKind: params.currentSessionSourceKind,
196
+ sourceId: params.currentSessionSourceId,
197
+ }),
198
+ );
199
+ return true;
200
+ }
201
+
202
+ if (
203
+ parsedSessionCommand.scope === "session-alias-bind" &&
204
+ parsedSessionCommand.sourceKind &&
205
+ parsedSessionCommand.sourceId &&
206
+ parsedSessionCommand.peerId
207
+ ) {
208
+ const aliasValidationError = validateSessionAlias(parsedSessionCommand.peerId);
209
+ if (aliasValidationError) {
210
+ await params.sendReply(formatSessionAliasValidationErrorReply(aliasValidationError));
211
+ return true;
212
+ }
213
+ params.setSessionPeerOverride({
214
+ storePath: params.accountStorePath,
215
+ accountId: params.accountId,
216
+ sourceKind: parsedSessionCommand.sourceKind,
217
+ sourceId: parsedSessionCommand.sourceId,
218
+ peerId: parsedSessionCommand.peerId,
219
+ });
220
+ await params.sendReply(
221
+ formatSessionAliasBoundReply({
222
+ sourceKind: parsedSessionCommand.sourceKind,
223
+ sourceId: parsedSessionCommand.sourceId,
224
+ peerId: parsedSessionCommand.peerId,
225
+ }),
226
+ );
227
+ return true;
228
+ }
229
+
230
+ if (
231
+ parsedSessionCommand.scope === "session-alias-unbind" &&
232
+ parsedSessionCommand.sourceKind &&
233
+ parsedSessionCommand.sourceId
234
+ ) {
235
+ const existed = params.clearSessionPeerOverride({
236
+ storePath: params.accountStorePath,
237
+ accountId: params.accountId,
238
+ sourceKind: parsedSessionCommand.sourceKind,
239
+ sourceId: parsedSessionCommand.sourceId,
240
+ });
241
+ await params.sendReply(
242
+ formatSessionAliasUnboundReply({
243
+ sourceKind: parsedSessionCommand.sourceKind,
244
+ sourceId: parsedSessionCommand.sourceId,
245
+ existed,
246
+ }),
247
+ );
248
+ return true;
249
+ }
250
+
251
+ if (parsedLearnCommand.scope === "global" && parsedLearnCommand.instruction) {
252
+ const applied = applyManualGlobalLearningRule({
253
+ storePath: params.accountStorePath,
254
+ accountId: params.accountId,
255
+ instruction: parsedLearnCommand.instruction,
256
+ });
257
+ await params.sendReply(
258
+ formatLearnAppliedReply({
259
+ scope: "global",
260
+ instruction: parsedLearnCommand.instruction,
261
+ ruleId: applied?.ruleId,
262
+ }),
263
+ );
264
+ return true;
265
+ }
266
+
267
+ if (parsedLearnCommand.scope === "session" && parsedLearnCommand.instruction) {
268
+ applyManualSessionLearningNote({
269
+ storePath: params.accountStorePath,
270
+ accountId: params.accountId,
271
+ targetId: params.data.conversationId,
272
+ instruction: parsedLearnCommand.instruction,
273
+ });
274
+ await params.sendReply(
275
+ formatLearnAppliedReply({
276
+ scope: "session",
277
+ instruction: parsedLearnCommand.instruction,
278
+ }),
279
+ );
280
+ return true;
281
+ }
282
+
283
+ if (parsedLearnCommand.scope === "here" && parsedLearnCommand.instruction) {
284
+ const applied = applyManualTargetLearningRule({
285
+ storePath: params.accountStorePath,
286
+ accountId: params.accountId,
287
+ targetId: params.data.conversationId,
288
+ instruction: parsedLearnCommand.instruction,
289
+ });
290
+ await params.sendReply(
291
+ formatLearnAppliedReply({
292
+ scope: "target",
293
+ targetId: params.data.conversationId,
294
+ instruction: parsedLearnCommand.instruction,
295
+ ruleId: applied?.ruleId,
296
+ }),
297
+ );
298
+ return true;
299
+ }
300
+
301
+ if (
302
+ parsedLearnCommand.scope === "target" &&
303
+ parsedLearnCommand.targetId &&
304
+ parsedLearnCommand.instruction
305
+ ) {
306
+ const applied = applyManualTargetLearningRule({
307
+ storePath: params.accountStorePath,
308
+ accountId: params.accountId,
309
+ targetId: parsedLearnCommand.targetId,
310
+ instruction: parsedLearnCommand.instruction,
311
+ });
312
+ await params.sendReply(
313
+ formatLearnAppliedReply({
314
+ scope: "target",
315
+ targetId: parsedLearnCommand.targetId,
316
+ instruction: parsedLearnCommand.instruction,
317
+ ruleId: applied?.ruleId,
318
+ }),
319
+ );
320
+ return true;
321
+ }
322
+
323
+ if (
324
+ parsedLearnCommand.scope === "targets" &&
325
+ parsedLearnCommand.targetIds?.length &&
326
+ parsedLearnCommand.instruction
327
+ ) {
328
+ const applied = applyManualTargetsLearningRule({
329
+ storePath: params.accountStorePath,
330
+ accountId: params.accountId,
331
+ targetIds: parsedLearnCommand.targetIds,
332
+ instruction: parsedLearnCommand.instruction,
333
+ });
334
+ await params.sendReply(
335
+ formatLearnAppliedReply({
336
+ scope: "targets",
337
+ targetIds: parsedLearnCommand.targetIds,
338
+ instruction: parsedLearnCommand.instruction,
339
+ ruleId: applied[0]?.ruleId,
340
+ }),
341
+ );
342
+ return true;
343
+ }
344
+
345
+ if (
346
+ parsedLearnCommand.scope === "target-set-create" &&
347
+ parsedLearnCommand.setName &&
348
+ parsedLearnCommand.targetIds?.length
349
+ ) {
350
+ const saved = createOrUpdateTargetSet({
351
+ storePath: params.accountStorePath,
352
+ accountId: params.accountId,
353
+ name: parsedLearnCommand.setName,
354
+ targetIds: parsedLearnCommand.targetIds,
355
+ });
356
+ await params.sendReply(
357
+ saved
358
+ ? formatTargetSetSavedReply({
359
+ setName: parsedLearnCommand.setName,
360
+ targetIds: parsedLearnCommand.targetIds,
361
+ })
362
+ : "目标组保存失败,请检查名称和目标列表。",
363
+ );
364
+ return true;
365
+ }
366
+
367
+ if (
368
+ parsedLearnCommand.scope === "target-set-apply" &&
369
+ parsedLearnCommand.setName &&
370
+ parsedLearnCommand.instruction
371
+ ) {
372
+ const applied = applyTargetSetLearningRule({
373
+ storePath: params.accountStorePath,
374
+ accountId: params.accountId,
375
+ name: parsedLearnCommand.setName,
376
+ instruction: parsedLearnCommand.instruction,
377
+ });
378
+ await params.sendReply(
379
+ applied.length > 0
380
+ ? formatLearnAppliedReply({
381
+ scope: "target-set",
382
+ setName: parsedLearnCommand.setName,
383
+ targetIds: applied.map((item) => item.targetId),
384
+ instruction: parsedLearnCommand.instruction,
385
+ ruleId: applied[0]?.ruleId,
386
+ })
387
+ : `未找到目标组 \`${parsedLearnCommand.setName}\`,或该目标组为空。`,
388
+ );
389
+ return true;
390
+ }
391
+
392
+ if (parsedLearnCommand.scope === "list") {
393
+ const rules = listScopedLearningRules({
394
+ storePath: params.accountStorePath,
395
+ accountId: params.accountId,
396
+ })
397
+ .slice(0, 20)
398
+ .map((rule) => {
399
+ const scope = rule.scope === "target" ? `target(${rule.targetId})` : "global";
400
+ const status = rule.enabled ? "enabled" : "disabled";
401
+ return `- [${scope}] ${rule.ruleId} (${status}) => ${rule.instruction}`;
402
+ });
403
+ const targetSets = listLearningTargetSets({
404
+ storePath: params.accountStorePath,
405
+ accountId: params.accountId,
406
+ })
407
+ .slice(0, 10)
408
+ .map((targetSet) => `- [target-set] ${targetSet.name} => ${targetSet.targetIds.join(", ")}`);
409
+ await params.sendReply(formatLearnListReply([...rules, ...targetSets]));
410
+ return true;
411
+ }
412
+
413
+ if (parsedLearnCommand.scope === "disable" && parsedLearnCommand.ruleId) {
414
+ const result = disableManualRule({
415
+ storePath: params.accountStorePath,
416
+ accountId: params.accountId,
417
+ ruleId: parsedLearnCommand.ruleId,
418
+ });
419
+ await params.sendReply(
420
+ formatLearnDisabledReply({
421
+ ruleId: parsedLearnCommand.ruleId,
422
+ existed: result.existed,
423
+ scope: result.scope,
424
+ targetId: result.targetId,
425
+ }),
426
+ );
427
+ return true;
428
+ }
429
+
430
+ if (parsedLearnCommand.scope === "delete" && parsedLearnCommand.ruleId) {
431
+ const result = deleteManualRule({
432
+ storePath: params.accountStorePath,
433
+ accountId: params.accountId,
434
+ ruleId: parsedLearnCommand.ruleId,
435
+ });
436
+ await params.sendReply(
437
+ formatLearnDeletedReply({
438
+ ruleId: parsedLearnCommand.ruleId,
439
+ existed: result.existed,
440
+ scope: result.scope,
441
+ targetId: result.targetId,
442
+ }),
443
+ );
444
+ return true;
445
+ }
446
+ }
447
+
448
+ const forcedContent: MessageContent = {
449
+ text: params.extractedText,
450
+ messageType: params.messageType,
451
+ };
452
+ const manualForcedReply = resolveManualForcedReply({
453
+ storePath: params.accountStorePath,
454
+ accountId: params.accountId,
455
+ targetId: params.data.conversationId,
456
+ content: forcedContent,
457
+ });
458
+ if (manualForcedReply) {
459
+ await params.sendReply(manualForcedReply);
460
+ return true;
461
+ }
462
+
463
+ return false;
464
+ }
@@ -7,65 +7,78 @@ const AckReactionSchema = z.union([
7
7
  z.string().min(1),
8
8
  ]);
9
9
 
10
+ const CardStreamingModeSchema = z.enum(["off", "answer", "all"]);
11
+ const ContextVisibilitySchema = z.enum(["all", "allowlist", "allowlist_quote"]);
12
+
13
+ /**
14
+ * Runtime-parsed DingTalk account config.
15
+ *
16
+ * Compatibility note:
17
+ * - `agentId`, `corpId`, `showThinkingStream`, and `asyncMode` are intentionally
18
+ * not parsed here. They remain only in manifest metadata for legacy host/UI
19
+ * compatibility and are ignored by the current runtime.
20
+ */
10
21
  const DingTalkAccountConfigShape = {
11
22
  /** Account name (optional display name) */
12
23
  name: z.string().optional(),
13
24
 
14
- /** Whether this channel is enabled */
25
+ /** Enable or disable this DingTalk channel/account without deleting saved credentials. */
15
26
  enabled: z.boolean().optional().default(true),
16
27
 
17
- /** DingTalk App Key (Client ID) - required for authentication */
28
+ /** DingTalk App Key (Client ID) used to authenticate API and Stream connections. */
18
29
  clientId: z.string().optional(),
19
30
 
20
- /** DingTalk App Secret (Client Secret) - required for authentication */
31
+ /** DingTalk App Secret (Client Secret) used to obtain DingTalk access tokens. */
21
32
  clientSecret: z.string().optional(),
22
33
 
23
- /** Direct message policy: open, pairing, or allowlist */
34
+ /** Direct-message access policy: open, pairing, or allowlist. */
24
35
  dmPolicy: z.enum(["open", "pairing", "allowlist"]).optional().default("open"),
25
36
 
26
- /** Group message policy: open, allowlist, or disabled */
37
+ /** Group-message access policy: open, allowlist, or disabled. */
27
38
  groupPolicy: z.enum(["open", "allowlist", "disabled"]).optional().default("open"),
28
39
 
29
- /** List of allowed user IDs for allowlist policy */
40
+ /** User IDs allowed when `dmPolicy` is `allowlist`. */
30
41
  allowFrom: z.array(z.string()).optional(),
31
42
 
32
- /** List of allowed user IDs for group allowlist policy */
43
+ /** Sender IDs allowed when `groupPolicy` is `allowlist`. */
33
44
  groupAllowFrom: z.array(z.string()).optional(),
34
45
 
35
- /** 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. */
46
+ /** Default disabled. Enabling `all` allows learned displayName lookup but may misroute on stale or duplicate names and is available to all callers until upstream exposes requester authz context. */
36
47
  displayNameResolution: z.enum(["disabled", "all"]).optional().default("disabled"),
37
48
 
49
+ /** Controls how much supplemental host context remains visible to the reply runtime. `allowlist_quote` is the safest advanced mode when only explicit quotes or replies should remain visible. */
50
+ contextVisibility: ContextVisibilitySchema.optional(),
51
+
52
+ /** Allowed remote media download hosts, IPs, or CIDRs for media fetches. */
38
53
  mediaUrlAllowlist: z.array(z.string()).optional(),
39
54
 
40
- /** Native ack reaction mode: off, emoji, or kaomoji */
55
+ /** Native acknowledgement reaction mode: off, emoji, kaomoji, or a custom compatibility string. */
41
56
  ackReaction: AckReactionSchema.optional(),
42
57
 
58
+ /** Retention window in days for short-lived message context used by quoting and media recovery. */
43
59
  journalTTLDays: z.number().int().min(1).optional().default(DEFAULT_MESSAGE_CONTEXT_TTL_DAYS),
44
- /** Enable debug logging */
60
+ /** Enable verbose DingTalk channel debug logging. */
45
61
  debug: z.boolean().optional().default(false),
46
62
 
47
- /** Message type for replies: markdown or card */
63
+ /** Default reply delivery mode: markdown or card. */
48
64
  messageType: z.enum(["markdown", "card"]).optional().default("markdown"),
49
65
 
50
- /** Card template ID for AI interactive cards
51
- * obtain the template ID from DingTalk Developer Console.
52
- * ref: https://github.com/soimy/openclaw-channel-dingtalk/blob/main/README.md#3-%E5%BB%BA%E7%AB%8B%E5%8D%A1%E7%89%87%E6%A8%A1%E6%9D%BF%E5%8F%AF%E9%80%89
53
- */
66
+ /** Deprecated and ignored. AI card replies now always use the built-in DingTalk template contract. Keep only for backward-compatible config parsing. */
54
67
  cardTemplateId: z.string().optional(),
55
68
 
56
- /** Card template key for streaming updates
57
- * Default: 'content' - maps to the content field in the card template
58
- * This key is used in the streaming API to update specific fields in the card.
59
- */
69
+ /** Deprecated and ignored. The built-in AI card contract owns the streaming field mapping. Keep only for backward-compatible config parsing. */
60
70
  cardTemplateKey: z.string().optional().default("content"),
61
71
 
62
- /** Per-group configuration, keyed by conversationId (supports "*" wildcard) */
72
+ /** Per-group overrides keyed by conversationId. Supports `*` as a wildcard fallback. */
63
73
  groups: z
64
74
  .record(
65
75
  z.string(),
66
76
  z.object({
77
+ /** Additional system prompt appended for this group. */
67
78
  systemPrompt: z.string().optional(),
79
+ /** Require an explicit @mention before the bot answers in this group. */
68
80
  requireMention: z.boolean().optional(),
81
+ /** Optional per-group sender allowlist for tighter access control than the channel default. */
69
82
  groupAllowFrom: z.array(z.string()).optional(),
70
83
  }),
71
84
  )
@@ -73,59 +86,70 @@ const DingTalkAccountConfigShape = {
73
86
 
74
87
  /** Connection robustness configuration */
75
88
 
76
- /** Maximum number of connection attempts before giving up (default: 10) */
89
+ /** Maximum connection attempts in a single reconnect cycle before backing off or giving up. */
77
90
  maxConnectionAttempts: z.number().int().min(1).optional().default(10),
78
91
 
79
- /** Initial reconnection delay in milliseconds (default: 1000ms) */
92
+ /** Initial reconnect backoff delay in milliseconds. */
80
93
  initialReconnectDelay: z.number().int().min(100).optional().default(1000),
81
94
 
82
- /** Maximum reconnection delay in milliseconds for exponential backoff (default: 60000ms = 1 minute) */
95
+ /** Upper bound for reconnect backoff delay in milliseconds. */
83
96
  maxReconnectDelay: z.number().int().min(1000).optional().default(60000),
84
97
 
85
- /** Jitter factor for reconnection delay randomization (0-1, default: 0.3) */
98
+ /** Randomization factor added to reconnect backoff to avoid synchronized reconnect storms. */
86
99
  reconnectJitter: z.number().min(0).max(1).optional().default(0.3),
87
100
 
88
- /** Maximum number of runtime reconnect cycles before giving up (default: 10) */
101
+ /** Maximum reconnect cycles before the channel stops retrying and waits for the next lifecycle restart. */
89
102
  maxReconnectCycles: z.number().int().min(1).optional().default(10),
90
103
 
91
- /** Maximum time (ms) for a single reconnect cycle before starting a new cycle (default: 50000) */
104
+ /** Time limit in milliseconds for one reconnect cycle before starting a fresh cycle. */
92
105
  reconnectDeadlineMs: z.number().int().min(5000).optional().default(50000),
93
106
 
94
- /** Whether to use ConnectionManager (default: true). When false, rely on DWClient native keepAlive+autoReconnect. */
107
+ /** Enable the plugin connection manager. Disable only when you intentionally rely on DWClient native keepAlive plus autoReconnect behavior. */
95
108
  useConnectionManager: z.boolean().optional().default(true),
96
109
 
97
- /** Maximum inbound media file size in MB (overrides runtime default when set) */
110
+ /** Maximum inbound media size in MB accepted by the plugin. When omitted, the runtime default is used. */
98
111
  mediaMaxMb: z.number().int().min(1).optional(),
99
112
 
100
- /** Whether to enable underlying stream keepAlive heartbeat; defaults to !useConnectionManager when omitted */
113
+ /** Enable the underlying Stream client heartbeat. When omitted, runtime derives a default from `useConnectionManager`. */
101
114
  keepAlive: z.boolean().optional(),
102
- /** Bypass system/global HTTP(S) proxy for DingTalk outbound send/card/upload APIs */
115
+ /** Bypass global or system HTTP(S) proxy settings for DingTalk send, upload, and card APIs. */
103
116
  bypassProxyForSend: z.boolean().optional().default(false),
117
+ /** Controls the proactive-send permission reminder shown when a conversation has not granted send rights yet. */
104
118
  proactivePermissionHint: z
105
119
  .object({
120
+ /** Show the proactive-send permission hint when the runtime detects missing DingTalk proactive permission. */
106
121
  enabled: z.boolean().optional().default(true),
122
+ /** Minimum cooldown in hours before the same proactive permission hint can be shown again. */
107
123
  cooldownHours: z.number().int().min(1).max(24 * 30).optional().default(24),
108
124
  })
109
125
  .optional()
110
126
  .default({ enabled: true, cooldownHours: 24 }),
111
127
 
112
- /** Enable real-time card streaming (default: false).
113
- * When true, card updates are streamed per-token with 300ms throttle for a smoother experience, at the cost of more API calls. */
114
- cardRealTimeStream: z.boolean().optional().default(false),
128
+ /** Deprecated compatibility flag. When true and `cardStreamingMode` is unset, runtime resolves to `cardStreamingMode: "all"`. Do not use in new configs. */
129
+ cardRealTimeStream: z.boolean().optional(),
130
+
131
+ /** Card streaming mode:
132
+ * - off: disable incremental streaming
133
+ * - answer: stream answer text
134
+ * - all: stream answer + reasoning or thinking text */
135
+ cardStreamingMode: CardStreamingModeSchema.optional(),
136
+
137
+ /** Throttle interval in milliseconds between AI card streaming updates. */
138
+ cardStreamInterval: z.number().int().min(200).optional().default(1000),
115
139
 
116
- /** AICard degrade duration in milliseconds after trigger errors (default: 30 minutes) */
140
+ /** Cooldown window in milliseconds after AI card trigger errors. Replies fall back to non-card delivery during this period. */
117
141
  aicardDegradeMs: z.number().int().min(60_000).optional().default(30 * 60 * 1000),
118
142
 
119
- /** Enable local learning loop (default: false) */
143
+ /** Enable the local feedback-learning loop for notes, reflections, and command-assisted learning. */
120
144
  learningEnabled: z.boolean().optional(),
121
145
 
122
- /** Auto-apply generated reflections into session notes/global rules (default: false) */
146
+ /** Automatically apply generated learning output into session notes or global rules when available. */
123
147
  learningAutoApply: z.boolean().optional(),
124
148
 
125
- /** Session learning note TTL in milliseconds (default: 6 hours) */
149
+ /** Retention window in milliseconds for temporary learning notes. */
126
150
  learningNoteTtlMs: z.number().int().min(60_000).optional(),
127
151
 
128
- /** Whether to convert markdown tables to plain text for better rendering on some clients (default: true) */
152
+ /** Convert markdown tables to plain text before sending when you want more consistent DingTalk rendering. */
129
153
  convertMarkdownTables: z.boolean().optional().default(true),
130
154
 
131
155
  /** @mention the sender after card finalization in group chats.