@soimy/dingtalk 3.5.1 → 3.5.2
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 +7 -1
- package/package.json +8 -3
- 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/reasoning-block-assembler.ts +157 -0
- package/src/card-callback-service.ts +1 -1
- package/src/card-draft-controller.ts +32 -0
- package/src/card-service.ts +1 -1
- package/src/channel.ts +71 -55
- package/src/command/card-stop-command.ts +4 -22
- package/src/command/inbound-command-dispatch-service.ts +464 -0
- package/src/docs-service.ts +5 -5
- package/src/http-client.ts +20 -0
- package/src/inbound-handler.ts +120 -463
- package/src/logger-context.ts +16 -2
- package/src/media-utils.ts +3 -3
- package/src/{attachment-text-extractor.ts → messaging/attachment-text-extractor.ts} +1 -1
- package/src/{quoted-file-service.ts → messaging/quoted-file-service.ts} +5 -5
- package/src/reply-strategy-card.ts +64 -13
- package/src/reply-strategy-markdown.ts +123 -18
- package/src/reply-strategy.ts +4 -0
- package/src/send-service.ts +68 -3
- package/src/targeting/agent-routing.ts +11 -4
- package/src/{group-members-store.ts → targeting/group-members-store.ts} +1 -1
- package/src/utils.ts +165 -0
package/src/inbound-handler.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import axios from "./http-client";
|
|
2
3
|
import { normalizeAllowFrom, isSenderAllowed, resolveGroupAccess } from "./access-control";
|
|
3
4
|
import { buildAgentSessionKey, resolveSubAgentRoute, dispatchSubAgents } from "./targeting/agent-routing";
|
|
4
5
|
import { classifyAckReactionEmoji } from "./ack-reaction-classifier";
|
|
5
6
|
import { attachNativeAckReaction } from "./ack-reaction-service";
|
|
6
7
|
import { createDynamicAckReactionController } from "./ack-reaction/dynamic-ack-reaction-controller";
|
|
7
|
-
import { extractAttachmentText } from "./attachment-text-extractor";
|
|
8
|
+
import { extractAttachmentText } from "./messaging/attachment-text-extractor";
|
|
8
9
|
import { getAccessToken } from "./auth";
|
|
9
10
|
import { createAICard, finishAICard, isCardInTerminalState } from "./card-service";
|
|
11
|
+
import { handleInboundCommandDispatch } from "./command/inbound-command-dispatch-service";
|
|
10
12
|
import { resolveAckReactionSetting, resolveGroupConfig, resolveRobotCode } from "./config";
|
|
11
13
|
import { AICardStatus } from "./types";
|
|
12
14
|
import {
|
|
@@ -15,35 +17,10 @@ import {
|
|
|
15
17
|
removeCardRun,
|
|
16
18
|
} from "./card/card-run-registry";
|
|
17
19
|
import {
|
|
18
|
-
applyManualTargetLearningRule,
|
|
19
|
-
applyManualTargetsLearningRule,
|
|
20
|
-
applyManualGlobalLearningRule,
|
|
21
|
-
applyManualSessionLearningNote,
|
|
22
|
-
applyTargetSetLearningRule,
|
|
23
20
|
buildLearningContextBlock,
|
|
24
|
-
createOrUpdateTargetSet,
|
|
25
|
-
deleteManualRule,
|
|
26
|
-
disableManualRule,
|
|
27
21
|
isLearningEnabled,
|
|
28
|
-
listLearningTargetSets,
|
|
29
|
-
listScopedLearningRules,
|
|
30
|
-
resolveManualForcedReply,
|
|
31
22
|
} 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";
|
|
23
|
+
import { formatGroupMembers, noteGroupMember } from "./targeting/group-members-store";
|
|
47
24
|
import { setCurrentLogger } from "./logger-context";
|
|
48
25
|
import { prepareMediaInput, resolveOutboundMediaType } from "./media-utils";
|
|
49
26
|
import {
|
|
@@ -63,21 +40,11 @@ import {
|
|
|
63
40
|
clearProactiveRiskObservationsForTest,
|
|
64
41
|
getProactiveRiskObservationForAny,
|
|
65
42
|
} from "./proactive-risk-registry";
|
|
66
|
-
import { downloadGroupFile, getUnionIdByStaffId, resolveQuotedFile } from "./quoted-file-service";
|
|
43
|
+
import { downloadGroupFile, getUnionIdByStaffId, resolveQuotedFile } from "./messaging/quoted-file-service";
|
|
67
44
|
import { createReplyStrategy } from "./reply-strategy";
|
|
68
45
|
import type { DeliverPayload } from "./reply-strategy";
|
|
69
46
|
import { getDingTalkRuntime } from "./runtime";
|
|
70
47
|
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
48
|
import { acquireSessionLock } from "./session-lock";
|
|
82
49
|
import {
|
|
83
50
|
clearSessionPeerOverride,
|
|
@@ -89,7 +56,7 @@ import {
|
|
|
89
56
|
upsertObservedGroupTarget,
|
|
90
57
|
upsertObservedUserTarget,
|
|
91
58
|
} from "./targeting/target-directory-store";
|
|
92
|
-
import type { DingTalkConfig, HandleDingTalkMessageParams, MediaFile } from "./types";
|
|
59
|
+
import type { DingTalkConfig, HandleDingTalkMessageParams, Logger, MediaFile } from "./types";
|
|
93
60
|
import { formatDingTalkErrorPayloadLog, getErrorMessage, getErrorResponseData, maskSensitiveData } from "./utils";
|
|
94
61
|
import { isAbortRequestText } from "openclaw/plugin-sdk/reply-runtime";
|
|
95
62
|
|
|
@@ -98,6 +65,72 @@ const MIN_THINKING_REACTION_VISIBLE_MS = 1200;
|
|
|
98
65
|
const MAX_DYNAMIC_ACK_DISPOSE_WAIT_MS = 500;
|
|
99
66
|
const ATTACHMENT_TEXT_PREFIX = "[附件内容摘录]";
|
|
100
67
|
const proactiveHintLastSentAt = new Map<string, number>();
|
|
68
|
+
const sessionReasoningLevelCache = new Map<string, {
|
|
69
|
+
updatedAt?: number;
|
|
70
|
+
reasoningLevel?: string;
|
|
71
|
+
}>();
|
|
72
|
+
type ReplyMode = "card" | "markdown";
|
|
73
|
+
|
|
74
|
+
function readSessionReasoningLevel(params: {
|
|
75
|
+
storePath?: string;
|
|
76
|
+
sessionKey: string;
|
|
77
|
+
sessionUpdatedAt?: number;
|
|
78
|
+
log?: Logger;
|
|
79
|
+
}): string | undefined {
|
|
80
|
+
if (!params.storePath || !params.sessionKey) {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
const cacheKey = `${params.storePath}:${params.sessionKey}`;
|
|
84
|
+
const cached = sessionReasoningLevelCache.get(cacheKey);
|
|
85
|
+
if (
|
|
86
|
+
cached
|
|
87
|
+
&& params.sessionUpdatedAt !== undefined
|
|
88
|
+
&& cached.updatedAt === params.sessionUpdatedAt
|
|
89
|
+
) {
|
|
90
|
+
return cached.reasoningLevel;
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
const raw = fs.readFileSync(params.storePath, "utf8");
|
|
94
|
+
const parsed = JSON.parse(raw) as Record<string, { reasoningLevel?: unknown }>;
|
|
95
|
+
const value = parsed?.[params.sessionKey]?.reasoningLevel;
|
|
96
|
+
const reasoningLevel = typeof value === "string" ? value.trim().toLowerCase() : undefined;
|
|
97
|
+
sessionReasoningLevelCache.set(cacheKey, {
|
|
98
|
+
updatedAt: params.sessionUpdatedAt,
|
|
99
|
+
reasoningLevel,
|
|
100
|
+
});
|
|
101
|
+
return reasoningLevel;
|
|
102
|
+
} catch (err: unknown) {
|
|
103
|
+
params.log?.debug?.(
|
|
104
|
+
`[DingTalk][Session] Failed to read session reasoning level from ${params.storePath}: ${getErrorMessage(err)}`,
|
|
105
|
+
);
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function shouldDisableBlockStreamingForReplyMode(params: {
|
|
111
|
+
replyMode: ReplyMode;
|
|
112
|
+
reasoningLevel?: string;
|
|
113
|
+
sessionKey: string;
|
|
114
|
+
log?: Logger;
|
|
115
|
+
}): boolean {
|
|
116
|
+
if (params.replyMode === "markdown") {
|
|
117
|
+
const shouldDisable = params.reasoningLevel === "on" || params.reasoningLevel === "stream";
|
|
118
|
+
if (shouldDisable) {
|
|
119
|
+
params.log?.debug?.(
|
|
120
|
+
`[DingTalk][Markdown] Disable block streaming for reasoningLevel=${params.reasoningLevel} sessionKey=${params.sessionKey}`,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
return shouldDisable;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const shouldDisable = params.reasoningLevel !== "on";
|
|
127
|
+
if (!shouldDisable) {
|
|
128
|
+
params.log?.debug?.(
|
|
129
|
+
`[DingTalk][Card] Enable block streaming for reasoningLevel=${params.reasoningLevel} sessionKey=${params.sessionKey}`,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
return shouldDisable;
|
|
133
|
+
}
|
|
101
134
|
|
|
102
135
|
function resolvePinnedMainDmOwner(params: {
|
|
103
136
|
dmScope?: string;
|
|
@@ -239,6 +272,7 @@ function buildGroupTurnContextPrompt(params: {
|
|
|
239
272
|
|
|
240
273
|
type ReplyStreamPayload = {
|
|
241
274
|
text?: string;
|
|
275
|
+
isReasoning?: boolean;
|
|
242
276
|
};
|
|
243
277
|
|
|
244
278
|
type ReplyChunkInfo = {
|
|
@@ -365,7 +399,7 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
|
|
|
365
399
|
const rt = getDingTalkRuntime();
|
|
366
400
|
|
|
367
401
|
// Save logger globally so shared services can log consistently without threading log everywhere.
|
|
368
|
-
setCurrentLogger(log);
|
|
402
|
+
setCurrentLogger(log, accountId);
|
|
369
403
|
|
|
370
404
|
log?.debug?.("[DingTalk] Full Inbound Data: " + JSON.stringify(maskSensitiveData(data)));
|
|
371
405
|
|
|
@@ -628,423 +662,31 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
|
|
|
628
662
|
});
|
|
629
663
|
|
|
630
664
|
const to = isDirect ? senderId : groupId;
|
|
631
|
-
const
|
|
632
|
-
const parsedSessionCommand = parseSessionCommand(extractedContent.text);
|
|
633
|
-
const isOwner = isLearningOwner({
|
|
665
|
+
const commandHandled = await handleInboundCommandDispatch({
|
|
634
666
|
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
667
|
accountId,
|
|
1043
|
-
|
|
1044
|
-
|
|
668
|
+
dingtalkConfig,
|
|
669
|
+
senderId,
|
|
670
|
+
isDirect,
|
|
671
|
+
extractedText: extractedContent.text,
|
|
672
|
+
messageType: extractedContent.messageType,
|
|
673
|
+
data: {
|
|
674
|
+
conversationId: data.conversationId,
|
|
675
|
+
senderId: data.senderId,
|
|
676
|
+
senderStaffId: data.senderStaffId,
|
|
677
|
+
},
|
|
678
|
+
accountStorePath,
|
|
679
|
+
currentSessionSourceKind,
|
|
680
|
+
currentSessionSourceId,
|
|
681
|
+
peerIdOverride,
|
|
682
|
+
sessionPeer,
|
|
683
|
+
sendReply: async (text: string) => {
|
|
684
|
+
await sendBySession(dingtalkConfig, sessionWebhook, text, { log });
|
|
685
|
+
},
|
|
686
|
+
clearSessionPeerOverride,
|
|
687
|
+
setSessionPeerOverride,
|
|
1045
688
|
});
|
|
1046
|
-
if (
|
|
1047
|
-
await sendBySession(dingtalkConfig, sessionWebhook, manualForcedReply, { log });
|
|
689
|
+
if (commandHandled) {
|
|
1048
690
|
return;
|
|
1049
691
|
}
|
|
1050
692
|
// 3) Select response mode (card vs markdown).
|
|
@@ -1634,13 +1276,11 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
|
|
|
1634
1276
|
// tryFastAbortFromMessage (inside the SDK) kill any in-flight generation immediately,
|
|
1635
1277
|
// rather than waiting for it to finish before the stop message is processed.
|
|
1636
1278
|
//
|
|
1637
|
-
//
|
|
1638
|
-
//
|
|
1639
|
-
//
|
|
1640
|
-
// in
|
|
1641
|
-
const textForAbortCheck =
|
|
1642
|
-
? inboundText.replace(/^(?:@\S+\s+)*/u, "").trim()
|
|
1643
|
-
: inboundText;
|
|
1279
|
+
// Strip leading @mention tokens before the abort check so that messages like
|
|
1280
|
+
// "@Agent /stop" are correctly recognised as abort requests in both DM and group
|
|
1281
|
+
// chats. In groups DingTalk usually strips @BotName at the protocol level, but
|
|
1282
|
+
// in DMs with multi-agent routing the @mention prefix survives all the way here.
|
|
1283
|
+
const textForAbortCheck = inboundText.replace(/^(?:@\S+\s+)*/u, "").trim();
|
|
1644
1284
|
if (isAbortRequestText(textForAbortCheck)) {
|
|
1645
1285
|
log?.info?.(
|
|
1646
1286
|
`[DingTalk] Abort request detected, bypassing session lock for session=${route.sessionKey}`,
|
|
@@ -1856,16 +1496,31 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
|
|
|
1856
1496
|
}
|
|
1857
1497
|
|
|
1858
1498
|
// ---- Create reply strategy (card or markdown) ----
|
|
1499
|
+
const replyMode: ReplyMode = useCardMode && !!currentAICard ? "card" : "markdown";
|
|
1500
|
+
const sessionReasoningLevel = readSessionReasoningLevel({
|
|
1501
|
+
storePath,
|
|
1502
|
+
sessionKey: route.sessionKey,
|
|
1503
|
+
sessionUpdatedAt: previousTimestamp,
|
|
1504
|
+
log,
|
|
1505
|
+
});
|
|
1859
1506
|
const strategy = createReplyStrategy({
|
|
1860
1507
|
config: dingtalkConfig,
|
|
1861
1508
|
card: currentAICard,
|
|
1862
|
-
useCardMode:
|
|
1509
|
+
useCardMode: replyMode === "card",
|
|
1863
1510
|
to,
|
|
1864
1511
|
sessionWebhook,
|
|
1865
1512
|
senderId,
|
|
1866
1513
|
isDirect,
|
|
1867
1514
|
accountId,
|
|
1868
1515
|
storePath: accountStorePath,
|
|
1516
|
+
sessionKey: route.sessionKey,
|
|
1517
|
+
sessionAgentId: route.agentId,
|
|
1518
|
+
disableBlockStreaming: shouldDisableBlockStreamingForReplyMode({
|
|
1519
|
+
replyMode,
|
|
1520
|
+
sessionKey: route.sessionKey,
|
|
1521
|
+
reasoningLevel: sessionReasoningLevel,
|
|
1522
|
+
log,
|
|
1523
|
+
}),
|
|
1869
1524
|
groupId,
|
|
1870
1525
|
log,
|
|
1871
1526
|
replyQuotedRef,
|
|
@@ -1886,10 +1541,12 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
|
|
|
1886
1541
|
}
|
|
1887
1542
|
try {
|
|
1888
1543
|
const mediaUrls = extractMediaUrls(payload);
|
|
1544
|
+
const richPayload = payload as ReplyStreamPayload & { isReasoning?: boolean };
|
|
1889
1545
|
await strategy.deliver({
|
|
1890
1546
|
text: payload.text,
|
|
1891
1547
|
mediaUrls,
|
|
1892
1548
|
kind: (info?.kind as DeliverPayload["kind"]) || "block",
|
|
1549
|
+
isReasoning: richPayload.isReasoning === true,
|
|
1893
1550
|
});
|
|
1894
1551
|
} catch (err: unknown) {
|
|
1895
1552
|
log?.error?.(`[DingTalk] Reply failed: ${getErrorMessage(err)}`);
|