@soimy/dingtalk 3.2.0 → 3.4.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 +796 -42
- package/index.ts +62 -0
- package/package.json +4 -2
- package/src/access-control.ts +83 -0
- package/src/ack-reaction/dynamic-ack-reaction-controller.ts +271 -0
- package/src/ack-reaction/dynamic-ack-reaction-events.ts +123 -0
- package/src/ack-reaction/dynamic-ack-reaction-progress.ts +59 -0
- package/src/ack-reaction-classifier.ts +75 -0
- package/src/ack-reaction-service.ts +182 -0
- package/src/attachment-text-extractor.ts +148 -0
- package/src/card-callback-service.ts +119 -0
- package/src/card-draft-controller.ts +114 -0
- package/src/card-service.ts +666 -26
- package/src/channel.ts +455 -150
- package/src/config-schema.ts +64 -6
- package/src/config.ts +161 -5
- 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 +1374 -259
- package/src/learning-command-service.ts +339 -0
- package/src/media-utils.ts +94 -50
- package/src/message-context-store.ts +787 -0
- package/src/message-utils.ts +487 -46
- package/src/messaging/quoted-context.ts +269 -0
- package/src/messaging/quoted-ref.ts +97 -0
- package/src/onboarding.ts +96 -1
- package/src/peer-id-registry.ts +102 -0
- package/src/persistence-store.ts +131 -0
- package/src/quoted-file-service.ts +385 -0
- package/src/reply-strategy-card.ts +225 -0
- package/src/reply-strategy-markdown.ts +55 -0
- package/src/reply-strategy-with-reaction.ts +190 -0
- package/src/reply-strategy.ts +72 -0
- package/src/send-service.ts +267 -45
- 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/targeting/agent-name-matcher.ts +148 -0
- package/src/targeting/agent-routing.ts +181 -0
- package/src/targeting/target-directory-adapter.ts +151 -0
- package/src/targeting/target-directory-store.ts +396 -0
- package/src/targeting/target-input.ts +62 -0
- package/src/types.ts +261 -28
- package/src/utils.ts +231 -12
package/src/message-utils.ts
CHANGED
|
@@ -1,4 +1,232 @@
|
|
|
1
|
-
import type { DingTalkInboundMessage, MessageContent, SendMessageOptions } from "./types";
|
|
1
|
+
import type { AtMention, DingTalkInboundMessage, MessageContent, QuotedInfo, SendMessageOptions } from "./types";
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
interface DingTalkDocMeta {
|
|
5
|
+
spaceId: string;
|
|
6
|
+
fileId: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function parseBizCustomActionUrl(url: string | undefined): DingTalkDocMeta | null {
|
|
10
|
+
if (!url || typeof url !== "string") {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const queryIndex = url.indexOf("?");
|
|
15
|
+
if (queryIndex < 0 || queryIndex === url.length - 1) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const params = new URLSearchParams(url.slice(queryIndex + 1));
|
|
21
|
+
const route = params.get("route");
|
|
22
|
+
const type = params.get("type");
|
|
23
|
+
const spaceId = params.get("spaceId");
|
|
24
|
+
const fileId = params.get("fileId");
|
|
25
|
+
if (route !== "previewDentry" || type !== "file" || !spaceId || !fileId) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
return { spaceId, fileId };
|
|
29
|
+
} catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function extractRichTextQuoteParts(
|
|
35
|
+
richText: Array<Record<string, unknown>> | undefined,
|
|
36
|
+
): { summary: string; pictureDownloadCode?: string; pictureDownloadCodes?: string[] } | null {
|
|
37
|
+
if (!Array.isArray(richText) || richText.length === 0) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const textParts: string[] = [];
|
|
42
|
+
const pictureDownloadCodes: string[] = [];
|
|
43
|
+
|
|
44
|
+
for (const part of richText) {
|
|
45
|
+
const partType =
|
|
46
|
+
typeof part.msgType === "string"
|
|
47
|
+
? part.msgType
|
|
48
|
+
: typeof part.type === "string"
|
|
49
|
+
? part.type
|
|
50
|
+
: undefined;
|
|
51
|
+
const textValue =
|
|
52
|
+
typeof part.content === "string"
|
|
53
|
+
? part.content
|
|
54
|
+
: typeof part.text === "string"
|
|
55
|
+
? part.text
|
|
56
|
+
: undefined;
|
|
57
|
+
|
|
58
|
+
if ((partType === "text" || partType === undefined) && textValue) {
|
|
59
|
+
textParts.push(textValue);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (partType === "emoji" && textValue) {
|
|
63
|
+
textParts.push(textValue);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (partType === "picture") {
|
|
67
|
+
textParts.push("[图片]");
|
|
68
|
+
if (typeof part.downloadCode === "string" && part.downloadCode.trim()) {
|
|
69
|
+
pictureDownloadCodes.push(part.downloadCode.trim());
|
|
70
|
+
}
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (partType === "at") {
|
|
74
|
+
const atName =
|
|
75
|
+
typeof part.atName === "string"
|
|
76
|
+
? part.atName
|
|
77
|
+
: typeof textValue === "string"
|
|
78
|
+
? textValue
|
|
79
|
+
: "某人";
|
|
80
|
+
textParts.push(`@${atName}`);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (textValue) {
|
|
84
|
+
textParts.push(textValue);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const summary = textParts.join("").trim();
|
|
89
|
+
const uniquePictureDownloadCodes = [...new Set(pictureDownloadCodes)];
|
|
90
|
+
const pictureDownloadCode = uniquePictureDownloadCodes[0];
|
|
91
|
+
if (!summary && !pictureDownloadCode) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
summary,
|
|
96
|
+
pictureDownloadCode,
|
|
97
|
+
pictureDownloadCodes: uniquePictureDownloadCodes.length > 0 ? uniquePictureDownloadCodes : undefined,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function trimString(value: string | undefined): string | undefined {
|
|
102
|
+
if (typeof value !== "string") {
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
const trimmed = value.trim();
|
|
106
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function buildQuotedMessageTypePlaceholder(messageType: string | undefined, fileName?: string): string | undefined {
|
|
110
|
+
switch (messageType) {
|
|
111
|
+
case "text":
|
|
112
|
+
return undefined;
|
|
113
|
+
case "picture":
|
|
114
|
+
return "<media:image>";
|
|
115
|
+
case "audio":
|
|
116
|
+
return "<media:voice>";
|
|
117
|
+
case "video":
|
|
118
|
+
return "<media:video>";
|
|
119
|
+
case "file":
|
|
120
|
+
case "unknownMsgType":
|
|
121
|
+
return fileName ? `<media:file> (${fileName})` : "<media:file>";
|
|
122
|
+
case "interactiveCardFile":
|
|
123
|
+
return "[钉钉文档]";
|
|
124
|
+
case "interactiveCard":
|
|
125
|
+
return "[interactiveCard消息]";
|
|
126
|
+
case "richText":
|
|
127
|
+
return "[富文本消息]";
|
|
128
|
+
default:
|
|
129
|
+
return messageType ? `[Quoted ${messageType}]` : undefined;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function buildLegacyQuoteMessagePreview(message: DingTalkInboundMessage["quoteMessage"]): {
|
|
134
|
+
previewText?: string;
|
|
135
|
+
previewMessageType?: string;
|
|
136
|
+
previewSenderId?: string;
|
|
137
|
+
} {
|
|
138
|
+
const previewMessageType = trimString(message?.msgtype);
|
|
139
|
+
return {
|
|
140
|
+
previewText:
|
|
141
|
+
trimString(message?.text?.content) ||
|
|
142
|
+
buildQuotedMessageTypePlaceholder(previewMessageType),
|
|
143
|
+
previewMessageType,
|
|
144
|
+
previewSenderId: trimString(message?.senderId),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function buildRepliedMessagePreview(params: {
|
|
149
|
+
data: DingTalkInboundMessage;
|
|
150
|
+
repliedMsg: NonNullable<NonNullable<DingTalkInboundMessage["text"]>["repliedMsg"]>;
|
|
151
|
+
}): Partial<QuotedInfo> {
|
|
152
|
+
const { data, repliedMsg } = params;
|
|
153
|
+
const repliedMsgType = trimString(repliedMsg.msgType);
|
|
154
|
+
const content = repliedMsg.content;
|
|
155
|
+
const fileName = trimString(content?.fileName);
|
|
156
|
+
const richTextQuote = extractRichTextQuoteParts(content?.richText);
|
|
157
|
+
const docMeta = parseBizCustomActionUrl(content?.biz_custom_action_url);
|
|
158
|
+
|
|
159
|
+
if (repliedMsgType === "picture") {
|
|
160
|
+
return {
|
|
161
|
+
mediaDownloadCode: trimString(content?.downloadCode),
|
|
162
|
+
mediaType: trimString(content?.downloadCode) ? "image" : undefined,
|
|
163
|
+
previewText: buildQuotedMessageTypePlaceholder("picture"),
|
|
164
|
+
previewMessageType: "picture",
|
|
165
|
+
previewSenderId: trimString(repliedMsg.senderId),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (repliedMsgType === "richText") {
|
|
170
|
+
return {
|
|
171
|
+
mediaDownloadCode: richTextQuote?.pictureDownloadCode,
|
|
172
|
+
mediaType: richTextQuote?.pictureDownloadCode ? "image" : undefined,
|
|
173
|
+
previewText:
|
|
174
|
+
trimString(richTextQuote?.summary) || buildQuotedMessageTypePlaceholder("richText"),
|
|
175
|
+
previewMessageType: "richText",
|
|
176
|
+
previewSenderId: trimString(repliedMsg.senderId),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (repliedMsgType === "unknownMsgType") {
|
|
181
|
+
return {
|
|
182
|
+
isQuotedFile: true,
|
|
183
|
+
fileCreatedAt: repliedMsg.createdAt,
|
|
184
|
+
previewText: buildQuotedMessageTypePlaceholder("unknownMsgType", fileName),
|
|
185
|
+
previewMessageType: "unknownMsgType",
|
|
186
|
+
previewFileName: fileName,
|
|
187
|
+
previewSenderId: trimString(repliedMsg.senderId),
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (repliedMsgType === "interactiveCard") {
|
|
192
|
+
const isBotCard = repliedMsg.senderId === data.chatbotUserId;
|
|
193
|
+
if (isBotCard) {
|
|
194
|
+
return {
|
|
195
|
+
isQuotedCard: true,
|
|
196
|
+
cardCreatedAt: repliedMsg.createdAt,
|
|
197
|
+
processQueryKey: trimString(data.originalProcessQueryKey),
|
|
198
|
+
previewText:
|
|
199
|
+
trimString(content?.text) || buildQuotedMessageTypePlaceholder("interactiveCard"),
|
|
200
|
+
previewMessageType: "interactiveCard",
|
|
201
|
+
previewSenderId: trimString(repliedMsg.senderId),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
isQuotedDocCard: true,
|
|
207
|
+
fileCreatedAt: repliedMsg.createdAt,
|
|
208
|
+
previewText:
|
|
209
|
+
trimString(content?.text) ||
|
|
210
|
+
buildQuotedMessageTypePlaceholder(docMeta ? "interactiveCardFile" : "interactiveCard"),
|
|
211
|
+
previewMessageType: docMeta ? "interactiveCardFile" : "interactiveCard",
|
|
212
|
+
previewSenderId: trimString(repliedMsg.senderId),
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const textPreview =
|
|
217
|
+
trimString(content?.text) ||
|
|
218
|
+
trimString(richTextQuote?.summary) ||
|
|
219
|
+
buildQuotedMessageTypePlaceholder(repliedMsgType, fileName);
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
mediaDownloadCode: richTextQuote?.pictureDownloadCode,
|
|
223
|
+
mediaType: richTextQuote?.pictureDownloadCode ? "image" : undefined,
|
|
224
|
+
previewText: textPreview,
|
|
225
|
+
previewMessageType: repliedMsgType,
|
|
226
|
+
previewFileName: fileName,
|
|
227
|
+
previewSenderId: trimString(repliedMsg.senderId),
|
|
228
|
+
};
|
|
229
|
+
}
|
|
2
230
|
|
|
3
231
|
/**
|
|
4
232
|
* Auto-detect markdown usage and derive message title.
|
|
@@ -24,76 +252,209 @@ export function detectMarkdownAndExtractTitle(
|
|
|
24
252
|
return { useMarkdown, title };
|
|
25
253
|
}
|
|
26
254
|
|
|
255
|
+
function isMarkdownTableSeparator(line: string): boolean {
|
|
256
|
+
const normalized = line.trim();
|
|
257
|
+
if (!normalized.includes("-")) {
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
const cells = normalized
|
|
261
|
+
.replace(/^\|/, "")
|
|
262
|
+
.replace(/\|$/, "")
|
|
263
|
+
.split("|")
|
|
264
|
+
.map((cell) => cell.trim());
|
|
265
|
+
return cells.length > 0 && cells.every((cell) => /^:?-{3,}:?$/.test(cell));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function isMarkdownTableRow(line: string): boolean {
|
|
269
|
+
const trimmed = line.trim();
|
|
270
|
+
return trimmed.includes("|") && !trimmed.startsWith("```");
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function parseMarkdownTableRow(line: string): string[] {
|
|
274
|
+
return line
|
|
275
|
+
.trim()
|
|
276
|
+
.replace(/^\|/, "")
|
|
277
|
+
.replace(/\|$/, "")
|
|
278
|
+
.split("|")
|
|
279
|
+
.map((cell) => cell.trim());
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function renderMarkdownTable(lines: string[]): string {
|
|
283
|
+
const rows = lines.map(parseMarkdownTableRow).filter((cells) => cells.length > 0);
|
|
284
|
+
return rows.map((cells) => cells.join(" | ")).join(" \n");
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export function convertMarkdownTablesToPlainText(text: string): string {
|
|
288
|
+
const lines = text.split("\n");
|
|
289
|
+
const output: string[] = [];
|
|
290
|
+
let index = 0;
|
|
291
|
+
let inCodeFence = false;
|
|
292
|
+
|
|
293
|
+
while (index < lines.length) {
|
|
294
|
+
const line = lines[index] || "";
|
|
295
|
+
if (line.trim().startsWith("```")) {
|
|
296
|
+
inCodeFence = !inCodeFence;
|
|
297
|
+
output.push(line);
|
|
298
|
+
index += 1;
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (
|
|
303
|
+
!inCodeFence &&
|
|
304
|
+
index + 1 < lines.length &&
|
|
305
|
+
isMarkdownTableRow(line) &&
|
|
306
|
+
isMarkdownTableSeparator(lines[index + 1] || "")
|
|
307
|
+
) {
|
|
308
|
+
const tableLines = [line];
|
|
309
|
+
index += 2;
|
|
310
|
+
while (index < lines.length && isMarkdownTableRow(lines[index] || "")) {
|
|
311
|
+
tableLines.push(lines[index] || "");
|
|
312
|
+
index += 1;
|
|
313
|
+
}
|
|
314
|
+
output.push(renderMarkdownTable(tableLines));
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
output.push(line);
|
|
319
|
+
index += 1;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return output.join("\n");
|
|
323
|
+
}
|
|
324
|
+
|
|
27
325
|
export function extractMessageContent(data: DingTalkInboundMessage): MessageContent {
|
|
28
326
|
const msgtype = data.msgtype || "text";
|
|
327
|
+
const atMentions: AtMention[] = [];
|
|
29
328
|
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
|
|
329
|
+
// 提取通过 @picker 选中的真实用户的 dingtalkId
|
|
330
|
+
// 这些是真实钉钉用户,不包含 agent 名(如 @frontend)
|
|
331
|
+
const atUserDingtalkIds = data.atUsers?.map((u) => u.dingtalkId).filter(Boolean);
|
|
332
|
+
|
|
333
|
+
const formatQuotedContent = (): QuotedInfo | null => {
|
|
334
|
+
const textField = data.text;
|
|
33
335
|
|
|
34
336
|
if (textField?.isReplyMsg && textField?.repliedMsg) {
|
|
35
337
|
const repliedMsg = textField.repliedMsg;
|
|
36
|
-
const
|
|
338
|
+
const repliedMsgType = trimString(repliedMsg.msgType);
|
|
339
|
+
const content = repliedMsg.content;
|
|
340
|
+
const repliedMsgId = trimString(repliedMsg.msgId) || trimString(data.originalMsgId);
|
|
341
|
+
const repliedPreview = buildRepliedMessagePreview({ data, repliedMsg });
|
|
37
342
|
|
|
38
|
-
if (content?.
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
343
|
+
if (repliedMsgType === "picture" && content?.downloadCode) {
|
|
344
|
+
return {
|
|
345
|
+
mediaDownloadCode: content.downloadCode,
|
|
346
|
+
mediaType: "image",
|
|
347
|
+
...repliedPreview,
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (repliedMsgType === "richText") {
|
|
352
|
+
const richTextQuote = extractRichTextQuoteParts(content?.richText);
|
|
353
|
+
if (richTextQuote) {
|
|
354
|
+
return {
|
|
355
|
+
msgId: repliedMsgId,
|
|
356
|
+
mediaDownloadCode: richTextQuote.pictureDownloadCode,
|
|
357
|
+
mediaType: richTextQuote.pictureDownloadCode ? "image" : undefined,
|
|
358
|
+
...repliedPreview,
|
|
359
|
+
};
|
|
42
360
|
}
|
|
43
361
|
}
|
|
44
362
|
|
|
45
|
-
if (
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
363
|
+
if (repliedMsgType === "unknownMsgType") {
|
|
364
|
+
return {
|
|
365
|
+
isQuotedFile: true,
|
|
366
|
+
fileCreatedAt: repliedMsg.createdAt,
|
|
367
|
+
msgId: repliedMsgId,
|
|
368
|
+
...repliedPreview,
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (repliedMsgType === "interactiveCard") {
|
|
373
|
+
const isBotCard = repliedMsg.senderId === data.chatbotUserId;
|
|
374
|
+
if (isBotCard) {
|
|
375
|
+
return {
|
|
376
|
+
isQuotedCard: true,
|
|
377
|
+
cardCreatedAt: repliedMsg.createdAt,
|
|
378
|
+
processQueryKey: data.originalProcessQueryKey,
|
|
379
|
+
msgId: repliedMsgId,
|
|
380
|
+
...repliedPreview,
|
|
381
|
+
};
|
|
59
382
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
383
|
+
|
|
384
|
+
return {
|
|
385
|
+
isQuotedDocCard: true,
|
|
386
|
+
fileCreatedAt: repliedMsg.createdAt,
|
|
387
|
+
msgId: repliedMsgId,
|
|
388
|
+
...repliedPreview,
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (repliedMsgType && repliedMsgId) {
|
|
393
|
+
return { msgId: repliedMsgId, ...repliedPreview };
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// No msgType — backward compat: extract text or richText from content.
|
|
397
|
+
if (content?.text?.trim()) {
|
|
398
|
+
return repliedMsgId ? { msgId: repliedMsgId, ...repliedPreview } : null;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (content?.richText && Array.isArray(content.richText)) {
|
|
402
|
+
const richTextQuote = extractRichTextQuoteParts(content.richText);
|
|
403
|
+
if (richTextQuote) {
|
|
404
|
+
return {
|
|
405
|
+
msgId: repliedMsgId,
|
|
406
|
+
mediaDownloadCode: richTextQuote.pictureDownloadCode,
|
|
407
|
+
mediaType: richTextQuote.pictureDownloadCode ? "image" : undefined,
|
|
408
|
+
...repliedPreview,
|
|
409
|
+
};
|
|
63
410
|
}
|
|
64
411
|
}
|
|
65
412
|
}
|
|
66
413
|
|
|
67
|
-
// Some clients only send originalMsgId for rich media reply messages.
|
|
68
414
|
if (textField?.isReplyMsg && !textField?.repliedMsg && data.originalMsgId) {
|
|
69
|
-
return
|
|
415
|
+
return {
|
|
416
|
+
msgId: data.originalMsgId,
|
|
417
|
+
};
|
|
70
418
|
}
|
|
71
419
|
|
|
72
420
|
if (data.quoteMessage) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
421
|
+
if (data.quoteMessage.msgId) {
|
|
422
|
+
return {
|
|
423
|
+
msgId: data.quoteMessage.msgId,
|
|
424
|
+
...buildLegacyQuoteMessagePreview(data.quoteMessage),
|
|
425
|
+
};
|
|
76
426
|
}
|
|
77
427
|
}
|
|
78
428
|
|
|
79
|
-
|
|
80
|
-
return `[引用消息: "${data.content.quoteContent}"]\n\n`;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
return "";
|
|
429
|
+
return null;
|
|
84
430
|
};
|
|
85
431
|
|
|
86
|
-
const
|
|
432
|
+
const quoted = formatQuotedContent();
|
|
87
433
|
|
|
88
|
-
// Unified extraction by DingTalk msgtype for downstream routing/agent processing.
|
|
89
434
|
if (msgtype === "text") {
|
|
90
|
-
|
|
435
|
+
const textContent = data.text?.content?.trim() || "";
|
|
436
|
+
|
|
437
|
+
// Strip quoted prefix before extracting @mentions to avoid matching @names inside quotes.
|
|
438
|
+
const textForAtExtraction = textContent.replace(/^\[引用[^\]]*\]\s*/, "");
|
|
439
|
+
// Match @name but exclude email-like patterns (user@domain.com) and emoji (@_@).
|
|
440
|
+
const atMatches = textForAtExtraction.matchAll(/(?<!\w)@([^\s@.]+)(?!\.\w)/g);
|
|
441
|
+
for (const match of atMatches) {
|
|
442
|
+
atMentions.push({ name: match[1].trim() });
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return {
|
|
446
|
+
text: textContent,
|
|
447
|
+
messageType: "text",
|
|
448
|
+
quoted: quoted ?? undefined,
|
|
449
|
+
atMentions,
|
|
450
|
+
atUserDingtalkIds,
|
|
451
|
+
};
|
|
91
452
|
}
|
|
92
453
|
|
|
93
454
|
if (msgtype === "richText") {
|
|
94
455
|
const richTextParts = data.content?.richText || [];
|
|
95
456
|
let text = "";
|
|
96
|
-
|
|
457
|
+
const pictureDownloadCodes: string[] = [];
|
|
97
458
|
// Keep first image downloadCode while preserving readable text and @mention parts.
|
|
98
459
|
for (const part of richTextParts) {
|
|
99
460
|
if (part.text && (part.type === "text" || part.type === undefined)) {
|
|
@@ -101,17 +462,28 @@ export function extractMessageContent(data: DingTalkInboundMessage): MessageCont
|
|
|
101
462
|
}
|
|
102
463
|
if (part.type === "at" && part.atName) {
|
|
103
464
|
text += `@${part.atName} `;
|
|
465
|
+
// 提取 @ 提及信息,包括 atUserId
|
|
466
|
+
atMentions.push({
|
|
467
|
+
name: part.atName.trim(),
|
|
468
|
+
userId: part.atUserId,
|
|
469
|
+
});
|
|
104
470
|
}
|
|
105
|
-
if (part.type === "picture" && part.downloadCode
|
|
106
|
-
|
|
471
|
+
if (part.type === "picture" && part.downloadCode) {
|
|
472
|
+
pictureDownloadCodes.push(part.downloadCode);
|
|
107
473
|
}
|
|
108
474
|
}
|
|
475
|
+
const uniquePictureDownloadCodes = [...new Set(pictureDownloadCodes)];
|
|
476
|
+
const pictureDownloadCode = uniquePictureDownloadCodes[0];
|
|
109
477
|
return {
|
|
110
|
-
text:
|
|
111
|
-
quotedPrefix + (text.trim() || (pictureDownloadCode ? "<media:image>" : "[富文本消息]")),
|
|
478
|
+
text: text.trim() || (pictureDownloadCode ? "<media:image>" : "[富文本消息]"),
|
|
112
479
|
mediaPath: pictureDownloadCode,
|
|
480
|
+
mediaPaths: uniquePictureDownloadCodes.length > 0 ? uniquePictureDownloadCodes : undefined,
|
|
113
481
|
mediaType: pictureDownloadCode ? "image" : undefined,
|
|
482
|
+
mediaTypes: uniquePictureDownloadCodes.length > 0 ? uniquePictureDownloadCodes.map(() => "image") : undefined,
|
|
114
483
|
messageType: "richText",
|
|
484
|
+
quoted: quoted ?? undefined,
|
|
485
|
+
atMentions,
|
|
486
|
+
atUserDingtalkIds,
|
|
115
487
|
};
|
|
116
488
|
}
|
|
117
489
|
|
|
@@ -121,6 +493,8 @@ export function extractMessageContent(data: DingTalkInboundMessage): MessageCont
|
|
|
121
493
|
mediaPath: data.content?.downloadCode,
|
|
122
494
|
mediaType: "image",
|
|
123
495
|
messageType: "picture",
|
|
496
|
+
atMentions,
|
|
497
|
+
atUserDingtalkIds,
|
|
124
498
|
};
|
|
125
499
|
}
|
|
126
500
|
|
|
@@ -130,6 +504,8 @@ export function extractMessageContent(data: DingTalkInboundMessage): MessageCont
|
|
|
130
504
|
mediaPath: data.content?.downloadCode,
|
|
131
505
|
mediaType: "audio",
|
|
132
506
|
messageType: "audio",
|
|
507
|
+
atMentions,
|
|
508
|
+
atUserDingtalkIds,
|
|
133
509
|
};
|
|
134
510
|
}
|
|
135
511
|
|
|
@@ -139,6 +515,8 @@ export function extractMessageContent(data: DingTalkInboundMessage): MessageCont
|
|
|
139
515
|
mediaPath: data.content?.downloadCode,
|
|
140
516
|
mediaType: "video",
|
|
141
517
|
messageType: "video",
|
|
518
|
+
atMentions,
|
|
519
|
+
atUserDingtalkIds,
|
|
142
520
|
};
|
|
143
521
|
}
|
|
144
522
|
|
|
@@ -148,9 +526,72 @@ export function extractMessageContent(data: DingTalkInboundMessage): MessageCont
|
|
|
148
526
|
mediaPath: data.content?.downloadCode,
|
|
149
527
|
mediaType: "file",
|
|
150
528
|
messageType: "file",
|
|
529
|
+
atMentions,
|
|
530
|
+
atUserDingtalkIds,
|
|
151
531
|
};
|
|
152
532
|
}
|
|
153
533
|
|
|
154
|
-
|
|
155
|
-
|
|
534
|
+
if (msgtype === "interactiveCard") {
|
|
535
|
+
const docMeta = parseBizCustomActionUrl(data.content?.biz_custom_action_url);
|
|
536
|
+
if (docMeta) {
|
|
537
|
+
return {
|
|
538
|
+
text: "[钉钉文档]\n\n",
|
|
539
|
+
messageType: "interactiveCardFile",
|
|
540
|
+
docSpaceId: docMeta.spaceId,
|
|
541
|
+
docFileId: docMeta.fileId,
|
|
542
|
+
quoted: quoted ?? undefined,
|
|
543
|
+
atMentions,
|
|
544
|
+
atUserDingtalkIds,
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
return {
|
|
548
|
+
text: data.text?.content?.trim() || "[interactiveCard消息]",
|
|
549
|
+
messageType: msgtype,
|
|
550
|
+
quoted: quoted ?? undefined,
|
|
551
|
+
atMentions,
|
|
552
|
+
atUserDingtalkIds,
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
if (msgtype === "chatRecord") {
|
|
556
|
+
const content = data.content as Record<string, unknown> | undefined;
|
|
557
|
+
const summary = typeof content?.summary === "string" ? content.summary.trim() : "";
|
|
558
|
+
const rawRecord = content?.chatRecord;
|
|
559
|
+
if (
|
|
560
|
+
summary === "[]" ||
|
|
561
|
+
(typeof rawRecord === "string" && rawRecord.trim() === "[]") ||
|
|
562
|
+
(Array.isArray(rawRecord) && rawRecord.length === 0)
|
|
563
|
+
) {
|
|
564
|
+
return {
|
|
565
|
+
text: "[系统提示] 没有读到引用记录(chatRecord 为空)。请改用逐条转发、复制原文,或重新转发非空聊天记录。",
|
|
566
|
+
messageType: "chatRecord",
|
|
567
|
+
quoted: quoted ?? undefined,
|
|
568
|
+
atMentions,
|
|
569
|
+
atUserDingtalkIds,
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
if (summary) {
|
|
573
|
+
return {
|
|
574
|
+
text: `[聊天记录摘要] ${summary}`,
|
|
575
|
+
messageType: "chatRecord",
|
|
576
|
+
quoted: quoted ?? undefined,
|
|
577
|
+
atMentions,
|
|
578
|
+
atUserDingtalkIds,
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
return {
|
|
582
|
+
text: "[chatRecord消息: 无可读内容]",
|
|
583
|
+
messageType: "chatRecord",
|
|
584
|
+
quoted: quoted ?? undefined,
|
|
585
|
+
atMentions,
|
|
586
|
+
atUserDingtalkIds,
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
return {
|
|
591
|
+
text: data.text?.content?.trim() || `[${msgtype}消息]`,
|
|
592
|
+
messageType: msgtype,
|
|
593
|
+
quoted: quoted ?? undefined,
|
|
594
|
+
atMentions,
|
|
595
|
+
atUserDingtalkIds,
|
|
596
|
+
};
|
|
156
597
|
}
|