@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
package/src/message-utils.ts
CHANGED
|
@@ -1,4 +1,101 @@
|
|
|
1
|
-
import type { DingTalkInboundMessage, MessageContent, SendMessageOptions } from "./types";
|
|
1
|
+
import type { DingTalkInboundMessage, MessageContent, QuotedInfo, SendMessageOptions } from "./types";
|
|
2
|
+
|
|
3
|
+
interface DingTalkDocMeta {
|
|
4
|
+
spaceId: string;
|
|
5
|
+
fileId: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function parseBizCustomActionUrl(url: string | undefined): DingTalkDocMeta | null {
|
|
9
|
+
if (!url || typeof url !== "string") {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const queryIndex = url.indexOf("?");
|
|
14
|
+
if (queryIndex < 0 || queryIndex === url.length - 1) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const params = new URLSearchParams(url.slice(queryIndex + 1));
|
|
20
|
+
const route = params.get("route");
|
|
21
|
+
const type = params.get("type");
|
|
22
|
+
const spaceId = params.get("spaceId");
|
|
23
|
+
const fileId = params.get("fileId");
|
|
24
|
+
if (route !== "previewDentry" || type !== "file" || !spaceId || !fileId) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
return { spaceId, fileId };
|
|
28
|
+
} catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function extractRichTextQuoteParts(
|
|
34
|
+
richText: Array<Record<string, unknown>> | undefined,
|
|
35
|
+
): { summary: string; pictureDownloadCode?: string; pictureDownloadCodes?: string[] } | null {
|
|
36
|
+
if (!Array.isArray(richText) || richText.length === 0) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const textParts: string[] = [];
|
|
41
|
+
const pictureDownloadCodes: string[] = [];
|
|
42
|
+
|
|
43
|
+
for (const part of richText) {
|
|
44
|
+
const partType =
|
|
45
|
+
typeof part.msgType === "string"
|
|
46
|
+
? part.msgType
|
|
47
|
+
: typeof part.type === "string"
|
|
48
|
+
? part.type
|
|
49
|
+
: undefined;
|
|
50
|
+
const textValue =
|
|
51
|
+
typeof part.content === "string"
|
|
52
|
+
? part.content
|
|
53
|
+
: typeof part.text === "string"
|
|
54
|
+
? part.text
|
|
55
|
+
: undefined;
|
|
56
|
+
|
|
57
|
+
if ((partType === "text" || partType === undefined) && textValue) {
|
|
58
|
+
textParts.push(textValue);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (partType === "emoji" && textValue) {
|
|
62
|
+
textParts.push(textValue);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (partType === "picture") {
|
|
66
|
+
textParts.push("[图片]");
|
|
67
|
+
if (typeof part.downloadCode === "string" && part.downloadCode.trim()) {
|
|
68
|
+
pictureDownloadCodes.push(part.downloadCode.trim());
|
|
69
|
+
}
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (partType === "at") {
|
|
73
|
+
const atName =
|
|
74
|
+
typeof part.atName === "string"
|
|
75
|
+
? part.atName
|
|
76
|
+
: typeof textValue === "string"
|
|
77
|
+
? textValue
|
|
78
|
+
: "某人";
|
|
79
|
+
textParts.push(`@${atName}`);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (textValue) {
|
|
83
|
+
textParts.push(textValue);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const summary = textParts.join("").trim();
|
|
88
|
+
const uniquePictureDownloadCodes = [...new Set(pictureDownloadCodes)];
|
|
89
|
+
const pictureDownloadCode = uniquePictureDownloadCodes[0];
|
|
90
|
+
if (!summary && !pictureDownloadCode) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
summary,
|
|
95
|
+
pictureDownloadCode,
|
|
96
|
+
pictureDownloadCodes: uniquePictureDownloadCodes.length > 0 ? uniquePictureDownloadCodes : undefined,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
2
99
|
|
|
3
100
|
/**
|
|
4
101
|
* Auto-detect markdown usage and derive message title.
|
|
@@ -24,76 +121,199 @@ export function detectMarkdownAndExtractTitle(
|
|
|
24
121
|
return { useMarkdown, title };
|
|
25
122
|
}
|
|
26
123
|
|
|
124
|
+
function isMarkdownTableSeparator(line: string): boolean {
|
|
125
|
+
const normalized = line.trim();
|
|
126
|
+
if (!normalized.includes("-")) {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
const cells = normalized
|
|
130
|
+
.replace(/^\|/, "")
|
|
131
|
+
.replace(/\|$/, "")
|
|
132
|
+
.split("|")
|
|
133
|
+
.map((cell) => cell.trim());
|
|
134
|
+
return cells.length > 0 && cells.every((cell) => /^:?-{3,}:?$/.test(cell));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function isMarkdownTableRow(line: string): boolean {
|
|
138
|
+
const trimmed = line.trim();
|
|
139
|
+
return trimmed.includes("|") && !trimmed.startsWith("```");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function parseMarkdownTableRow(line: string): string[] {
|
|
143
|
+
return line
|
|
144
|
+
.trim()
|
|
145
|
+
.replace(/^\|/, "")
|
|
146
|
+
.replace(/\|$/, "")
|
|
147
|
+
.split("|")
|
|
148
|
+
.map((cell) => cell.trim());
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function renderMarkdownTable(lines: string[]): string {
|
|
152
|
+
const rows = lines.map(parseMarkdownTableRow).filter((cells) => cells.length > 0);
|
|
153
|
+
return rows.map((cells) => cells.join(" | ")).join("\n");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function convertMarkdownTablesToPlainText(text: string): string {
|
|
157
|
+
const lines = text.split("\n");
|
|
158
|
+
const output: string[] = [];
|
|
159
|
+
let index = 0;
|
|
160
|
+
let inCodeFence = false;
|
|
161
|
+
|
|
162
|
+
while (index < lines.length) {
|
|
163
|
+
const line = lines[index] || "";
|
|
164
|
+
if (line.trim().startsWith("```")) {
|
|
165
|
+
inCodeFence = !inCodeFence;
|
|
166
|
+
output.push(line);
|
|
167
|
+
index += 1;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (
|
|
172
|
+
!inCodeFence &&
|
|
173
|
+
index + 1 < lines.length &&
|
|
174
|
+
isMarkdownTableRow(line) &&
|
|
175
|
+
isMarkdownTableSeparator(lines[index + 1] || "")
|
|
176
|
+
) {
|
|
177
|
+
const tableLines = [line];
|
|
178
|
+
index += 2;
|
|
179
|
+
while (index < lines.length && isMarkdownTableRow(lines[index] || "")) {
|
|
180
|
+
tableLines.push(lines[index] || "");
|
|
181
|
+
index += 1;
|
|
182
|
+
}
|
|
183
|
+
output.push(renderMarkdownTable(tableLines));
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
output.push(line);
|
|
188
|
+
index += 1;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return output.join("\n");
|
|
192
|
+
}
|
|
193
|
+
|
|
27
194
|
export function extractMessageContent(data: DingTalkInboundMessage): MessageContent {
|
|
28
195
|
const msgtype = data.msgtype || "text";
|
|
29
196
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const textField = data.text as any;
|
|
197
|
+
const formatQuotedContent = (): QuotedInfo | null => {
|
|
198
|
+
const textField = data.text;
|
|
33
199
|
|
|
34
200
|
if (textField?.isReplyMsg && textField?.repliedMsg) {
|
|
35
201
|
const repliedMsg = textField.repliedMsg;
|
|
36
|
-
const
|
|
202
|
+
const repliedMsgType = repliedMsg.msgType;
|
|
203
|
+
const content = repliedMsg.content;
|
|
204
|
+
|
|
205
|
+
if (repliedMsgType === "text" && content?.text?.trim()) {
|
|
206
|
+
return { prefix: `[引用消息: "${content.text.trim()}"]\n\n` };
|
|
207
|
+
}
|
|
37
208
|
|
|
38
|
-
if (content?.
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
209
|
+
if (repliedMsgType === "picture" && content?.downloadCode) {
|
|
210
|
+
return {
|
|
211
|
+
prefix: "[引用图片]\n\n",
|
|
212
|
+
mediaDownloadCode: content.downloadCode,
|
|
213
|
+
mediaType: "image",
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (repliedMsgType === "richText") {
|
|
218
|
+
const richTextQuote = extractRichTextQuoteParts(content?.richText);
|
|
219
|
+
if (richTextQuote) {
|
|
220
|
+
const quoteImageCount = richTextQuote.pictureDownloadCodes?.length || 0;
|
|
221
|
+
const prefix =
|
|
222
|
+
richTextQuote.summary && richTextQuote.summary !== "[图片]"
|
|
223
|
+
? `[引用消息: "${richTextQuote.summary}"]${quoteImageCount > 1 ? ` [含${quoteImageCount}张引用图片]` : ""}\n\n`
|
|
224
|
+
: "[引用图片]\n\n";
|
|
225
|
+
return {
|
|
226
|
+
prefix,
|
|
227
|
+
mediaDownloadCode: richTextQuote.pictureDownloadCode,
|
|
228
|
+
mediaType: richTextQuote.pictureDownloadCode ? "image" : undefined,
|
|
229
|
+
};
|
|
42
230
|
}
|
|
43
231
|
}
|
|
44
232
|
|
|
45
|
-
if (
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
233
|
+
if (repliedMsgType === "unknownMsgType") {
|
|
234
|
+
return {
|
|
235
|
+
prefix: "[引用文件]\n\n",
|
|
236
|
+
isQuotedFile: true,
|
|
237
|
+
fileCreatedAt: repliedMsg.createdAt,
|
|
238
|
+
msgId: repliedMsg.msgId,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (repliedMsgType === "interactiveCard") {
|
|
243
|
+
const isBotCard = repliedMsg.senderId === data.chatbotUserId;
|
|
244
|
+
if (isBotCard) {
|
|
245
|
+
return {
|
|
246
|
+
prefix: "[引用了机器人的回复]\n\n",
|
|
247
|
+
isQuotedCard: true,
|
|
248
|
+
cardCreatedAt: repliedMsg.createdAt,
|
|
249
|
+
processQueryKey: data.originalProcessQueryKey,
|
|
250
|
+
msgId: repliedMsg.msgId,
|
|
251
|
+
};
|
|
59
252
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
prefix: "[引用了钉钉文档]\n\n",
|
|
256
|
+
isQuotedDocCard: true,
|
|
257
|
+
fileCreatedAt: repliedMsg.createdAt,
|
|
258
|
+
msgId: repliedMsg.msgId,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Has msgType but not one we handle — generic fallback.
|
|
263
|
+
if (repliedMsgType) {
|
|
264
|
+
const idPart = repliedMsg.msgId ? `,原消息ID: ${repliedMsg.msgId}` : "";
|
|
265
|
+
return { prefix: `[引用消息不可见: msgType=${repliedMsgType}${idPart}]\n\n` };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// No msgType — backward compat: extract text or richText from content.
|
|
269
|
+
if (content?.text?.trim()) {
|
|
270
|
+
return { prefix: `[引用消息: "${content.text.trim()}"]\n\n` };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (content?.richText && Array.isArray(content.richText)) {
|
|
274
|
+
const richTextQuote = extractRichTextQuoteParts(content.richText);
|
|
275
|
+
if (richTextQuote?.summary) {
|
|
276
|
+
return {
|
|
277
|
+
prefix: `[引用消息: "${richTextQuote.summary}"]\n\n`,
|
|
278
|
+
mediaDownloadCode: richTextQuote.pictureDownloadCode,
|
|
279
|
+
mediaType: richTextQuote.pictureDownloadCode ? "image" : undefined,
|
|
280
|
+
};
|
|
63
281
|
}
|
|
64
282
|
}
|
|
65
283
|
}
|
|
66
284
|
|
|
67
|
-
// Some clients only send originalMsgId for rich media reply messages.
|
|
68
285
|
if (textField?.isReplyMsg && !textField?.repliedMsg && data.originalMsgId) {
|
|
69
|
-
return
|
|
286
|
+
return {
|
|
287
|
+
prefix: `[这是一条引用消息,原消息ID: ${data.originalMsgId}]\n\n`,
|
|
288
|
+
msgId: data.originalMsgId,
|
|
289
|
+
};
|
|
70
290
|
}
|
|
71
291
|
|
|
72
292
|
if (data.quoteMessage) {
|
|
73
293
|
const quoteText = data.quoteMessage.text?.content?.trim() || "";
|
|
74
294
|
if (quoteText) {
|
|
75
|
-
return `[引用消息: "${quoteText}"]\n\n
|
|
295
|
+
return { prefix: `[引用消息: "${quoteText}"]\n\n` };
|
|
76
296
|
}
|
|
77
297
|
}
|
|
78
298
|
|
|
79
299
|
if (data.content?.quoteContent) {
|
|
80
|
-
return `[引用消息: "${data.content.quoteContent}"]\n\n
|
|
300
|
+
return { prefix: `[引用消息: "${data.content.quoteContent}"]\n\n` };
|
|
81
301
|
}
|
|
82
302
|
|
|
83
|
-
return
|
|
303
|
+
return null;
|
|
84
304
|
};
|
|
85
305
|
|
|
86
|
-
const
|
|
306
|
+
const quoted = formatQuotedContent();
|
|
307
|
+
const quotedPrefix = quoted?.prefix || "";
|
|
87
308
|
|
|
88
|
-
// Unified extraction by DingTalk msgtype for downstream routing/agent processing.
|
|
89
309
|
if (msgtype === "text") {
|
|
90
|
-
return { text: quotedPrefix + (data.text?.content?.trim() || ""), messageType: "text" };
|
|
310
|
+
return { text: quotedPrefix + (data.text?.content?.trim() || ""), messageType: "text", quoted: quoted ?? undefined };
|
|
91
311
|
}
|
|
92
312
|
|
|
93
313
|
if (msgtype === "richText") {
|
|
94
314
|
const richTextParts = data.content?.richText || [];
|
|
95
315
|
let text = "";
|
|
96
|
-
|
|
316
|
+
const pictureDownloadCodes: string[] = [];
|
|
97
317
|
// Keep first image downloadCode while preserving readable text and @mention parts.
|
|
98
318
|
for (const part of richTextParts) {
|
|
99
319
|
if (part.text && (part.type === "text" || part.type === undefined)) {
|
|
@@ -102,16 +322,21 @@ export function extractMessageContent(data: DingTalkInboundMessage): MessageCont
|
|
|
102
322
|
if (part.type === "at" && part.atName) {
|
|
103
323
|
text += `@${part.atName} `;
|
|
104
324
|
}
|
|
105
|
-
if (part.type === "picture" && part.downloadCode
|
|
106
|
-
|
|
325
|
+
if (part.type === "picture" && part.downloadCode) {
|
|
326
|
+
pictureDownloadCodes.push(part.downloadCode);
|
|
107
327
|
}
|
|
108
328
|
}
|
|
329
|
+
const uniquePictureDownloadCodes = [...new Set(pictureDownloadCodes)];
|
|
330
|
+
const pictureDownloadCode = uniquePictureDownloadCodes[0];
|
|
109
331
|
return {
|
|
110
332
|
text:
|
|
111
333
|
quotedPrefix + (text.trim() || (pictureDownloadCode ? "<media:image>" : "[富文本消息]")),
|
|
112
334
|
mediaPath: pictureDownloadCode,
|
|
335
|
+
mediaPaths: uniquePictureDownloadCodes.length > 0 ? uniquePictureDownloadCodes : undefined,
|
|
113
336
|
mediaType: pictureDownloadCode ? "image" : undefined,
|
|
337
|
+
mediaTypes: uniquePictureDownloadCodes.length > 0 ? uniquePictureDownloadCodes.map(() => "image") : undefined,
|
|
114
338
|
messageType: "richText",
|
|
339
|
+
quoted: quoted ?? undefined,
|
|
115
340
|
};
|
|
116
341
|
}
|
|
117
342
|
|
|
@@ -151,6 +376,43 @@ export function extractMessageContent(data: DingTalkInboundMessage): MessageCont
|
|
|
151
376
|
};
|
|
152
377
|
}
|
|
153
378
|
|
|
154
|
-
|
|
155
|
-
|
|
379
|
+
if (msgtype === "interactiveCard") {
|
|
380
|
+
const docMeta = parseBizCustomActionUrl(data.content?.biz_custom_action_url);
|
|
381
|
+
if (docMeta) {
|
|
382
|
+
return {
|
|
383
|
+
text: "[钉钉文档]\n\n",
|
|
384
|
+
messageType: "interactiveCardFile",
|
|
385
|
+
docSpaceId: docMeta.spaceId,
|
|
386
|
+
docFileId: docMeta.fileId,
|
|
387
|
+
quoted: quoted ?? undefined,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
return {
|
|
391
|
+
text: data.text?.content?.trim() || "[interactiveCard消息]",
|
|
392
|
+
messageType: msgtype,
|
|
393
|
+
quoted: quoted ?? undefined,
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
if (msgtype === "chatRecord") {
|
|
397
|
+
const content = data.content as Record<string, unknown> | undefined;
|
|
398
|
+
const summary = typeof content?.summary === "string" ? content.summary.trim() : "";
|
|
399
|
+
const rawRecord = content?.chatRecord;
|
|
400
|
+
if (
|
|
401
|
+
summary === "[]" ||
|
|
402
|
+
(typeof rawRecord === "string" && rawRecord.trim() === "[]") ||
|
|
403
|
+
(Array.isArray(rawRecord) && rawRecord.length === 0)
|
|
404
|
+
) {
|
|
405
|
+
return {
|
|
406
|
+
text: "[系统提示] 没有读到引用记录(chatRecord 为空)。请改用逐条转发、复制原文,或重新转发非空聊天记录。",
|
|
407
|
+
messageType: "chatRecord",
|
|
408
|
+
quoted: quoted ?? undefined,
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
if (summary) {
|
|
412
|
+
return { text: `[聊天记录摘要] ${summary}`, messageType: "chatRecord", quoted: quoted ?? undefined };
|
|
413
|
+
}
|
|
414
|
+
return { text: "[chatRecord消息: 无可读内容]", messageType: "chatRecord", quoted: quoted ?? undefined };
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return { text: data.text?.content?.trim() || `[${msgtype}消息]`, messageType: msgtype, quoted: quoted ?? undefined };
|
|
156
418
|
}
|
package/src/onboarding.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { OpenClawConfig, ChannelOnboardingAdapter, WizardPrompter } from "o
|
|
|
2
2
|
import { DEFAULT_ACCOUNT_ID, normalizeAccountId, formatDocsLink } from "openclaw/plugin-sdk";
|
|
3
3
|
import type { DingTalkConfig, DingTalkChannelConfig } from "./types.js";
|
|
4
4
|
import { listDingTalkAccountIds, resolveDingTalkAccount } from "./types.js";
|
|
5
|
+
import { DEFAULT_JOURNAL_TTL_DAYS } from "./quote-journal.js";
|
|
5
6
|
|
|
6
7
|
const channel = "dingtalk" as const;
|
|
7
8
|
|
|
@@ -109,6 +110,11 @@ function applyAccountConfig(params: {
|
|
|
109
110
|
...(typeof input.maxReconnectCycles === "number"
|
|
110
111
|
? { maxReconnectCycles: input.maxReconnectCycles }
|
|
111
112
|
: {}),
|
|
113
|
+
...(typeof input.useConnectionManager === "boolean"
|
|
114
|
+
? { useConnectionManager: input.useConnectionManager }
|
|
115
|
+
: {}),
|
|
116
|
+
...(typeof input.mediaMaxMb === "number" ? { mediaMaxMb: input.mediaMaxMb } : {}),
|
|
117
|
+
...(typeof input.journalTTLDays === "number" ? { journalTTLDays: input.journalTTLDays } : {}),
|
|
112
118
|
};
|
|
113
119
|
|
|
114
120
|
if (useDefault) {
|
|
@@ -271,10 +277,10 @@ export const dingtalkOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
271
277
|
String(
|
|
272
278
|
await prompter.text({
|
|
273
279
|
message: "Card Template Key (content field name)",
|
|
274
|
-
placeholder: "
|
|
275
|
-
initialValue: resolved.cardTemplateKey ?? "
|
|
280
|
+
placeholder: "content",
|
|
281
|
+
initialValue: resolved.cardTemplateKey ?? "content",
|
|
276
282
|
}),
|
|
277
|
-
).trim() || "
|
|
283
|
+
).trim() || "content";
|
|
278
284
|
|
|
279
285
|
messageType = "card";
|
|
280
286
|
}
|
|
@@ -298,6 +304,14 @@ export const dingtalkOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
298
304
|
allowFrom = parsed.length > 0 ? parsed : undefined;
|
|
299
305
|
}
|
|
300
306
|
|
|
307
|
+
const mediaUrlAllowlistEntry = await prompter.text({
|
|
308
|
+
message: "Media URL allowlist (comma-separated host/IP/CIDR, optional)",
|
|
309
|
+
placeholder: "cdn.example.com, 192.168.1.23, 10.0.0.0/8",
|
|
310
|
+
initialValue: (resolved.mediaUrlAllowlist || []).join(", ") || undefined,
|
|
311
|
+
});
|
|
312
|
+
const mediaUrlAllowlistParsed = parseList(String(mediaUrlAllowlistEntry ?? ""));
|
|
313
|
+
const mediaUrlAllowlist = mediaUrlAllowlistParsed.length > 0 ? mediaUrlAllowlistParsed : undefined;
|
|
314
|
+
|
|
301
315
|
const groupPolicyValue = await prompter.select({
|
|
302
316
|
message: "Group message policy",
|
|
303
317
|
options: [
|
|
@@ -336,6 +350,71 @@ export const dingtalkOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
336
350
|
maxReconnectCycles = Number.isInteger(parsedCycles) && parsedCycles > 0 ? parsedCycles : 10;
|
|
337
351
|
}
|
|
338
352
|
|
|
353
|
+
let mediaMaxMb: number | undefined;
|
|
354
|
+
const wantsMediaMax = await prompter.confirm({
|
|
355
|
+
message: "Configure inbound media max size in MB? (optional)",
|
|
356
|
+
initialValue: typeof resolved.mediaMaxMb === "number",
|
|
357
|
+
});
|
|
358
|
+
if (wantsMediaMax) {
|
|
359
|
+
const parsedMediaMax = Number(
|
|
360
|
+
String(
|
|
361
|
+
await prompter.text({
|
|
362
|
+
message: "Max inbound media size (MB)",
|
|
363
|
+
placeholder: "20",
|
|
364
|
+
initialValue:
|
|
365
|
+
typeof resolved.mediaMaxMb === "number" ? String(resolved.mediaMaxMb) : "20",
|
|
366
|
+
validate: (value) => {
|
|
367
|
+
const raw = String(value ?? "").trim();
|
|
368
|
+
const num = Number(raw);
|
|
369
|
+
if (!raw) {
|
|
370
|
+
return "Required";
|
|
371
|
+
}
|
|
372
|
+
if (!Number.isInteger(num) || num < 1) {
|
|
373
|
+
return "Must be an integer >= 1";
|
|
374
|
+
}
|
|
375
|
+
return undefined;
|
|
376
|
+
},
|
|
377
|
+
}),
|
|
378
|
+
).trim(),
|
|
379
|
+
);
|
|
380
|
+
mediaMaxMb = Number.isInteger(parsedMediaMax) && parsedMediaMax > 0 ? parsedMediaMax : 20;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
let journalTTLDays: number | undefined;
|
|
384
|
+
const wantsJournalTTL = await prompter.confirm({
|
|
385
|
+
message: "Configure quote journal retention in days?",
|
|
386
|
+
initialValue: typeof resolved.journalTTLDays === "number",
|
|
387
|
+
});
|
|
388
|
+
if (wantsJournalTTL) {
|
|
389
|
+
const parsedJournalTTL = Number(
|
|
390
|
+
String(
|
|
391
|
+
await prompter.text({
|
|
392
|
+
message: "Quote journal retention days",
|
|
393
|
+
placeholder: String(DEFAULT_JOURNAL_TTL_DAYS),
|
|
394
|
+
initialValue:
|
|
395
|
+
typeof resolved.journalTTLDays === "number"
|
|
396
|
+
? String(resolved.journalTTLDays)
|
|
397
|
+
: String(DEFAULT_JOURNAL_TTL_DAYS),
|
|
398
|
+
validate: (value) => {
|
|
399
|
+
const raw = String(value ?? "").trim();
|
|
400
|
+
const num = Number(raw);
|
|
401
|
+
if (!raw) {
|
|
402
|
+
return "Required";
|
|
403
|
+
}
|
|
404
|
+
if (!Number.isInteger(num) || num < 1) {
|
|
405
|
+
return "Must be an integer >= 1";
|
|
406
|
+
}
|
|
407
|
+
return undefined;
|
|
408
|
+
},
|
|
409
|
+
}),
|
|
410
|
+
).trim(),
|
|
411
|
+
);
|
|
412
|
+
journalTTLDays =
|
|
413
|
+
Number.isInteger(parsedJournalTTL) && parsedJournalTTL > 0
|
|
414
|
+
? parsedJournalTTL
|
|
415
|
+
: DEFAULT_JOURNAL_TTL_DAYS;
|
|
416
|
+
}
|
|
417
|
+
|
|
339
418
|
const next = applyAccountConfig({
|
|
340
419
|
cfg,
|
|
341
420
|
accountId,
|
|
@@ -348,10 +427,13 @@ export const dingtalkOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
348
427
|
dmPolicy: dmPolicyValue as "open" | "allowlist",
|
|
349
428
|
groupPolicy: groupPolicyValue as "open" | "allowlist",
|
|
350
429
|
allowFrom,
|
|
430
|
+
mediaUrlAllowlist,
|
|
351
431
|
messageType,
|
|
352
432
|
cardTemplateId,
|
|
353
433
|
cardTemplateKey,
|
|
354
434
|
maxReconnectCycles,
|
|
435
|
+
mediaMaxMb,
|
|
436
|
+
journalTTLDays,
|
|
355
437
|
},
|
|
356
438
|
});
|
|
357
439
|
|
package/src/peer-id-registry.ts
CHANGED
|
@@ -8,7 +8,13 @@
|
|
|
8
8
|
* can be delivered correctly.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import { readFileSync, readdirSync } from "fs";
|
|
12
|
+
import * as os from "os";
|
|
13
|
+
import { join } from "path";
|
|
14
|
+
import { getLogger } from "./logger-context";
|
|
15
|
+
|
|
11
16
|
const peerIdMap = new Map<string, string>();
|
|
17
|
+
let preloaded = false;
|
|
12
18
|
|
|
13
19
|
/**
|
|
14
20
|
* Register an original peer ID, keyed by its lowercased form.
|
|
@@ -20,14 +26,31 @@ export function registerPeerId(originalId: string): void {
|
|
|
20
26
|
peerIdMap.set(originalId.toLowerCase(), originalId);
|
|
21
27
|
}
|
|
22
28
|
|
|
29
|
+
function maybeRegisterDingTalkGroupPeerId(value: unknown): void {
|
|
30
|
+
// DingTalk group openConversationId values are base64-like and consistently start with "cid".
|
|
31
|
+
// User IDs in DM contexts do not follow this pattern and do not require case restoration.
|
|
32
|
+
if (typeof value === "string" && value.startsWith("cid")) {
|
|
33
|
+
registerPeerId(value);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
23
37
|
/**
|
|
24
38
|
* Resolve a possibly-lowercased peer ID back to its original casing.
|
|
39
|
+
*
|
|
40
|
+
* If registry is empty at first outbound call (for example, cron/delivery queue
|
|
41
|
+
* fires before inbound callbacks), perform a one-time lazy preload from sessions.
|
|
42
|
+
*
|
|
25
43
|
* Returns the original if found, otherwise returns the input as-is.
|
|
26
44
|
*/
|
|
27
45
|
export function resolveOriginalPeerId(id: string): string {
|
|
28
46
|
if (!id) {
|
|
29
47
|
return id;
|
|
30
48
|
}
|
|
49
|
+
|
|
50
|
+
if (!preloaded) {
|
|
51
|
+
preloadPeerIdsFromSessions();
|
|
52
|
+
}
|
|
53
|
+
|
|
31
54
|
return peerIdMap.get(id.toLowerCase()) || id;
|
|
32
55
|
}
|
|
33
56
|
|
|
@@ -36,4 +59,83 @@ export function resolveOriginalPeerId(id: string): string {
|
|
|
36
59
|
*/
|
|
37
60
|
export function clearPeerIdRegistry(): void {
|
|
38
61
|
peerIdMap.clear();
|
|
62
|
+
preloaded = false;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Preload known peer IDs from all agents' sessions.json files.
|
|
67
|
+
*
|
|
68
|
+
* Safe to call repeatedly:
|
|
69
|
+
* - without explicit homeDir: runs once, then no-ops;
|
|
70
|
+
* - with explicit homeDir: always runs (useful for tests).
|
|
71
|
+
*/
|
|
72
|
+
export function preloadPeerIdsFromSessions(homeDir?: string): void {
|
|
73
|
+
if (!homeDir && preloaded) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const home = homeDir || os.homedir();
|
|
78
|
+
const agentsDir = join(home, ".openclaw", "agents");
|
|
79
|
+
const log = getLogger();
|
|
80
|
+
let preloadCompleted = false;
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const agentDirs = readdirSync(agentsDir, { withFileTypes: true })
|
|
84
|
+
.filter((entry) => entry.isDirectory())
|
|
85
|
+
.map((entry) => entry.name);
|
|
86
|
+
|
|
87
|
+
for (const agentName of agentDirs) {
|
|
88
|
+
const sessionsPath = join(agentsDir, agentName, "sessions", "sessions.json");
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
const raw = readFileSync(sessionsPath, "utf-8");
|
|
92
|
+
const parsed = JSON.parse(raw);
|
|
93
|
+
if (!parsed || typeof parsed !== "object") {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
for (const session of Object.values(parsed as Record<string, unknown>)) {
|
|
98
|
+
if (!session || typeof session !== "object") {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const sessionRecord = session as Record<string, unknown>;
|
|
103
|
+
maybeRegisterDingTalkGroupPeerId(sessionRecord.lastTo);
|
|
104
|
+
|
|
105
|
+
const origin = sessionRecord.origin;
|
|
106
|
+
if (!origin || typeof origin !== "object") {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const originRecord = origin as Record<string, unknown>;
|
|
111
|
+
maybeRegisterDingTalkGroupPeerId(originRecord.from);
|
|
112
|
+
maybeRegisterDingTalkGroupPeerId(originRecord.to);
|
|
113
|
+
}
|
|
114
|
+
} catch (err) {
|
|
115
|
+
// sessions.json may be missing or malformed for some agents; ignore per file.
|
|
116
|
+
log?.debug?.(
|
|
117
|
+
`[DingTalk][PeerIdRegistry] Failed to parse preload sessions file ${sessionsPath}: ${
|
|
118
|
+
err instanceof Error ? err.message : String(err)
|
|
119
|
+
}`,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
preloadCompleted = true;
|
|
124
|
+
} catch (err) {
|
|
125
|
+
const errorCode = (err as NodeJS.ErrnoException | undefined)?.code;
|
|
126
|
+
if (errorCode === "ENOENT") {
|
|
127
|
+
// agents directory may not exist yet; this is a stable no-op state.
|
|
128
|
+
preloadCompleted = true;
|
|
129
|
+
} else {
|
|
130
|
+
log?.debug?.(
|
|
131
|
+
`[DingTalk][PeerIdRegistry] Failed to scan preload directory ${agentsDir}: ${
|
|
132
|
+
err instanceof Error ? err.message : String(err)
|
|
133
|
+
}`,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (!homeDir && preloadCompleted) {
|
|
139
|
+
preloaded = true;
|
|
140
|
+
}
|
|
39
141
|
}
|