@soimy/dingtalk 3.5.1 → 3.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -24
- package/openclaw.plugin.json +695 -0
- package/package.json +12 -7
- package/src/ack-reaction-service.ts +1 -1
- package/src/auth.ts +1 -1
- package/src/card/card-action-handler.ts +1 -1
- package/src/card/card-stop-handler.ts +1 -1
- package/src/card/card-streaming-mode.ts +30 -0
- package/src/card/reasoning-answer-split.ts +162 -0
- package/src/card/reasoning-block-assembler.ts +157 -0
- package/src/card-callback-service.ts +1 -1
- package/src/card-draft-controller.ts +117 -6
- package/src/card-service.ts +112 -1
- package/src/channel.ts +131 -96
- package/src/command/card-stop-command.ts +4 -22
- package/src/command/inbound-command-dispatch-service.ts +464 -0
- package/src/config-schema.ts +62 -38
- package/src/config.ts +25 -3
- package/src/docs-service.ts +5 -5
- package/src/http-client.ts +20 -0
- package/src/inbound-handler.ts +475 -501
- package/src/logger-context.ts +16 -2
- package/src/media-utils.ts +166 -10
- package/src/message-utils.ts +33 -5
- package/src/{attachment-text-extractor.ts → messaging/attachment-text-extractor.ts} +1 -1
- package/src/{quoted-file-service.ts → messaging/quoted-file-service.ts} +14 -9
- package/src/onboarding.ts +29 -0
- package/src/plugin-sdk-channel-actions-augment.ts +11 -0
- package/src/reply-strategy-card.ts +294 -28
- package/src/reply-strategy-markdown.ts +124 -19
- package/src/reply-strategy.ts +22 -2
- package/src/send-service.ts +178 -7
- package/src/targeting/agent-routing.ts +55 -32
- package/src/{group-members-store.ts → targeting/group-members-store.ts} +1 -1
- package/src/types.ts +60 -4
- package/src/utils.ts +190 -0
package/src/inbound-handler.ts
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
|
-
import
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import axios from "./http-client";
|
|
2
4
|
import { normalizeAllowFrom, isSenderAllowed, resolveGroupAccess } from "./access-control";
|
|
3
5
|
import { buildAgentSessionKey, resolveSubAgentRoute, dispatchSubAgents } from "./targeting/agent-routing";
|
|
4
6
|
import { classifyAckReactionEmoji } from "./ack-reaction-classifier";
|
|
5
7
|
import { attachNativeAckReaction } from "./ack-reaction-service";
|
|
6
8
|
import { createDynamicAckReactionController } from "./ack-reaction/dynamic-ack-reaction-controller";
|
|
7
|
-
import { extractAttachmentText } from "./attachment-text-extractor";
|
|
9
|
+
import { extractAttachmentText } from "./messaging/attachment-text-extractor";
|
|
8
10
|
import { getAccessToken } from "./auth";
|
|
9
11
|
import { createAICard, finishAICard, isCardInTerminalState } from "./card-service";
|
|
10
|
-
import {
|
|
12
|
+
import { handleInboundCommandDispatch } from "./command/inbound-command-dispatch-service";
|
|
13
|
+
import { resolveAckReactionSetting, resolveGroupConfig, resolveRelativePath, resolveRobotCode } from "./config";
|
|
11
14
|
import { AICardStatus } from "./types";
|
|
12
15
|
import {
|
|
13
16
|
isCardRunStopRequested,
|
|
@@ -15,35 +18,10 @@ import {
|
|
|
15
18
|
removeCardRun,
|
|
16
19
|
} from "./card/card-run-registry";
|
|
17
20
|
import {
|
|
18
|
-
applyManualTargetLearningRule,
|
|
19
|
-
applyManualTargetsLearningRule,
|
|
20
|
-
applyManualGlobalLearningRule,
|
|
21
|
-
applyManualSessionLearningNote,
|
|
22
|
-
applyTargetSetLearningRule,
|
|
23
21
|
buildLearningContextBlock,
|
|
24
|
-
createOrUpdateTargetSet,
|
|
25
|
-
deleteManualRule,
|
|
26
|
-
disableManualRule,
|
|
27
22
|
isLearningEnabled,
|
|
28
|
-
listLearningTargetSets,
|
|
29
|
-
listScopedLearningRules,
|
|
30
|
-
resolveManualForcedReply,
|
|
31
23
|
} from "./feedback-learning-service";
|
|
32
|
-
import { formatGroupMembers, noteGroupMember } from "./group-members-store";
|
|
33
|
-
import {
|
|
34
|
-
formatLearnAppliedReply,
|
|
35
|
-
formatLearnCommandHelp,
|
|
36
|
-
formatLearnDeletedReply,
|
|
37
|
-
formatLearnDisabledReply,
|
|
38
|
-
formatLearnListReply,
|
|
39
|
-
formatOwnerOnlyDeniedReply,
|
|
40
|
-
formatOwnerStatusReply,
|
|
41
|
-
formatTargetSetSavedReply,
|
|
42
|
-
formatWhereAmIReply,
|
|
43
|
-
formatWhoAmIReply,
|
|
44
|
-
isLearningOwner,
|
|
45
|
-
parseLearnCommand,
|
|
46
|
-
} from "./learning-command-service";
|
|
24
|
+
import { formatGroupMembers, noteGroupMember } from "./targeting/group-members-store";
|
|
47
25
|
import { setCurrentLogger } from "./logger-context";
|
|
48
26
|
import { prepareMediaInput, resolveOutboundMediaType } from "./media-utils";
|
|
49
27
|
import {
|
|
@@ -63,21 +41,11 @@ import {
|
|
|
63
41
|
clearProactiveRiskObservationsForTest,
|
|
64
42
|
getProactiveRiskObservationForAny,
|
|
65
43
|
} from "./proactive-risk-registry";
|
|
66
|
-
import { downloadGroupFile, getUnionIdByStaffId, resolveQuotedFile } from "./quoted-file-service";
|
|
44
|
+
import { downloadGroupFile, getUnionIdByStaffId, resolveQuotedFile } from "./messaging/quoted-file-service";
|
|
67
45
|
import { createReplyStrategy } from "./reply-strategy";
|
|
68
46
|
import type { DeliverPayload } from "./reply-strategy";
|
|
69
47
|
import { getDingTalkRuntime } from "./runtime";
|
|
70
48
|
import { sendBySession, sendMessage, sendProactiveMedia } from "./send-service";
|
|
71
|
-
import {
|
|
72
|
-
formatSessionAliasBoundReply,
|
|
73
|
-
formatSessionAliasClearedReply,
|
|
74
|
-
formatSessionAliasReply,
|
|
75
|
-
formatSessionAliasSetReply,
|
|
76
|
-
formatSessionAliasUnboundReply,
|
|
77
|
-
formatSessionAliasValidationErrorReply,
|
|
78
|
-
parseSessionCommand,
|
|
79
|
-
validateSessionAlias,
|
|
80
|
-
} from "./session-command-service";
|
|
81
49
|
import { acquireSessionLock } from "./session-lock";
|
|
82
50
|
import {
|
|
83
51
|
clearSessionPeerOverride,
|
|
@@ -89,15 +57,194 @@ import {
|
|
|
89
57
|
upsertObservedGroupTarget,
|
|
90
58
|
upsertObservedUserTarget,
|
|
91
59
|
} from "./targeting/target-directory-store";
|
|
92
|
-
import type { DingTalkConfig, HandleDingTalkMessageParams, MediaFile } from "./types";
|
|
93
|
-
import { formatDingTalkErrorPayloadLog, getErrorMessage, getErrorResponseData, maskSensitiveData } from "./utils";
|
|
60
|
+
import type { DingTalkConfig, HandleDingTalkMessageParams, Logger, MediaFile } from "./types";
|
|
61
|
+
import { formatDingTalkErrorPayloadLog, getErrorMessage, getErrorResponseData, maskSensitiveData, parseBooleanLike } from "./utils";
|
|
94
62
|
import { isAbortRequestText } from "openclaw/plugin-sdk/reply-runtime";
|
|
63
|
+
import { parseInlineDirectives } from "openclaw/plugin-sdk/text-runtime";
|
|
95
64
|
|
|
96
65
|
const DEFAULT_PROACTIVE_HINT_COOLDOWN_HOURS = 24;
|
|
97
66
|
const MIN_THINKING_REACTION_VISIBLE_MS = 1200;
|
|
98
67
|
const MAX_DYNAMIC_ACK_DISPOSE_WAIT_MS = 500;
|
|
99
68
|
const ATTACHMENT_TEXT_PREFIX = "[附件内容摘录]";
|
|
69
|
+
const MEDIA_DIRECTIVE_PREFIX = "MEDIA:";
|
|
70
|
+
const STANDALONE_MEDIA_PATH_EXTENSIONS = new Set([
|
|
71
|
+
".jpg",
|
|
72
|
+
".jpeg",
|
|
73
|
+
".png",
|
|
74
|
+
".gif",
|
|
75
|
+
".bmp",
|
|
76
|
+
".ogg",
|
|
77
|
+
".amr",
|
|
78
|
+
".mp3",
|
|
79
|
+
".wav",
|
|
80
|
+
".mp4",
|
|
81
|
+
".avi",
|
|
82
|
+
".mov",
|
|
83
|
+
".doc",
|
|
84
|
+
".docx",
|
|
85
|
+
".xls",
|
|
86
|
+
".xlsx",
|
|
87
|
+
".ppt",
|
|
88
|
+
".pptx",
|
|
89
|
+
".zip",
|
|
90
|
+
".pdf",
|
|
91
|
+
".rar",
|
|
92
|
+
]);
|
|
100
93
|
const proactiveHintLastSentAt = new Map<string, number>();
|
|
94
|
+
const sessionReasoningLevelCache = new Map<string, {
|
|
95
|
+
updatedAt?: number;
|
|
96
|
+
reasoningLevel?: string;
|
|
97
|
+
}>();
|
|
98
|
+
type ReplyMode = "card" | "markdown";
|
|
99
|
+
|
|
100
|
+
function resolveQuotedContextAllowFrom(
|
|
101
|
+
config: DingTalkConfig,
|
|
102
|
+
groupId: string,
|
|
103
|
+
): string[] | undefined {
|
|
104
|
+
const groupConfig = resolveGroupConfig(config, groupId);
|
|
105
|
+
return groupConfig?.groupAllowFrom ?? config.groupAllowFrom;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function resolveQuotedVisibilitySenderId(params: {
|
|
109
|
+
quotedSenderId?: string;
|
|
110
|
+
currentSenderId: string;
|
|
111
|
+
currentSenderOriginalId: string;
|
|
112
|
+
}): string | undefined {
|
|
113
|
+
const quotedSenderId = (params.quotedSenderId || "").trim();
|
|
114
|
+
if (!quotedSenderId) {
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
if (quotedSenderId === params.currentSenderOriginalId) {
|
|
118
|
+
return params.currentSenderId;
|
|
119
|
+
}
|
|
120
|
+
return quotedSenderId;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function filterQuotedRuntimeContext(params: {
|
|
124
|
+
context: ReturnType<typeof resolveQuotedRuntimeContext>;
|
|
125
|
+
config: DingTalkConfig;
|
|
126
|
+
isDirect: boolean;
|
|
127
|
+
groupId: string;
|
|
128
|
+
quotedSenderId?: string;
|
|
129
|
+
currentSenderId: string;
|
|
130
|
+
currentSenderOriginalId: string;
|
|
131
|
+
}): ReturnType<typeof resolveQuotedRuntimeContext> {
|
|
132
|
+
const { context, config, isDirect, groupId, quotedSenderId, currentSenderId, currentSenderOriginalId } = params;
|
|
133
|
+
if (!context || isDirect) {
|
|
134
|
+
return context;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const mode = config.contextVisibility || "all";
|
|
138
|
+
if (mode === "all") {
|
|
139
|
+
return context;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const allow = normalizeAllowFrom(resolveQuotedContextAllowFrom(config, groupId));
|
|
143
|
+
const senderId = resolveQuotedVisibilitySenderId({
|
|
144
|
+
quotedSenderId,
|
|
145
|
+
currentSenderId,
|
|
146
|
+
currentSenderOriginalId,
|
|
147
|
+
});
|
|
148
|
+
const senderAllowed =
|
|
149
|
+
allow.hasEntries && !!senderId
|
|
150
|
+
? isSenderAllowed({ allow, senderId })
|
|
151
|
+
: false;
|
|
152
|
+
|
|
153
|
+
if (senderAllowed) {
|
|
154
|
+
return context;
|
|
155
|
+
}
|
|
156
|
+
if (mode === "allowlist_quote") {
|
|
157
|
+
return {
|
|
158
|
+
...context,
|
|
159
|
+
untrustedContext: undefined,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function readSessionReasoningLevel(params: {
|
|
166
|
+
storePath?: string;
|
|
167
|
+
sessionKey: string;
|
|
168
|
+
sessionUpdatedAt?: number;
|
|
169
|
+
log?: Logger;
|
|
170
|
+
}): string | undefined {
|
|
171
|
+
if (!params.storePath || !params.sessionKey) {
|
|
172
|
+
return undefined;
|
|
173
|
+
}
|
|
174
|
+
const cacheKey = `${params.storePath}:${params.sessionKey}`;
|
|
175
|
+
const cached = sessionReasoningLevelCache.get(cacheKey);
|
|
176
|
+
if (
|
|
177
|
+
cached
|
|
178
|
+
&& params.sessionUpdatedAt !== undefined
|
|
179
|
+
&& cached.updatedAt === params.sessionUpdatedAt
|
|
180
|
+
) {
|
|
181
|
+
return cached.reasoningLevel;
|
|
182
|
+
}
|
|
183
|
+
try {
|
|
184
|
+
const raw = fs.readFileSync(params.storePath, "utf8");
|
|
185
|
+
const parsed = JSON.parse(raw) as Record<string, { reasoningLevel?: unknown }>;
|
|
186
|
+
const value = parsed?.[params.sessionKey]?.reasoningLevel;
|
|
187
|
+
const reasoningLevel = typeof value === "string" ? value.trim().toLowerCase() : undefined;
|
|
188
|
+
sessionReasoningLevelCache.set(cacheKey, {
|
|
189
|
+
updatedAt: params.sessionUpdatedAt,
|
|
190
|
+
reasoningLevel,
|
|
191
|
+
});
|
|
192
|
+
return reasoningLevel;
|
|
193
|
+
} catch (err: unknown) {
|
|
194
|
+
params.log?.debug?.(
|
|
195
|
+
`[DingTalk][Session] Failed to read session reasoning level from ${params.storePath}: ${getErrorMessage(err)}`,
|
|
196
|
+
);
|
|
197
|
+
return undefined;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function shouldDisableBlockStreamingForReplyMode(params: {
|
|
202
|
+
replyMode: ReplyMode;
|
|
203
|
+
reasoningLevel?: string;
|
|
204
|
+
sessionKey: string;
|
|
205
|
+
log?: Logger;
|
|
206
|
+
}): boolean {
|
|
207
|
+
if (params.replyMode === "markdown") {
|
|
208
|
+
const shouldDisable = params.reasoningLevel === "on" || params.reasoningLevel === "stream";
|
|
209
|
+
if (shouldDisable) {
|
|
210
|
+
params.log?.debug?.(
|
|
211
|
+
`[DingTalk][Markdown] Disable block streaming for reasoningLevel=${params.reasoningLevel} sessionKey=${params.sessionKey}`,
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
return shouldDisable;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const shouldDisable = params.reasoningLevel !== "on";
|
|
218
|
+
if (!shouldDisable) {
|
|
219
|
+
params.log?.debug?.(
|
|
220
|
+
`[DingTalk][Card] Enable block streaming for reasoningLevel=${params.reasoningLevel} sessionKey=${params.sessionKey}`,
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
return shouldDisable;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function resolveLegacyCardStreamReasoningForInternalUse(params: {
|
|
227
|
+
cfg: HandleDingTalkMessageParams["cfg"];
|
|
228
|
+
accountId: string;
|
|
229
|
+
}): boolean | undefined {
|
|
230
|
+
const dingtalk = (params.cfg?.channels?.dingtalk ?? null) as
|
|
231
|
+
| (Record<string, unknown> & {
|
|
232
|
+
cardStreamReasoning?: unknown;
|
|
233
|
+
accounts?: Record<string, Record<string, unknown> | undefined>;
|
|
234
|
+
})
|
|
235
|
+
| null;
|
|
236
|
+
if (!dingtalk) {
|
|
237
|
+
return undefined;
|
|
238
|
+
}
|
|
239
|
+
const accountConfig = params.accountId ? dingtalk.accounts?.[params.accountId] : undefined;
|
|
240
|
+
if (typeof accountConfig?.cardStreamReasoning === "boolean") {
|
|
241
|
+
return accountConfig.cardStreamReasoning;
|
|
242
|
+
}
|
|
243
|
+
if (typeof dingtalk.cardStreamReasoning === "boolean") {
|
|
244
|
+
return dingtalk.cardStreamReasoning;
|
|
245
|
+
}
|
|
246
|
+
return undefined;
|
|
247
|
+
}
|
|
101
248
|
|
|
102
249
|
function resolvePinnedMainDmOwner(params: {
|
|
103
250
|
dmScope?: string;
|
|
@@ -239,6 +386,7 @@ function buildGroupTurnContextPrompt(params: {
|
|
|
239
386
|
|
|
240
387
|
type ReplyStreamPayload = {
|
|
241
388
|
text?: string;
|
|
389
|
+
isReasoning?: boolean;
|
|
242
390
|
};
|
|
243
391
|
|
|
244
392
|
type ReplyChunkInfo = {
|
|
@@ -256,6 +404,7 @@ export async function downloadMedia(
|
|
|
256
404
|
config: DingTalkConfig,
|
|
257
405
|
downloadCode: string,
|
|
258
406
|
log?: any,
|
|
407
|
+
originalFilename?: string,
|
|
259
408
|
): Promise<MediaFile | null> {
|
|
260
409
|
const rt = getDingTalkRuntime();
|
|
261
410
|
let downloadUrl: string | undefined;
|
|
@@ -329,9 +478,13 @@ export async function downloadMedia(
|
|
|
329
478
|
|
|
330
479
|
const maxBytes =
|
|
331
480
|
config.mediaMaxMb && config.mediaMaxMb > 0 ? config.mediaMaxMb * 1024 * 1024 : undefined;
|
|
332
|
-
const saved =
|
|
333
|
-
|
|
334
|
-
|
|
481
|
+
const saved = await rt.channel.media.saveMediaBuffer(
|
|
482
|
+
buffer,
|
|
483
|
+
contentType,
|
|
484
|
+
"inbound",
|
|
485
|
+
maxBytes,
|
|
486
|
+
originalFilename,
|
|
487
|
+
);
|
|
335
488
|
log?.debug?.(`[DingTalk] Media saved: ${saved.path}`);
|
|
336
489
|
return { path: saved.path, mimeType: saved.contentType ?? contentType };
|
|
337
490
|
} catch (err: any) {
|
|
@@ -365,7 +518,7 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
|
|
|
365
518
|
const rt = getDingTalkRuntime();
|
|
366
519
|
|
|
367
520
|
// Save logger globally so shared services can log consistently without threading log everywhere.
|
|
368
|
-
setCurrentLogger(log);
|
|
521
|
+
setCurrentLogger(log, accountId);
|
|
369
522
|
|
|
370
523
|
log?.debug?.("[DingTalk] Full Inbound Data: " + JSON.stringify(maskSensitiveData(data)));
|
|
371
524
|
|
|
@@ -628,423 +781,31 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
|
|
|
628
781
|
});
|
|
629
782
|
|
|
630
783
|
const to = isDirect ? senderId : groupId;
|
|
631
|
-
const
|
|
632
|
-
const parsedSessionCommand = parseSessionCommand(extractedContent.text);
|
|
633
|
-
const isOwner = isLearningOwner({
|
|
784
|
+
const commandHandled = await handleInboundCommandDispatch({
|
|
634
785
|
cfg,
|
|
635
|
-
config: dingtalkConfig,
|
|
636
|
-
senderId,
|
|
637
|
-
rawSenderId: data.senderId,
|
|
638
|
-
});
|
|
639
|
-
if (isDirect && parsedLearnCommand.scope === "whoami") {
|
|
640
|
-
await sendBySession(
|
|
641
|
-
dingtalkConfig,
|
|
642
|
-
sessionWebhook,
|
|
643
|
-
formatWhoAmIReply({
|
|
644
|
-
senderId,
|
|
645
|
-
rawSenderId: data.senderId,
|
|
646
|
-
senderStaffId: data.senderStaffId,
|
|
647
|
-
isOwner,
|
|
648
|
-
}),
|
|
649
|
-
{ log },
|
|
650
|
-
);
|
|
651
|
-
return;
|
|
652
|
-
}
|
|
653
|
-
if (parsedLearnCommand.scope === "whereami") {
|
|
654
|
-
await sendBySession(
|
|
655
|
-
dingtalkConfig,
|
|
656
|
-
sessionWebhook,
|
|
657
|
-
formatWhereAmIReply({
|
|
658
|
-
conversationId: data.conversationId,
|
|
659
|
-
conversationType: isDirect ? "dm" : "group",
|
|
660
|
-
peerId: sessionPeer.peerId,
|
|
661
|
-
}),
|
|
662
|
-
{ log },
|
|
663
|
-
);
|
|
664
|
-
return;
|
|
665
|
-
}
|
|
666
|
-
if (isDirect && parsedLearnCommand.scope === "owner-status") {
|
|
667
|
-
await sendBySession(
|
|
668
|
-
dingtalkConfig,
|
|
669
|
-
sessionWebhook,
|
|
670
|
-
formatOwnerStatusReply({
|
|
671
|
-
senderId,
|
|
672
|
-
rawSenderId: data.senderId,
|
|
673
|
-
isOwner,
|
|
674
|
-
}),
|
|
675
|
-
{ log },
|
|
676
|
-
);
|
|
677
|
-
return;
|
|
678
|
-
}
|
|
679
|
-
if (parsedLearnCommand.scope === "help") {
|
|
680
|
-
await sendBySession(dingtalkConfig, sessionWebhook, formatLearnCommandHelp(), { log });
|
|
681
|
-
return;
|
|
682
|
-
}
|
|
683
|
-
if (
|
|
684
|
-
(parsedLearnCommand.scope === "global" ||
|
|
685
|
-
parsedLearnCommand.scope === "session" ||
|
|
686
|
-
parsedLearnCommand.scope === "here" ||
|
|
687
|
-
parsedLearnCommand.scope === "target" ||
|
|
688
|
-
parsedLearnCommand.scope === "targets" ||
|
|
689
|
-
parsedLearnCommand.scope === "list" ||
|
|
690
|
-
parsedLearnCommand.scope === "disable" ||
|
|
691
|
-
parsedLearnCommand.scope === "delete" ||
|
|
692
|
-
parsedLearnCommand.scope === "target-set-create" ||
|
|
693
|
-
parsedLearnCommand.scope === "target-set-apply" ||
|
|
694
|
-
parsedSessionCommand.scope === "session-alias-show" ||
|
|
695
|
-
parsedSessionCommand.scope === "session-alias-set" ||
|
|
696
|
-
parsedSessionCommand.scope === "session-alias-clear" ||
|
|
697
|
-
parsedSessionCommand.scope === "session-alias-bind" ||
|
|
698
|
-
parsedSessionCommand.scope === "session-alias-unbind") &&
|
|
699
|
-
!isOwner
|
|
700
|
-
) {
|
|
701
|
-
await sendBySession(dingtalkConfig, sessionWebhook, formatOwnerOnlyDeniedReply(), { log });
|
|
702
|
-
return;
|
|
703
|
-
}
|
|
704
|
-
if (isOwner) {
|
|
705
|
-
if (parsedSessionCommand.scope === "session-alias-show") {
|
|
706
|
-
await sendBySession(
|
|
707
|
-
dingtalkConfig,
|
|
708
|
-
sessionWebhook,
|
|
709
|
-
formatSessionAliasReply({
|
|
710
|
-
sourceKind: currentSessionSourceKind,
|
|
711
|
-
sourceId: currentSessionSourceId,
|
|
712
|
-
peerId: sessionPeer.peerId,
|
|
713
|
-
aliasSource: peerIdOverride ? "override" : "default",
|
|
714
|
-
}),
|
|
715
|
-
{ log },
|
|
716
|
-
);
|
|
717
|
-
return;
|
|
718
|
-
}
|
|
719
|
-
if (parsedSessionCommand.scope === "session-alias-set" && parsedSessionCommand.peerId) {
|
|
720
|
-
const aliasValidationError = validateSessionAlias(parsedSessionCommand.peerId);
|
|
721
|
-
if (aliasValidationError) {
|
|
722
|
-
await sendBySession(
|
|
723
|
-
dingtalkConfig,
|
|
724
|
-
sessionWebhook,
|
|
725
|
-
formatSessionAliasValidationErrorReply(aliasValidationError),
|
|
726
|
-
{ log },
|
|
727
|
-
);
|
|
728
|
-
return;
|
|
729
|
-
}
|
|
730
|
-
setSessionPeerOverride({
|
|
731
|
-
storePath: accountStorePath,
|
|
732
|
-
accountId,
|
|
733
|
-
sourceKind: currentSessionSourceKind,
|
|
734
|
-
sourceId: currentSessionSourceId,
|
|
735
|
-
peerId: parsedSessionCommand.peerId,
|
|
736
|
-
});
|
|
737
|
-
await sendBySession(
|
|
738
|
-
dingtalkConfig,
|
|
739
|
-
sessionWebhook,
|
|
740
|
-
formatSessionAliasSetReply({
|
|
741
|
-
sourceKind: currentSessionSourceKind,
|
|
742
|
-
sourceId: currentSessionSourceId,
|
|
743
|
-
peerId: parsedSessionCommand.peerId,
|
|
744
|
-
}),
|
|
745
|
-
{ log },
|
|
746
|
-
);
|
|
747
|
-
return;
|
|
748
|
-
}
|
|
749
|
-
if (parsedSessionCommand.scope === "session-alias-clear") {
|
|
750
|
-
clearSessionPeerOverride({
|
|
751
|
-
storePath: accountStorePath,
|
|
752
|
-
accountId,
|
|
753
|
-
sourceKind: currentSessionSourceKind,
|
|
754
|
-
sourceId: currentSessionSourceId,
|
|
755
|
-
});
|
|
756
|
-
await sendBySession(
|
|
757
|
-
dingtalkConfig,
|
|
758
|
-
sessionWebhook,
|
|
759
|
-
formatSessionAliasClearedReply({
|
|
760
|
-
sourceKind: currentSessionSourceKind,
|
|
761
|
-
sourceId: currentSessionSourceId,
|
|
762
|
-
}),
|
|
763
|
-
{ log },
|
|
764
|
-
);
|
|
765
|
-
return;
|
|
766
|
-
}
|
|
767
|
-
if (
|
|
768
|
-
parsedSessionCommand.scope === "session-alias-bind" &&
|
|
769
|
-
parsedSessionCommand.sourceKind &&
|
|
770
|
-
parsedSessionCommand.sourceId &&
|
|
771
|
-
parsedSessionCommand.peerId
|
|
772
|
-
) {
|
|
773
|
-
const aliasValidationError = validateSessionAlias(parsedSessionCommand.peerId);
|
|
774
|
-
if (aliasValidationError) {
|
|
775
|
-
await sendBySession(
|
|
776
|
-
dingtalkConfig,
|
|
777
|
-
sessionWebhook,
|
|
778
|
-
formatSessionAliasValidationErrorReply(aliasValidationError),
|
|
779
|
-
{ log },
|
|
780
|
-
);
|
|
781
|
-
return;
|
|
782
|
-
}
|
|
783
|
-
setSessionPeerOverride({
|
|
784
|
-
storePath: accountStorePath,
|
|
785
|
-
accountId,
|
|
786
|
-
sourceKind: parsedSessionCommand.sourceKind,
|
|
787
|
-
sourceId: parsedSessionCommand.sourceId,
|
|
788
|
-
peerId: parsedSessionCommand.peerId,
|
|
789
|
-
});
|
|
790
|
-
await sendBySession(
|
|
791
|
-
dingtalkConfig,
|
|
792
|
-
sessionWebhook,
|
|
793
|
-
formatSessionAliasBoundReply({
|
|
794
|
-
sourceKind: parsedSessionCommand.sourceKind,
|
|
795
|
-
sourceId: parsedSessionCommand.sourceId,
|
|
796
|
-
peerId: parsedSessionCommand.peerId,
|
|
797
|
-
}),
|
|
798
|
-
{ log },
|
|
799
|
-
);
|
|
800
|
-
return;
|
|
801
|
-
}
|
|
802
|
-
if (
|
|
803
|
-
parsedSessionCommand.scope === "session-alias-unbind" &&
|
|
804
|
-
parsedSessionCommand.sourceKind &&
|
|
805
|
-
parsedSessionCommand.sourceId
|
|
806
|
-
) {
|
|
807
|
-
const existed = clearSessionPeerOverride({
|
|
808
|
-
storePath: accountStorePath,
|
|
809
|
-
accountId,
|
|
810
|
-
sourceKind: parsedSessionCommand.sourceKind,
|
|
811
|
-
sourceId: parsedSessionCommand.sourceId,
|
|
812
|
-
});
|
|
813
|
-
await sendBySession(
|
|
814
|
-
dingtalkConfig,
|
|
815
|
-
sessionWebhook,
|
|
816
|
-
formatSessionAliasUnboundReply({
|
|
817
|
-
sourceKind: parsedSessionCommand.sourceKind,
|
|
818
|
-
sourceId: parsedSessionCommand.sourceId,
|
|
819
|
-
existed,
|
|
820
|
-
}),
|
|
821
|
-
{ log },
|
|
822
|
-
);
|
|
823
|
-
return;
|
|
824
|
-
}
|
|
825
|
-
if (parsedLearnCommand.scope === "global" && parsedLearnCommand.instruction) {
|
|
826
|
-
const applied = applyManualGlobalLearningRule({
|
|
827
|
-
storePath: accountStorePath,
|
|
828
|
-
accountId,
|
|
829
|
-
instruction: parsedLearnCommand.instruction,
|
|
830
|
-
});
|
|
831
|
-
await sendBySession(
|
|
832
|
-
dingtalkConfig,
|
|
833
|
-
sessionWebhook,
|
|
834
|
-
formatLearnAppliedReply({
|
|
835
|
-
scope: "global",
|
|
836
|
-
instruction: parsedLearnCommand.instruction,
|
|
837
|
-
ruleId: applied?.ruleId,
|
|
838
|
-
}),
|
|
839
|
-
{ log },
|
|
840
|
-
);
|
|
841
|
-
return;
|
|
842
|
-
}
|
|
843
|
-
if (parsedLearnCommand.scope === "session" && parsedLearnCommand.instruction) {
|
|
844
|
-
applyManualSessionLearningNote({
|
|
845
|
-
storePath: accountStorePath,
|
|
846
|
-
accountId,
|
|
847
|
-
targetId: data.conversationId,
|
|
848
|
-
instruction: parsedLearnCommand.instruction,
|
|
849
|
-
});
|
|
850
|
-
await sendBySession(
|
|
851
|
-
dingtalkConfig,
|
|
852
|
-
sessionWebhook,
|
|
853
|
-
formatLearnAppliedReply({
|
|
854
|
-
scope: "session",
|
|
855
|
-
instruction: parsedLearnCommand.instruction,
|
|
856
|
-
}),
|
|
857
|
-
{ log },
|
|
858
|
-
);
|
|
859
|
-
return;
|
|
860
|
-
}
|
|
861
|
-
if (parsedLearnCommand.scope === "here" && parsedLearnCommand.instruction) {
|
|
862
|
-
const applied = applyManualTargetLearningRule({
|
|
863
|
-
storePath: accountStorePath,
|
|
864
|
-
accountId,
|
|
865
|
-
targetId: data.conversationId,
|
|
866
|
-
instruction: parsedLearnCommand.instruction,
|
|
867
|
-
});
|
|
868
|
-
await sendBySession(
|
|
869
|
-
dingtalkConfig,
|
|
870
|
-
sessionWebhook,
|
|
871
|
-
formatLearnAppliedReply({
|
|
872
|
-
scope: "target",
|
|
873
|
-
targetId: data.conversationId,
|
|
874
|
-
instruction: parsedLearnCommand.instruction,
|
|
875
|
-
ruleId: applied?.ruleId,
|
|
876
|
-
}),
|
|
877
|
-
{ log },
|
|
878
|
-
);
|
|
879
|
-
return;
|
|
880
|
-
}
|
|
881
|
-
if (
|
|
882
|
-
parsedLearnCommand.scope === "target" &&
|
|
883
|
-
parsedLearnCommand.targetId &&
|
|
884
|
-
parsedLearnCommand.instruction
|
|
885
|
-
) {
|
|
886
|
-
const applied = applyManualTargetLearningRule({
|
|
887
|
-
storePath: accountStorePath,
|
|
888
|
-
accountId,
|
|
889
|
-
targetId: parsedLearnCommand.targetId,
|
|
890
|
-
instruction: parsedLearnCommand.instruction,
|
|
891
|
-
});
|
|
892
|
-
await sendBySession(
|
|
893
|
-
dingtalkConfig,
|
|
894
|
-
sessionWebhook,
|
|
895
|
-
formatLearnAppliedReply({
|
|
896
|
-
scope: "target",
|
|
897
|
-
targetId: parsedLearnCommand.targetId,
|
|
898
|
-
instruction: parsedLearnCommand.instruction,
|
|
899
|
-
ruleId: applied?.ruleId,
|
|
900
|
-
}),
|
|
901
|
-
{ log },
|
|
902
|
-
);
|
|
903
|
-
return;
|
|
904
|
-
}
|
|
905
|
-
if (
|
|
906
|
-
parsedLearnCommand.scope === "targets" &&
|
|
907
|
-
parsedLearnCommand.targetIds?.length &&
|
|
908
|
-
parsedLearnCommand.instruction
|
|
909
|
-
) {
|
|
910
|
-
const applied = applyManualTargetsLearningRule({
|
|
911
|
-
storePath: accountStorePath,
|
|
912
|
-
accountId,
|
|
913
|
-
targetIds: parsedLearnCommand.targetIds,
|
|
914
|
-
instruction: parsedLearnCommand.instruction,
|
|
915
|
-
});
|
|
916
|
-
await sendBySession(
|
|
917
|
-
dingtalkConfig,
|
|
918
|
-
sessionWebhook,
|
|
919
|
-
formatLearnAppliedReply({
|
|
920
|
-
scope: "targets",
|
|
921
|
-
targetIds: parsedLearnCommand.targetIds,
|
|
922
|
-
instruction: parsedLearnCommand.instruction,
|
|
923
|
-
ruleId: applied[0]?.ruleId,
|
|
924
|
-
}),
|
|
925
|
-
{ log },
|
|
926
|
-
);
|
|
927
|
-
return;
|
|
928
|
-
}
|
|
929
|
-
if (
|
|
930
|
-
parsedLearnCommand.scope === "target-set-create" &&
|
|
931
|
-
parsedLearnCommand.setName &&
|
|
932
|
-
parsedLearnCommand.targetIds?.length
|
|
933
|
-
) {
|
|
934
|
-
const saved = createOrUpdateTargetSet({
|
|
935
|
-
storePath: accountStorePath,
|
|
936
|
-
accountId,
|
|
937
|
-
name: parsedLearnCommand.setName,
|
|
938
|
-
targetIds: parsedLearnCommand.targetIds,
|
|
939
|
-
});
|
|
940
|
-
await sendBySession(
|
|
941
|
-
dingtalkConfig,
|
|
942
|
-
sessionWebhook,
|
|
943
|
-
saved
|
|
944
|
-
? formatTargetSetSavedReply({
|
|
945
|
-
setName: parsedLearnCommand.setName,
|
|
946
|
-
targetIds: parsedLearnCommand.targetIds,
|
|
947
|
-
})
|
|
948
|
-
: "目标组保存失败,请检查名称和目标列表。",
|
|
949
|
-
{ log },
|
|
950
|
-
);
|
|
951
|
-
return;
|
|
952
|
-
}
|
|
953
|
-
if (
|
|
954
|
-
parsedLearnCommand.scope === "target-set-apply" &&
|
|
955
|
-
parsedLearnCommand.setName &&
|
|
956
|
-
parsedLearnCommand.instruction
|
|
957
|
-
) {
|
|
958
|
-
const applied = applyTargetSetLearningRule({
|
|
959
|
-
storePath: accountStorePath,
|
|
960
|
-
accountId,
|
|
961
|
-
name: parsedLearnCommand.setName,
|
|
962
|
-
instruction: parsedLearnCommand.instruction,
|
|
963
|
-
});
|
|
964
|
-
await sendBySession(
|
|
965
|
-
dingtalkConfig,
|
|
966
|
-
sessionWebhook,
|
|
967
|
-
applied.length > 0
|
|
968
|
-
? formatLearnAppliedReply({
|
|
969
|
-
scope: "target-set",
|
|
970
|
-
setName: parsedLearnCommand.setName,
|
|
971
|
-
targetIds: applied.map((item) => item.targetId),
|
|
972
|
-
instruction: parsedLearnCommand.instruction,
|
|
973
|
-
ruleId: applied[0]?.ruleId,
|
|
974
|
-
})
|
|
975
|
-
: `未找到目标组 \`${parsedLearnCommand.setName}\`,或该目标组为空。`,
|
|
976
|
-
{ log },
|
|
977
|
-
);
|
|
978
|
-
return;
|
|
979
|
-
}
|
|
980
|
-
if (parsedLearnCommand.scope === "list") {
|
|
981
|
-
const rules = listScopedLearningRules({ storePath: accountStorePath, accountId })
|
|
982
|
-
.slice(0, 20)
|
|
983
|
-
.map((rule) => {
|
|
984
|
-
const scope = rule.scope === "target" ? `target(${rule.targetId})` : "global";
|
|
985
|
-
const status = rule.enabled ? "enabled" : "disabled";
|
|
986
|
-
return `- [${scope}] ${rule.ruleId} (${status}) => ${rule.instruction}`;
|
|
987
|
-
});
|
|
988
|
-
const targetSets = listLearningTargetSets({ storePath: accountStorePath, accountId })
|
|
989
|
-
.slice(0, 10)
|
|
990
|
-
.map(
|
|
991
|
-
(targetSet) => `- [target-set] ${targetSet.name} => ${targetSet.targetIds.join(", ")}`,
|
|
992
|
-
);
|
|
993
|
-
await sendBySession(
|
|
994
|
-
dingtalkConfig,
|
|
995
|
-
sessionWebhook,
|
|
996
|
-
formatLearnListReply([...rules, ...targetSets]),
|
|
997
|
-
{ log },
|
|
998
|
-
);
|
|
999
|
-
return;
|
|
1000
|
-
}
|
|
1001
|
-
if (parsedLearnCommand.scope === "disable" && parsedLearnCommand.ruleId) {
|
|
1002
|
-
const result = disableManualRule({
|
|
1003
|
-
storePath: accountStorePath,
|
|
1004
|
-
accountId,
|
|
1005
|
-
ruleId: parsedLearnCommand.ruleId,
|
|
1006
|
-
});
|
|
1007
|
-
await sendBySession(
|
|
1008
|
-
dingtalkConfig,
|
|
1009
|
-
sessionWebhook,
|
|
1010
|
-
formatLearnDisabledReply({
|
|
1011
|
-
ruleId: parsedLearnCommand.ruleId,
|
|
1012
|
-
existed: result.existed,
|
|
1013
|
-
scope: result.scope,
|
|
1014
|
-
targetId: result.targetId,
|
|
1015
|
-
}),
|
|
1016
|
-
{ log },
|
|
1017
|
-
);
|
|
1018
|
-
return;
|
|
1019
|
-
}
|
|
1020
|
-
if (parsedLearnCommand.scope === "delete" && parsedLearnCommand.ruleId) {
|
|
1021
|
-
const result = deleteManualRule({
|
|
1022
|
-
storePath: accountStorePath,
|
|
1023
|
-
accountId,
|
|
1024
|
-
ruleId: parsedLearnCommand.ruleId,
|
|
1025
|
-
});
|
|
1026
|
-
await sendBySession(
|
|
1027
|
-
dingtalkConfig,
|
|
1028
|
-
sessionWebhook,
|
|
1029
|
-
formatLearnDeletedReply({
|
|
1030
|
-
ruleId: parsedLearnCommand.ruleId,
|
|
1031
|
-
existed: result.existed,
|
|
1032
|
-
scope: result.scope,
|
|
1033
|
-
targetId: result.targetId,
|
|
1034
|
-
}),
|
|
1035
|
-
{ log },
|
|
1036
|
-
);
|
|
1037
|
-
return;
|
|
1038
|
-
}
|
|
1039
|
-
}
|
|
1040
|
-
const manualForcedReply = resolveManualForcedReply({
|
|
1041
|
-
storePath: accountStorePath,
|
|
1042
786
|
accountId,
|
|
1043
|
-
|
|
1044
|
-
|
|
787
|
+
dingtalkConfig,
|
|
788
|
+
senderId,
|
|
789
|
+
isDirect,
|
|
790
|
+
extractedText: extractedContent.text,
|
|
791
|
+
messageType: extractedContent.messageType,
|
|
792
|
+
data: {
|
|
793
|
+
conversationId: data.conversationId,
|
|
794
|
+
senderId: data.senderId,
|
|
795
|
+
senderStaffId: data.senderStaffId,
|
|
796
|
+
},
|
|
797
|
+
accountStorePath,
|
|
798
|
+
currentSessionSourceKind,
|
|
799
|
+
currentSessionSourceId,
|
|
800
|
+
peerIdOverride,
|
|
801
|
+
sessionPeer,
|
|
802
|
+
sendReply: async (text: string) => {
|
|
803
|
+
await sendBySession(dingtalkConfig, sessionWebhook, text, { log });
|
|
804
|
+
},
|
|
805
|
+
clearSessionPeerOverride,
|
|
806
|
+
setSessionPeerOverride,
|
|
1045
807
|
});
|
|
1046
|
-
if (
|
|
1047
|
-
await sendBySession(dingtalkConfig, sessionWebhook, manualForcedReply, { log });
|
|
808
|
+
if (commandHandled) {
|
|
1048
809
|
return;
|
|
1049
810
|
}
|
|
1050
811
|
// 3) Select response mode (card vs markdown).
|
|
@@ -1129,6 +890,8 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
|
|
|
1129
890
|
messageType: content.messageType,
|
|
1130
891
|
text: content.text,
|
|
1131
892
|
quotedRef,
|
|
893
|
+
senderId,
|
|
894
|
+
senderName,
|
|
1132
895
|
createdAt: data.createAt,
|
|
1133
896
|
ttlMs: ttlDaysToMs(journalTTLDays),
|
|
1134
897
|
ttlReferenceMs: data.createAt,
|
|
@@ -1153,7 +916,12 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
|
|
|
1153
916
|
mediaType = preDownloadedMedia.mediaType;
|
|
1154
917
|
} else if (content.mediaPath && robotCode) {
|
|
1155
918
|
// Download media only if not pre-downloaded
|
|
1156
|
-
const media = await downloadMedia(
|
|
919
|
+
const media = await downloadMedia(
|
|
920
|
+
dingtalkConfig,
|
|
921
|
+
content.mediaPath,
|
|
922
|
+
log,
|
|
923
|
+
attachmentContextFileName,
|
|
924
|
+
);
|
|
1157
925
|
if (media) {
|
|
1158
926
|
mediaPath = media.path;
|
|
1159
927
|
mediaType = media.mimeType;
|
|
@@ -1174,6 +942,7 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
|
|
|
1174
942
|
spaceId: data.content?.spaceId,
|
|
1175
943
|
fileId: data.content?.fileId,
|
|
1176
944
|
},
|
|
945
|
+
attachmentFileName: attachmentContextFileName,
|
|
1177
946
|
ttlMs: DEFAULT_MEDIA_CONTEXT_TTL_MS,
|
|
1178
947
|
topic: null,
|
|
1179
948
|
});
|
|
@@ -1198,6 +967,7 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
|
|
|
1198
967
|
spaceId: content.docSpaceId,
|
|
1199
968
|
fileId: content.docFileId,
|
|
1200
969
|
},
|
|
970
|
+
attachmentFileName: attachmentContextFileName,
|
|
1201
971
|
ttlMs: DEFAULT_MEDIA_CONTEXT_TTL_MS,
|
|
1202
972
|
topic: null,
|
|
1203
973
|
});
|
|
@@ -1211,6 +981,7 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
|
|
|
1211
981
|
content.docFileId,
|
|
1212
982
|
unionId,
|
|
1213
983
|
log,
|
|
984
|
+
attachmentContextFileName,
|
|
1214
985
|
);
|
|
1215
986
|
if (docMedia) {
|
|
1216
987
|
mediaPath = docMedia.path;
|
|
@@ -1229,22 +1000,30 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
|
|
|
1229
1000
|
quotedRef,
|
|
1230
1001
|
log,
|
|
1231
1002
|
});
|
|
1232
|
-
const quotedRuntimeContext =
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1003
|
+
const quotedRuntimeContext = filterQuotedRuntimeContext({
|
|
1004
|
+
context: resolveQuotedRuntimeContext({
|
|
1005
|
+
storePath: accountStorePath,
|
|
1006
|
+
accountId,
|
|
1007
|
+
conversationId: data.conversationId,
|
|
1008
|
+
quotedRef,
|
|
1009
|
+
firstRecord: quotedRecord,
|
|
1010
|
+
firstPreview:
|
|
1011
|
+
content.quoted?.previewText ||
|
|
1012
|
+
content.quoted?.previewMessageType
|
|
1013
|
+
? {
|
|
1014
|
+
text: content.quoted.previewText,
|
|
1015
|
+
messageType: content.quoted.previewMessageType,
|
|
1016
|
+
senderId: content.quoted.previewSenderId,
|
|
1017
|
+
}
|
|
1018
|
+
: undefined,
|
|
1019
|
+
log,
|
|
1020
|
+
}),
|
|
1021
|
+
config: dingtalkConfig,
|
|
1022
|
+
isDirect,
|
|
1023
|
+
groupId,
|
|
1024
|
+
quotedSenderId: quotedRecord?.senderId || content.quoted?.previewSenderId,
|
|
1025
|
+
currentSenderId: senderId,
|
|
1026
|
+
currentSenderOriginalId: senderOriginalId,
|
|
1248
1027
|
});
|
|
1249
1028
|
|
|
1250
1029
|
// Try downloading a quoted file from cached downloadCode/spaceId+fileId.
|
|
@@ -1257,13 +1036,14 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
|
|
|
1257
1036
|
fileId?: string;
|
|
1258
1037
|
};
|
|
1259
1038
|
} | null,
|
|
1039
|
+
originalFilename?: string,
|
|
1260
1040
|
): Promise<MediaFile | null> => {
|
|
1261
1041
|
if (!record?.media) {
|
|
1262
1042
|
return null;
|
|
1263
1043
|
}
|
|
1264
1044
|
let media: MediaFile | null = null;
|
|
1265
1045
|
if (record.media.downloadCode) {
|
|
1266
|
-
media = await downloadMedia(dingtalkConfig, record.media.downloadCode, log);
|
|
1046
|
+
media = await downloadMedia(dingtalkConfig, record.media.downloadCode, log, originalFilename);
|
|
1267
1047
|
if (media) {
|
|
1268
1048
|
log?.debug?.(
|
|
1269
1049
|
`[DingTalk][QuotedRef] Recovered quoted media from cached downloadCode ` +
|
|
@@ -1280,6 +1060,7 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
|
|
|
1280
1060
|
record.media.fileId,
|
|
1281
1061
|
unionId,
|
|
1282
1062
|
log,
|
|
1063
|
+
originalFilename,
|
|
1283
1064
|
);
|
|
1284
1065
|
if (media) {
|
|
1285
1066
|
log?.debug?.(
|
|
@@ -1296,9 +1077,15 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
|
|
|
1296
1077
|
|
|
1297
1078
|
// Quoted picture: download via existing downloadMedia.
|
|
1298
1079
|
if (!mediaPath && content.quoted?.mediaDownloadCode && robotCode) {
|
|
1080
|
+
const quotedOriginalFilename = content.quoted.previewFileName;
|
|
1299
1081
|
const media =
|
|
1300
|
-
(await tryDownloadFromRecord(quotedRecord)) ||
|
|
1301
|
-
(await downloadMedia(
|
|
1082
|
+
(await tryDownloadFromRecord(quotedRecord, quotedOriginalFilename)) ||
|
|
1083
|
+
(await downloadMedia(
|
|
1084
|
+
dingtalkConfig,
|
|
1085
|
+
content.quoted.mediaDownloadCode,
|
|
1086
|
+
log,
|
|
1087
|
+
quotedOriginalFilename,
|
|
1088
|
+
));
|
|
1302
1089
|
if (media) {
|
|
1303
1090
|
if (!quotedRecord) {
|
|
1304
1091
|
log?.debug?.(
|
|
@@ -1323,7 +1110,12 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
|
|
|
1323
1110
|
|
|
1324
1111
|
// Step 0: Direct download via downloadCode from quoted payload (file/audio/video msgType).
|
|
1325
1112
|
if (!fileResolved && content.quoted.fileDownloadCode && robotCode) {
|
|
1326
|
-
const media = await downloadMedia(
|
|
1113
|
+
const media = await downloadMedia(
|
|
1114
|
+
dingtalkConfig,
|
|
1115
|
+
content.quoted.fileDownloadCode,
|
|
1116
|
+
log,
|
|
1117
|
+
content.quoted.previewFileName,
|
|
1118
|
+
);
|
|
1327
1119
|
if (media) {
|
|
1328
1120
|
mediaPath = media.path;
|
|
1329
1121
|
mediaType = media.mimeType;
|
|
@@ -1340,7 +1132,10 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
|
|
|
1340
1132
|
|
|
1341
1133
|
// Step 1: Prefer quotedRef-backed record lookup, then msgId-based cache.
|
|
1342
1134
|
if (!fileResolved) {
|
|
1343
|
-
const cachedMedia = await tryDownloadFromRecord(
|
|
1135
|
+
const cachedMedia = await tryDownloadFromRecord(
|
|
1136
|
+
quotedRecord,
|
|
1137
|
+
quotedRecord?.attachmentFileName || content.quoted.previewFileName,
|
|
1138
|
+
);
|
|
1344
1139
|
if (cachedMedia) {
|
|
1345
1140
|
mediaPath = cachedMedia.path;
|
|
1346
1141
|
mediaType = cachedMedia.mimeType;
|
|
@@ -1414,7 +1209,10 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
|
|
|
1414
1209
|
if (!mediaPath && content.quoted?.isQuotedDocCard) {
|
|
1415
1210
|
let docResolved = false;
|
|
1416
1211
|
|
|
1417
|
-
const cachedDocMedia = await tryDownloadFromRecord(
|
|
1212
|
+
const cachedDocMedia = await tryDownloadFromRecord(
|
|
1213
|
+
quotedRecord,
|
|
1214
|
+
quotedRecord?.attachmentFileName || content.quoted?.previewFileName,
|
|
1215
|
+
);
|
|
1418
1216
|
if (cachedDocMedia) {
|
|
1419
1217
|
mediaPath = cachedDocMedia.path;
|
|
1420
1218
|
mediaType = cachedDocMedia.mimeType;
|
|
@@ -1634,13 +1432,11 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
|
|
|
1634
1432
|
// tryFastAbortFromMessage (inside the SDK) kill any in-flight generation immediately,
|
|
1635
1433
|
// rather than waiting for it to finish before the stop message is processed.
|
|
1636
1434
|
//
|
|
1637
|
-
//
|
|
1638
|
-
//
|
|
1639
|
-
//
|
|
1640
|
-
// in
|
|
1641
|
-
const textForAbortCheck =
|
|
1642
|
-
? inboundText.replace(/^(?:@\S+\s+)*/u, "").trim()
|
|
1643
|
-
: inboundText;
|
|
1435
|
+
// Strip leading @mention tokens before the abort check so that messages like
|
|
1436
|
+
// "@Agent /stop" are correctly recognised as abort requests in both DM and group
|
|
1437
|
+
// chats. In groups DingTalk usually strips @BotName at the protocol level, but
|
|
1438
|
+
// in DMs with multi-agent routing the @mention prefix survives all the way here.
|
|
1439
|
+
const textForAbortCheck = inboundText.replace(/^(?:@\S+\s+)*/u, "").trim();
|
|
1644
1440
|
if (isAbortRequestText(textForAbortCheck)) {
|
|
1645
1441
|
log?.info?.(
|
|
1646
1442
|
`[DingTalk] Abort request detected, bypassing session lock for session=${route.sessionKey}`,
|
|
@@ -1740,8 +1536,117 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
|
|
|
1740
1536
|
}
|
|
1741
1537
|
}
|
|
1742
1538
|
|
|
1539
|
+
function parseInlineReplyPayloadText(text: unknown): {
|
|
1540
|
+
text?: string;
|
|
1541
|
+
mediaUrls: string[];
|
|
1542
|
+
audioAsVoice: boolean;
|
|
1543
|
+
} {
|
|
1544
|
+
if (typeof text !== "string") {
|
|
1545
|
+
return { text: undefined, mediaUrls: [], audioAsVoice: false };
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
const normalizedText = text.replace(/\r\n/g, "\n");
|
|
1549
|
+
const parsedInline = normalizedText.includes("[[")
|
|
1550
|
+
? parseInlineDirectives(normalizedText, {
|
|
1551
|
+
stripAudioTag: true,
|
|
1552
|
+
stripReplyTags: false,
|
|
1553
|
+
})
|
|
1554
|
+
: {
|
|
1555
|
+
text: normalizedText,
|
|
1556
|
+
audioAsVoice: false,
|
|
1557
|
+
replyToCurrent: false,
|
|
1558
|
+
hasAudioTag: false,
|
|
1559
|
+
hasReplyTag: false,
|
|
1560
|
+
};
|
|
1561
|
+
const mediaUrls: string[] = [];
|
|
1562
|
+
const contentLines: string[] = [];
|
|
1563
|
+
|
|
1564
|
+
for (const line of parsedInline.text.split("\n")) {
|
|
1565
|
+
const trimmed = line.trim();
|
|
1566
|
+
const mediaCandidate = trimmed.replace(/^(?:\[\[[^[\]]+\]\]\s*)+/, "");
|
|
1567
|
+
if (mediaCandidate.startsWith(MEDIA_DIRECTIVE_PREFIX)) {
|
|
1568
|
+
const mediaSource = mediaCandidate.slice(MEDIA_DIRECTIVE_PREFIX.length).trim();
|
|
1569
|
+
if (mediaSource) {
|
|
1570
|
+
mediaUrls.push(mediaSource);
|
|
1571
|
+
continue;
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
contentLines.push(line);
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
const cleanedText = contentLines.join("\n").trim();
|
|
1578
|
+
const inlineTextWasTransformed = parsedInline.text !== normalizedText;
|
|
1579
|
+
if (mediaUrls.length === 0) {
|
|
1580
|
+
const standaloneMediaSource = extractStandaloneMediaSource(cleanedText);
|
|
1581
|
+
if (standaloneMediaSource) {
|
|
1582
|
+
return {
|
|
1583
|
+
text: undefined,
|
|
1584
|
+
mediaUrls: [standaloneMediaSource],
|
|
1585
|
+
audioAsVoice: parsedInline.audioAsVoice,
|
|
1586
|
+
};
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
return {
|
|
1591
|
+
// Keep ordinary text formatting stable except for newline normalization.
|
|
1592
|
+
// Once inline parsing actually strips directives/media lines, return the
|
|
1593
|
+
// cleaned body text instead of the original raw payload.
|
|
1594
|
+
text: mediaUrls.length > 0 || inlineTextWasTransformed ? cleanedText || undefined : normalizedText,
|
|
1595
|
+
mediaUrls,
|
|
1596
|
+
audioAsVoice: parsedInline.audioAsVoice,
|
|
1597
|
+
};
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
function extractStandaloneMediaSource(text: string): string | undefined {
|
|
1601
|
+
const trimmed = text.trim();
|
|
1602
|
+
if (!trimmed || trimmed.includes("\n") || /\s/.test(trimmed)) {
|
|
1603
|
+
return undefined;
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
const hasPathLikeShape =
|
|
1607
|
+
/^https?:\/\/\S+$/i.test(trimmed) ||
|
|
1608
|
+
trimmed.startsWith("~/") ||
|
|
1609
|
+
trimmed.startsWith("~\\") ||
|
|
1610
|
+
trimmed.startsWith("./") ||
|
|
1611
|
+
trimmed.startsWith(".\\") ||
|
|
1612
|
+
trimmed.startsWith("../") ||
|
|
1613
|
+
trimmed.startsWith("..\\") ||
|
|
1614
|
+
trimmed.startsWith("/") ||
|
|
1615
|
+
trimmed.startsWith("\\") ||
|
|
1616
|
+
/^[a-zA-Z]:[\\/]/.test(trimmed) ||
|
|
1617
|
+
trimmed.includes("/") ||
|
|
1618
|
+
trimmed.includes("\\");
|
|
1619
|
+
if (!hasPathLikeShape) {
|
|
1620
|
+
return undefined;
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
return STANDALONE_MEDIA_PATH_EXTENSIONS.has(path.extname(trimmed).toLowerCase())
|
|
1624
|
+
? trimmed
|
|
1625
|
+
: undefined;
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1743
1628
|
// ---- Shared media delivery helper ----
|
|
1744
|
-
|
|
1629
|
+
function extractSharedAudioAsVoice(
|
|
1630
|
+
payload: ReplyStreamPayload,
|
|
1631
|
+
inlineReplyPayload?: ReturnType<typeof parseInlineReplyPayloadText>,
|
|
1632
|
+
): boolean {
|
|
1633
|
+
// Normalize all reply-time voice hints into the shared `audioAsVoice`
|
|
1634
|
+
// semantic that strategies and media delivery use downstream.
|
|
1635
|
+
const richPayload = payload as ReplyStreamPayload & {
|
|
1636
|
+
audioAsVoice?: unknown;
|
|
1637
|
+
asVoice?: unknown;
|
|
1638
|
+
};
|
|
1639
|
+
const sharedValue = parseBooleanLike(richPayload.audioAsVoice);
|
|
1640
|
+
if (sharedValue !== undefined) {
|
|
1641
|
+
return sharedValue;
|
|
1642
|
+
}
|
|
1643
|
+
if (parseBooleanLike(richPayload.asVoice) === true) {
|
|
1644
|
+
return true;
|
|
1645
|
+
}
|
|
1646
|
+
return inlineReplyPayload?.audioAsVoice === true;
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
async function deliverMediaAttachments(urls: string[], options?: { audioAsVoice?: boolean }) {
|
|
1745
1650
|
for (const rawMediaUrl of urls) {
|
|
1746
1651
|
const preparedMedia = await prepareMediaInput(
|
|
1747
1652
|
rawMediaUrl,
|
|
@@ -1749,10 +1654,12 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
|
|
|
1749
1654
|
dingtalkConfig.mediaUrlAllowlist,
|
|
1750
1655
|
);
|
|
1751
1656
|
try {
|
|
1752
|
-
const actualMediaPath = preparedMedia.
|
|
1657
|
+
const actualMediaPath = preparedMedia.cleanup
|
|
1658
|
+
? preparedMedia.path
|
|
1659
|
+
: resolveRelativePath(preparedMedia.path);
|
|
1753
1660
|
const outMediaType = resolveOutboundMediaType({
|
|
1754
1661
|
mediaPath: actualMediaPath,
|
|
1755
|
-
asVoice:
|
|
1662
|
+
asVoice: options?.audioAsVoice === true,
|
|
1756
1663
|
});
|
|
1757
1664
|
if (sessionWebhook) {
|
|
1758
1665
|
const sendResult = await sendMessage(dingtalkConfig, to, "", {
|
|
@@ -1793,18 +1700,24 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
|
|
|
1793
1700
|
}
|
|
1794
1701
|
|
|
1795
1702
|
// ---- Extract mediaUrls from runtime payload ----
|
|
1796
|
-
function extractMediaUrls(
|
|
1703
|
+
function extractMediaUrls(
|
|
1704
|
+
payload: ReplyStreamPayload,
|
|
1705
|
+
inlineReplyPayload?: ReturnType<typeof parseInlineReplyPayloadText>,
|
|
1706
|
+
): string[] {
|
|
1797
1707
|
const richPayload = payload as typeof payload & {
|
|
1798
1708
|
mediaUrl?: string;
|
|
1799
1709
|
mediaUrls?: string[];
|
|
1800
1710
|
};
|
|
1801
|
-
|
|
1711
|
+
const explicitMediaUrls = Array.isArray(richPayload.mediaUrls)
|
|
1802
1712
|
? richPayload.mediaUrls.filter((entry: unknown) => typeof entry === "string" && entry.trim())
|
|
1803
1713
|
: richPayload.mediaUrl &&
|
|
1804
1714
|
typeof richPayload.mediaUrl === "string" &&
|
|
1805
1715
|
richPayload.mediaUrl.trim()
|
|
1806
1716
|
? [richPayload.mediaUrl]
|
|
1807
1717
|
: [];
|
|
1718
|
+
return explicitMediaUrls.length > 0
|
|
1719
|
+
? explicitMediaUrls
|
|
1720
|
+
: inlineReplyPayload?.mediaUrls ?? [];
|
|
1808
1721
|
}
|
|
1809
1722
|
|
|
1810
1723
|
// Serialize dispatchReply + card finalize per session to prevent the runtime
|
|
@@ -1856,16 +1769,39 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
|
|
|
1856
1769
|
}
|
|
1857
1770
|
|
|
1858
1771
|
// ---- Create reply strategy (card or markdown) ----
|
|
1772
|
+
const replyMode: ReplyMode = useCardMode && !!currentAICard ? "card" : "markdown";
|
|
1773
|
+
const sessionReasoningLevel = readSessionReasoningLevel({
|
|
1774
|
+
storePath,
|
|
1775
|
+
sessionKey: route.sessionKey,
|
|
1776
|
+
sessionUpdatedAt: previousTimestamp,
|
|
1777
|
+
log,
|
|
1778
|
+
});
|
|
1779
|
+
const legacyCardStreamReasoning = resolveLegacyCardStreamReasoningForInternalUse({
|
|
1780
|
+
cfg,
|
|
1781
|
+
accountId,
|
|
1782
|
+
});
|
|
1783
|
+
const strategyConfig =
|
|
1784
|
+
legacyCardStreamReasoning === undefined
|
|
1785
|
+
? dingtalkConfig
|
|
1786
|
+
: { ...dingtalkConfig, cardStreamReasoning: legacyCardStreamReasoning };
|
|
1859
1787
|
const strategy = createReplyStrategy({
|
|
1860
|
-
config:
|
|
1788
|
+
config: strategyConfig,
|
|
1861
1789
|
card: currentAICard,
|
|
1862
|
-
useCardMode:
|
|
1790
|
+
useCardMode: replyMode === "card",
|
|
1863
1791
|
to,
|
|
1864
1792
|
sessionWebhook,
|
|
1865
1793
|
senderId,
|
|
1866
1794
|
isDirect,
|
|
1867
1795
|
accountId,
|
|
1868
1796
|
storePath: accountStorePath,
|
|
1797
|
+
sessionKey: route.sessionKey,
|
|
1798
|
+
sessionAgentId: route.agentId,
|
|
1799
|
+
disableBlockStreaming: shouldDisableBlockStreamingForReplyMode({
|
|
1800
|
+
replyMode,
|
|
1801
|
+
sessionKey: route.sessionKey,
|
|
1802
|
+
reasoningLevel: sessionReasoningLevel,
|
|
1803
|
+
log,
|
|
1804
|
+
}),
|
|
1869
1805
|
groupId,
|
|
1870
1806
|
log,
|
|
1871
1807
|
replyQuotedRef,
|
|
@@ -1874,7 +1810,8 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
|
|
|
1874
1810
|
});
|
|
1875
1811
|
|
|
1876
1812
|
try {
|
|
1877
|
-
|
|
1813
|
+
let deliveredFinalCount = 0;
|
|
1814
|
+
const dispatchResult = await rt.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
1878
1815
|
ctx,
|
|
1879
1816
|
cfg,
|
|
1880
1817
|
dispatcherOptions: {
|
|
@@ -1883,13 +1820,20 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
|
|
|
1883
1820
|
if (isCurrentCardStopRequested()) {
|
|
1884
1821
|
log?.debug?.("[DingTalk][CardStop] Ignoring reply delivery because stop was already requested");
|
|
1885
1822
|
return;
|
|
1886
|
-
|
|
1823
|
+
}
|
|
1887
1824
|
try {
|
|
1888
|
-
|
|
1825
|
+
if (info?.kind === "final") {
|
|
1826
|
+
deliveredFinalCount += 1;
|
|
1827
|
+
}
|
|
1828
|
+
const inlineReplyPayload = parseInlineReplyPayloadText(payload.text);
|
|
1829
|
+
const mediaUrls = extractMediaUrls(payload, inlineReplyPayload);
|
|
1830
|
+
const richPayload = payload as ReplyStreamPayload & { isReasoning?: boolean };
|
|
1889
1831
|
await strategy.deliver({
|
|
1890
|
-
text:
|
|
1832
|
+
text: inlineReplyPayload.text,
|
|
1891
1833
|
mediaUrls,
|
|
1834
|
+
audioAsVoice: extractSharedAudioAsVoice(payload, inlineReplyPayload),
|
|
1892
1835
|
kind: (info?.kind as DeliverPayload["kind"]) || "block",
|
|
1836
|
+
isReasoning: richPayload.isReasoning === true,
|
|
1893
1837
|
});
|
|
1894
1838
|
} catch (err: unknown) {
|
|
1895
1839
|
log?.error?.(`[DingTalk] Reply failed: ${getErrorMessage(err)}`);
|
|
@@ -1903,6 +1847,36 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
|
|
|
1903
1847
|
},
|
|
1904
1848
|
replyOptions: strategy.getReplyOptions(),
|
|
1905
1849
|
});
|
|
1850
|
+
|
|
1851
|
+
const bufferedFinal =
|
|
1852
|
+
dispatchResult && typeof dispatchResult === "object" && "queuedFinal" in dispatchResult
|
|
1853
|
+
? (dispatchResult as { queuedFinal?: unknown }).queuedFinal
|
|
1854
|
+
: undefined;
|
|
1855
|
+
const finalCount =
|
|
1856
|
+
dispatchResult && typeof dispatchResult === "object" && "counts" in dispatchResult
|
|
1857
|
+
? (dispatchResult as { counts?: { final?: unknown } }).counts?.final
|
|
1858
|
+
: undefined;
|
|
1859
|
+
|
|
1860
|
+
log?.info?.(
|
|
1861
|
+
`[DingTalk][Dispatch] completed — deliveredFinalCount=${deliveredFinalCount} ` +
|
|
1862
|
+
`counts.final=${typeof finalCount === "number" ? finalCount : "n/a"} ` +
|
|
1863
|
+
`queuedFinalType=${typeof bufferedFinal}`,
|
|
1864
|
+
);
|
|
1865
|
+
|
|
1866
|
+
if (deliveredFinalCount === 0 && typeof bufferedFinal === "string" && bufferedFinal.trim()) {
|
|
1867
|
+
const inlineReplyPayload = parseInlineReplyPayloadText(bufferedFinal);
|
|
1868
|
+
const richBufferedPayload = {
|
|
1869
|
+
text: bufferedFinal,
|
|
1870
|
+
mediaUrls: [],
|
|
1871
|
+
} as ReplyStreamPayload;
|
|
1872
|
+
await strategy.deliver({
|
|
1873
|
+
text: inlineReplyPayload.text,
|
|
1874
|
+
mediaUrls: extractMediaUrls(richBufferedPayload, inlineReplyPayload),
|
|
1875
|
+
audioAsVoice: extractSharedAudioAsVoice(richBufferedPayload, inlineReplyPayload),
|
|
1876
|
+
kind: "final",
|
|
1877
|
+
isReasoning: false,
|
|
1878
|
+
});
|
|
1879
|
+
}
|
|
1906
1880
|
} catch (dispatchErr: unknown) {
|
|
1907
1881
|
const error = dispatchErr instanceof Error ? dispatchErr : new Error(getErrorMessage(dispatchErr));
|
|
1908
1882
|
await strategy.abort(error);
|