@soimy/dingtalk 3.2.0 → 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 +657 -32
- package/index.ts +62 -0
- package/package.json +3 -1
- 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 +776 -24
- package/src/channel.ts +408 -135
- package/src/config-schema.ts +40 -4
- package/src/config.ts +136 -4
- package/src/connection-manager.ts +354 -47
- 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 +1100 -159
- package/src/learning-command-service.ts +339 -0
- package/src/media-utils.ts +94 -50
- package/src/message-utils.ts +301 -39
- package/src/onboarding.ts +38 -0
- 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 +177 -43
- package/src/session-command-service.ts +147 -0
- package/src/session-lock.ts +2 -0
- package/src/session-peer-store.ts +77 -0
- package/src/session-routing.ts +33 -0
- package/src/types.ts +152 -24
- 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
|
|
|
@@ -113,6 +114,7 @@ function applyAccountConfig(params: {
|
|
|
113
114
|
? { useConnectionManager: input.useConnectionManager }
|
|
114
115
|
: {}),
|
|
115
116
|
...(typeof input.mediaMaxMb === "number" ? { mediaMaxMb: input.mediaMaxMb } : {}),
|
|
117
|
+
...(typeof input.journalTTLDays === "number" ? { journalTTLDays: input.journalTTLDays } : {}),
|
|
116
118
|
};
|
|
117
119
|
|
|
118
120
|
if (useDefault) {
|
|
@@ -378,6 +380,41 @@ export const dingtalkOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
378
380
|
mediaMaxMb = Number.isInteger(parsedMediaMax) && parsedMediaMax > 0 ? parsedMediaMax : 20;
|
|
379
381
|
}
|
|
380
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
|
+
|
|
381
418
|
const next = applyAccountConfig({
|
|
382
419
|
cfg,
|
|
383
420
|
accountId,
|
|
@@ -396,6 +433,7 @@ export const dingtalkOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
396
433
|
cardTemplateKey,
|
|
397
434
|
maxReconnectCycles,
|
|
398
435
|
mediaMaxMb,
|
|
436
|
+
journalTTLDays,
|
|
399
437
|
},
|
|
400
438
|
});
|
|
401
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
|
}
|