@soimy/dingtalk 3.1.4 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +808 -40
- package/index.ts +62 -0
- package/package.json +4 -2
- package/src/access-control.ts +18 -0
- package/src/ack-reaction-classifier.ts +62 -0
- package/src/ack-reaction-service.ts +135 -0
- package/src/attachment-text-extractor.ts +147 -0
- package/src/card-callback-service.ts +119 -0
- package/src/card-draft-controller.ts +114 -0
- package/src/card-service.ts +794 -62
- package/src/channel.ts +675 -179
- package/src/config-schema.ts +49 -5
- package/src/config.ts +136 -4
- package/src/connection-manager.ts +356 -36
- package/src/dedup.ts +1 -0
- package/src/docs-service.ts +198 -0
- package/src/draft-stream-loop.ts +119 -0
- package/src/feedback-learning-service.ts +643 -0
- package/src/feedback-learning-store.ts +543 -0
- package/src/group-members-store.ts +48 -14
- package/src/inbound-handler.ts +1191 -206
- package/src/learning-command-service.ts +339 -0
- package/src/media-utils.ts +566 -8
- package/src/message-utils.ts +301 -39
- package/src/onboarding.ts +85 -3
- package/src/peer-id-registry.ts +102 -0
- package/src/persistence-store.ts +131 -0
- package/src/quote-journal.ts +242 -0
- package/src/quoted-file-service.ts +385 -0
- package/src/quoted-msg-cache.ts +226 -0
- package/src/send-service.ts +239 -59
- package/src/session-command-service.ts +147 -0
- package/src/session-lock.ts +34 -0
- package/src/session-peer-store.ts +77 -0
- package/src/session-routing.ts +33 -0
- package/src/types.ts +165 -22
- package/src/utils.ts +231 -12
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import { normalizeAllowFrom, isSenderOwner } from "./access-control";
|
|
2
|
+
import type { DingTalkConfig } from "./types";
|
|
3
|
+
|
|
4
|
+
export interface ParsedLearnCommand {
|
|
5
|
+
scope:
|
|
6
|
+
| "help"
|
|
7
|
+
| "global"
|
|
8
|
+
| "session"
|
|
9
|
+
| "here"
|
|
10
|
+
| "target"
|
|
11
|
+
| "targets"
|
|
12
|
+
| "list"
|
|
13
|
+
| "disable"
|
|
14
|
+
| "delete"
|
|
15
|
+
| "whoami"
|
|
16
|
+
| "whereami"
|
|
17
|
+
| "owner-status"
|
|
18
|
+
| "target-set-create"
|
|
19
|
+
| "target-set-apply"
|
|
20
|
+
| "unknown";
|
|
21
|
+
instruction?: string;
|
|
22
|
+
ruleId?: string;
|
|
23
|
+
targetId?: string;
|
|
24
|
+
targetIds?: string[];
|
|
25
|
+
setName?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const TARGET_DELIMITER = "#@#";
|
|
29
|
+
|
|
30
|
+
export function parseLearnCommand(text: string | undefined): ParsedLearnCommand {
|
|
31
|
+
const raw = String(text || "").trim();
|
|
32
|
+
if (!raw) {
|
|
33
|
+
return { scope: "unknown" };
|
|
34
|
+
}
|
|
35
|
+
const normalized = raw.toLowerCase();
|
|
36
|
+
if (
|
|
37
|
+
normalized === "/learn help"
|
|
38
|
+
) {
|
|
39
|
+
return { scope: "help" };
|
|
40
|
+
}
|
|
41
|
+
if (
|
|
42
|
+
normalized === "/learn whoami"
|
|
43
|
+
|| normalized === "/whoami"
|
|
44
|
+
|| normalized === "我是谁"
|
|
45
|
+
|| normalized === "我的信息"
|
|
46
|
+
) {
|
|
47
|
+
return { scope: "whoami" };
|
|
48
|
+
}
|
|
49
|
+
if (
|
|
50
|
+
normalized === "/learn whereami"
|
|
51
|
+
|| normalized === "这里是谁"
|
|
52
|
+
|| normalized === "这个群是谁"
|
|
53
|
+
|| normalized === "这个会话是谁"
|
|
54
|
+
) {
|
|
55
|
+
return { scope: "whereami" };
|
|
56
|
+
}
|
|
57
|
+
if (
|
|
58
|
+
normalized === "/learn owner status"
|
|
59
|
+
|| normalized === "/owner status"
|
|
60
|
+
|| normalized === "/owner-status"
|
|
61
|
+
) {
|
|
62
|
+
return { scope: "owner-status" };
|
|
63
|
+
}
|
|
64
|
+
if (normalized === "/learn list") {
|
|
65
|
+
return { scope: "list" };
|
|
66
|
+
}
|
|
67
|
+
if (normalized === "/learn target-set list") {
|
|
68
|
+
return { scope: "list" };
|
|
69
|
+
}
|
|
70
|
+
if (normalized.startsWith("/learn disable ")) {
|
|
71
|
+
const ruleId = raw.slice("/learn disable ".length).trim();
|
|
72
|
+
return ruleId ? { scope: "disable", ruleId } : { scope: "unknown" };
|
|
73
|
+
}
|
|
74
|
+
if (normalized.startsWith("/learn delete ")) {
|
|
75
|
+
const ruleId = raw.slice("/learn delete ".length).trim();
|
|
76
|
+
return ruleId ? { scope: "delete", ruleId } : { scope: "unknown" };
|
|
77
|
+
}
|
|
78
|
+
if (normalized.startsWith("/learn global ")) {
|
|
79
|
+
return { scope: "global", instruction: raw.slice("/learn global ".length).trim() };
|
|
80
|
+
}
|
|
81
|
+
if (normalized.startsWith("/learn session ")) {
|
|
82
|
+
return { scope: "session", instruction: raw.slice("/learn session ".length).trim() };
|
|
83
|
+
}
|
|
84
|
+
if (normalized.startsWith("/learn here ")) {
|
|
85
|
+
const rawInstruction = raw.slice("/learn here ".length).trim();
|
|
86
|
+
const instruction = rawInstruction.startsWith(TARGET_DELIMITER)
|
|
87
|
+
? rawInstruction.slice(TARGET_DELIMITER.length).trim()
|
|
88
|
+
: rawInstruction;
|
|
89
|
+
return instruction ? { scope: "here", instruction } : { scope: "unknown" };
|
|
90
|
+
}
|
|
91
|
+
if (normalized.startsWith("/learn target-set create ")) {
|
|
92
|
+
const payload = splitDelimitedPayload(raw.slice("/learn target-set create ".length));
|
|
93
|
+
if (!payload) {
|
|
94
|
+
return { scope: "unknown" };
|
|
95
|
+
}
|
|
96
|
+
const targetIds = parseTargetIds(payload.body);
|
|
97
|
+
return payload.head && targetIds.length > 0
|
|
98
|
+
? { scope: "target-set-create", setName: payload.head, targetIds }
|
|
99
|
+
: { scope: "unknown" };
|
|
100
|
+
}
|
|
101
|
+
if (normalized.startsWith("/learn target-set apply ")) {
|
|
102
|
+
const payload = splitDelimitedPayload(raw.slice("/learn target-set apply ".length));
|
|
103
|
+
return payload?.head && payload.body
|
|
104
|
+
? { scope: "target-set-apply", setName: payload.head, instruction: payload.body }
|
|
105
|
+
: { scope: "unknown" };
|
|
106
|
+
}
|
|
107
|
+
if (normalized.startsWith("/learn target ")) {
|
|
108
|
+
const payload = splitDelimitedPayload(raw.slice("/learn target ".length));
|
|
109
|
+
return payload?.head && payload.body
|
|
110
|
+
? { scope: "target", targetId: payload.head, instruction: payload.body }
|
|
111
|
+
: { scope: "unknown" };
|
|
112
|
+
}
|
|
113
|
+
if (normalized.startsWith("/learn targets ")) {
|
|
114
|
+
const payload = splitDelimitedPayload(raw.slice("/learn targets ".length));
|
|
115
|
+
if (!payload) {
|
|
116
|
+
return { scope: "unknown" };
|
|
117
|
+
}
|
|
118
|
+
const targetIds = parseTargetIds(payload.head);
|
|
119
|
+
return targetIds.length > 0 && payload.body
|
|
120
|
+
? { scope: "targets", targetIds, instruction: payload.body }
|
|
121
|
+
: { scope: "unknown" };
|
|
122
|
+
}
|
|
123
|
+
return { scope: "unknown" };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function splitDelimitedPayload(raw: string): { head: string; body: string } | null {
|
|
127
|
+
const index = raw.indexOf(TARGET_DELIMITER);
|
|
128
|
+
if (index < 0) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
const head = raw.slice(0, index).trim();
|
|
132
|
+
const body = raw.slice(index + TARGET_DELIMITER.length).trim();
|
|
133
|
+
if (!head || !body) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
return { head, body };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function parseTargetIds(raw: string): string[] {
|
|
140
|
+
return raw
|
|
141
|
+
.split(",")
|
|
142
|
+
.map((item) => item.trim())
|
|
143
|
+
.filter(Boolean);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function isLearningOwner(params: {
|
|
147
|
+
cfg?: { commands?: { ownerAllowFrom?: Array<string | number> } };
|
|
148
|
+
config?: DingTalkConfig;
|
|
149
|
+
senderId?: string;
|
|
150
|
+
rawSenderId?: string;
|
|
151
|
+
}): boolean {
|
|
152
|
+
const allow = normalizeAllowFrom(params.cfg?.commands?.ownerAllowFrom as string[] | undefined);
|
|
153
|
+
return isSenderOwner({ allow, senderId: params.senderId, rawSenderId: params.rawSenderId });
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function formatWhoAmIReply(params: {
|
|
157
|
+
senderId: string;
|
|
158
|
+
rawSenderId?: string;
|
|
159
|
+
senderStaffId?: string;
|
|
160
|
+
isOwner?: boolean;
|
|
161
|
+
}): string {
|
|
162
|
+
return [
|
|
163
|
+
"这是你当前消息对应的身份信息:",
|
|
164
|
+
"",
|
|
165
|
+
`- senderId: \`${params.senderId || ""}\``,
|
|
166
|
+
`- rawSenderId: \`${params.rawSenderId || ""}\``,
|
|
167
|
+
`- senderStaffId: \`${params.senderStaffId || ""}\``,
|
|
168
|
+
`- isOwner: \`${params.isOwner ? "true" : "false"}\``,
|
|
169
|
+
"",
|
|
170
|
+
"后续如果要配置 owner 或控制命令权限,就以这里返回的 senderId 为准。",
|
|
171
|
+
].join("\n");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function formatWhereAmIReply(params: {
|
|
175
|
+
conversationId: string;
|
|
176
|
+
conversationType: "group" | "dm";
|
|
177
|
+
peerId?: string;
|
|
178
|
+
}): string {
|
|
179
|
+
return [
|
|
180
|
+
params.conversationType === "group" ? "这是当前群聊信息:" : "这是当前会话信息:",
|
|
181
|
+
"",
|
|
182
|
+
`- conversationId: \`${params.conversationId}\``,
|
|
183
|
+
`- conversationType: \`${params.conversationType}\``,
|
|
184
|
+
params.peerId ? `- peerId: \`${params.peerId}\`` : undefined,
|
|
185
|
+
"",
|
|
186
|
+
"后续如果要定向注入到这里,就使用这个 conversationId。",
|
|
187
|
+
].filter(Boolean).join("\n");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function formatOwnerStatusReply(params: {
|
|
191
|
+
senderId: string;
|
|
192
|
+
rawSenderId?: string;
|
|
193
|
+
isOwner: boolean;
|
|
194
|
+
}): string {
|
|
195
|
+
return [
|
|
196
|
+
"当前 owner 控制状态:",
|
|
197
|
+
"",
|
|
198
|
+
`- senderId: \`${params.senderId || ""}\``,
|
|
199
|
+
`- rawSenderId: \`${params.rawSenderId || ""}\``,
|
|
200
|
+
`- isOwner: \`${params.isOwner ? "true" : "false"}\``,
|
|
201
|
+
"",
|
|
202
|
+
"如果需要变更 owner,请由宿主修改本机运行配置。",
|
|
203
|
+
].join("\n");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function formatOwnerOnlyDeniedReply(): string {
|
|
207
|
+
return "这条学习/控制命令仅允许 owner 使用。先发送“我是谁”确认你的 senderId,再由宿主将该 senderId 加入 commands.ownerAllowFrom。";
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function formatLearnCommandHelp(): string {
|
|
211
|
+
return [
|
|
212
|
+
"可用的 owner 学习命令:",
|
|
213
|
+
"",
|
|
214
|
+
"- /learn whoami:查看当前 senderId",
|
|
215
|
+
"- /learn owner status:查看当前 owner 权限状态",
|
|
216
|
+
"- /learn whereami:查看当前会话/群 ID",
|
|
217
|
+
"- /learn global <规则>:发布到当前钉钉账号下所有会话",
|
|
218
|
+
"- /learn session <规则>:仅发布到当前私聊会话",
|
|
219
|
+
"- /learn here #@# <规则>:发布到当前群/当前私聊",
|
|
220
|
+
"- /learn target <conversationId> #@# <规则>:发布到单个指定群/私聊",
|
|
221
|
+
"- /learn targets <conversationId1,conversationId2> #@# <规则>:一次发布到多个指定群/私聊",
|
|
222
|
+
"- /learn target-set create <名称> #@# <conversationId1,conversationId2>:保存一组固定目标",
|
|
223
|
+
"- /learn target-set apply <名称> #@# <规则>:向一个目标组批量发布规则",
|
|
224
|
+
"- /learn list:查看当前全局规则、目标规则与目标组摘要",
|
|
225
|
+
"- /learn disable <ruleId>:停用一条规则,停止继续命中,但保留记录",
|
|
226
|
+
"- /learn delete <ruleId>:彻底删除一条规则或目标规则",
|
|
227
|
+
"",
|
|
228
|
+
"可用的 owner 会话控制命令:",
|
|
229
|
+
"- /session-alias show:查看当前会话共享会话别名",
|
|
230
|
+
"- /session-alias set <alias>:将当前会话绑定到指定共享会话别名",
|
|
231
|
+
"- /session-alias clear:清除当前会话共享会话别名,恢复为默认 peerId",
|
|
232
|
+
"- /session-alias bind direct <senderId> <alias>:由 owner 远程绑定某个私聊 senderId",
|
|
233
|
+
"- /session-alias bind group <conversationId> <alias>:由 owner 远程绑定某个群 conversationId",
|
|
234
|
+
"- /session-alias unbind direct <senderId>:由 owner 远程解除某个私聊 senderId 绑定",
|
|
235
|
+
"- /session-alias unbind group <conversationId>:由 owner 远程解除某个群 conversationId 绑定",
|
|
236
|
+
"",
|
|
237
|
+
"权限说明:",
|
|
238
|
+
"- 只有 owner 才能真正执行 /learn 的写操作和控制操作。",
|
|
239
|
+
"- 如果你现在不是 owner,也可以先用 `/learn whoami` 查看 senderId,再由宿主把它加入 commands.ownerAllowFrom。",
|
|
240
|
+
].join("\n");
|
|
241
|
+
}
|
|
242
|
+
export function formatLearnAppliedReply(params: {
|
|
243
|
+
scope: "global" | "session" | "target" | "targets" | "target-set";
|
|
244
|
+
instruction: string;
|
|
245
|
+
ruleId?: string;
|
|
246
|
+
targetId?: string;
|
|
247
|
+
targetIds?: string[];
|
|
248
|
+
setName?: string;
|
|
249
|
+
}): string {
|
|
250
|
+
const scopeLine = params.scope === "global"
|
|
251
|
+
? "- 生效范围:同一钉钉账号下所有会话,将在下一条消息进入时自动加载"
|
|
252
|
+
: params.scope === "session"
|
|
253
|
+
? "- 生效范围:当前会话,将在下一条消息进入时自动加载"
|
|
254
|
+
: params.scope === "target"
|
|
255
|
+
? `- 生效范围:指定目标 \`${params.targetId}\``
|
|
256
|
+
: params.scope === "targets"
|
|
257
|
+
? `- 生效范围:${params.targetIds?.length || 0} 个目标`
|
|
258
|
+
: `- 生效范围:目标组 \`${params.setName}\``;
|
|
259
|
+
return [
|
|
260
|
+
params.scope === "global"
|
|
261
|
+
? "已注入全局知识。"
|
|
262
|
+
: params.scope === "session"
|
|
263
|
+
? "已注入当前会话知识。"
|
|
264
|
+
: params.scope === "target"
|
|
265
|
+
? "已注入指定目标知识。"
|
|
266
|
+
: params.scope === "targets"
|
|
267
|
+
? "已批量注入多个目标。"
|
|
268
|
+
: "已向目标组批量注入规则。",
|
|
269
|
+
"",
|
|
270
|
+
params.ruleId ? `- ruleId: \`${params.ruleId}\`` : undefined,
|
|
271
|
+
`- instruction: ${params.instruction}`,
|
|
272
|
+
scopeLine,
|
|
273
|
+
].filter(Boolean).join("\n");
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export function formatLearnListReply(lines: string[]): string {
|
|
277
|
+
if (lines.length === 0) {
|
|
278
|
+
return "当前还没有规则或目标组。";
|
|
279
|
+
}
|
|
280
|
+
return ["当前规则与目标组摘要:", "", ...lines].join("\n");
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export function formatLearnDisabledReply(params: {
|
|
284
|
+
ruleId: string;
|
|
285
|
+
existed: boolean;
|
|
286
|
+
scope?: "global" | "target";
|
|
287
|
+
targetId?: string;
|
|
288
|
+
}): string {
|
|
289
|
+
if (!params.existed) {
|
|
290
|
+
return [
|
|
291
|
+
"未找到对应规则。",
|
|
292
|
+
"",
|
|
293
|
+
`- ruleId: \`${params.ruleId}\``,
|
|
294
|
+
"可先使用 `/learn list` 查看当前可停用的规则。",
|
|
295
|
+
].join("\n");
|
|
296
|
+
}
|
|
297
|
+
return [
|
|
298
|
+
"已停用规则。",
|
|
299
|
+
"",
|
|
300
|
+
`- ruleId: \`${params.ruleId}\``,
|
|
301
|
+
params.scope === "target" && params.targetId ? `- scope: target (\`${params.targetId}\`)` : "- scope: global",
|
|
302
|
+
"- 当前效果:这条规则会停止生效,但记录仍会保留,便于后续排查或恢复。",
|
|
303
|
+
].join("\n");
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export function formatLearnDeletedReply(params: {
|
|
307
|
+
ruleId: string;
|
|
308
|
+
existed: boolean;
|
|
309
|
+
scope?: "global" | "target";
|
|
310
|
+
targetId?: string;
|
|
311
|
+
}): string {
|
|
312
|
+
if (!params.existed) {
|
|
313
|
+
return [
|
|
314
|
+
"未找到对应规则。",
|
|
315
|
+
"",
|
|
316
|
+
`- ruleId: \`${params.ruleId}\``,
|
|
317
|
+
"可先使用 `/learn list` 查看当前规则。",
|
|
318
|
+
].join("\n");
|
|
319
|
+
}
|
|
320
|
+
return [
|
|
321
|
+
"已删除规则。",
|
|
322
|
+
"",
|
|
323
|
+
`- ruleId: \`${params.ruleId}\``,
|
|
324
|
+
params.scope === "target" && params.targetId ? `- scope: target (\`${params.targetId}\`)` : "- scope: global",
|
|
325
|
+
].join("\n");
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export function formatTargetSetSavedReply(params: {
|
|
329
|
+
setName: string;
|
|
330
|
+
targetIds: string[];
|
|
331
|
+
}): string {
|
|
332
|
+
return [
|
|
333
|
+
"已保存目标组。",
|
|
334
|
+
"",
|
|
335
|
+
`- name: \`${params.setName}\``,
|
|
336
|
+
`- targetCount: \`${params.targetIds.length}\``,
|
|
337
|
+
`- targetIds: ${params.targetIds.map((targetId) => `\`${targetId}\``).join(", ")}`,
|
|
338
|
+
].join("\n");
|
|
339
|
+
}
|