@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.
- package/README.md +13 -24
- package/openclaw.plugin.json +695 -0
- package/package.json +12 -7
- package/src/ack-reaction-service.ts +1 -1
- package/src/auth.ts +1 -1
- package/src/card/card-action-handler.ts +1 -1
- package/src/card/card-stop-handler.ts +1 -1
- package/src/card/card-streaming-mode.ts +30 -0
- package/src/card/reasoning-answer-split.ts +162 -0
- package/src/card/reasoning-block-assembler.ts +157 -0
- package/src/card-callback-service.ts +1 -1
- package/src/card-draft-controller.ts +117 -6
- package/src/card-service.ts +112 -1
- package/src/channel.ts +131 -96
- package/src/command/card-stop-command.ts +4 -22
- package/src/command/inbound-command-dispatch-service.ts +464 -0
- package/src/config-schema.ts +62 -38
- package/src/config.ts +25 -3
- package/src/docs-service.ts +5 -5
- package/src/http-client.ts +20 -0
- package/src/inbound-handler.ts +475 -501
- package/src/logger-context.ts +16 -2
- package/src/media-utils.ts +166 -10
- package/src/message-utils.ts +33 -5
- package/src/{attachment-text-extractor.ts → messaging/attachment-text-extractor.ts} +1 -1
- package/src/{quoted-file-service.ts → messaging/quoted-file-service.ts} +14 -9
- package/src/onboarding.ts +29 -0
- package/src/plugin-sdk-channel-actions-augment.ts +11 -0
- package/src/reply-strategy-card.ts +294 -28
- package/src/reply-strategy-markdown.ts +124 -19
- package/src/reply-strategy.ts +22 -2
- package/src/send-service.ts +178 -7
- package/src/targeting/agent-routing.ts +55 -32
- package/src/{group-members-store.ts → targeting/group-members-store.ts} +1 -1
- package/src/types.ts +60 -4
- 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
|
+
}
|
package/src/config-schema.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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)
|
|
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)
|
|
31
|
+
/** DingTalk App Secret (Client Secret) used to obtain DingTalk access tokens. */
|
|
21
32
|
clientSecret: z.string().optional(),
|
|
22
33
|
|
|
23
|
-
/** Direct
|
|
34
|
+
/** Direct-message access policy: open, pairing, or allowlist. */
|
|
24
35
|
dmPolicy: z.enum(["open", "pairing", "allowlist"]).optional().default("open"),
|
|
25
36
|
|
|
26
|
-
/** Group
|
|
37
|
+
/** Group-message access policy: open, allowlist, or disabled. */
|
|
27
38
|
groupPolicy: z.enum(["open", "allowlist", "disabled"]).optional().default("open"),
|
|
28
39
|
|
|
29
|
-
/**
|
|
40
|
+
/** User IDs allowed when `dmPolicy` is `allowlist`. */
|
|
30
41
|
allowFrom: z.array(z.string()).optional(),
|
|
31
42
|
|
|
32
|
-
/**
|
|
43
|
+
/** Sender IDs allowed when `groupPolicy` is `allowlist`. */
|
|
33
44
|
groupAllowFrom: z.array(z.string()).optional(),
|
|
34
45
|
|
|
35
|
-
/** Default disabled. Enabling
|
|
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
|
|
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
|
-
/**
|
|
63
|
+
/** Default reply delivery mode: markdown or card. */
|
|
48
64
|
messageType: z.enum(["markdown", "card"]).optional().default("markdown"),
|
|
49
65
|
|
|
50
|
-
/**
|
|
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
|
-
/**
|
|
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
|
|
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
|
|
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
|
|
92
|
+
/** Initial reconnect backoff delay in milliseconds. */
|
|
80
93
|
initialReconnectDelay: z.number().int().min(100).optional().default(1000),
|
|
81
94
|
|
|
82
|
-
/**
|
|
95
|
+
/** Upper bound for reconnect backoff delay in milliseconds. */
|
|
83
96
|
maxReconnectDelay: z.number().int().min(1000).optional().default(60000),
|
|
84
97
|
|
|
85
|
-
/**
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
|
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
|
-
/**
|
|
113
|
+
/** Enable the underlying Stream client heartbeat. When omitted, runtime derives a default from `useConnectionManager`. */
|
|
101
114
|
keepAlive: z.boolean().optional(),
|
|
102
|
-
/** Bypass
|
|
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
|
-
/**
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
/**
|
|
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
|
|
143
|
+
/** Enable the local feedback-learning loop for notes, reflections, and command-assisted learning. */
|
|
120
144
|
learningEnabled: z.boolean().optional(),
|
|
121
145
|
|
|
122
|
-
/**
|
|
146
|
+
/** Automatically apply generated learning output into session notes or global rules when available. */
|
|
123
147
|
learningAutoApply: z.boolean().optional(),
|
|
124
148
|
|
|
125
|
-
/**
|
|
149
|
+
/** Retention window in milliseconds for temporary learning notes. */
|
|
126
150
|
learningNoteTtlMs: z.number().int().min(60_000).optional(),
|
|
127
151
|
|
|
128
|
-
/**
|
|
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.
|