@soimy/dingtalk 3.5.0 → 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 +14 -3
- package/src/ack-reaction-service.ts +1 -1
- package/src/auth.ts +1 -1
- package/src/card/card-action-handler.ts +62 -0
- package/src/card/card-run-registry.ts +118 -0
- package/src/card/card-stop-handler.ts +94 -0
- package/src/card/card-template.ts +20 -0
- package/src/card/reasoning-block-assembler.ts +157 -0
- package/src/card-callback-service.ts +90 -8
- package/src/card-draft-controller.ts +32 -0
- package/src/card-service.ts +189 -128
- package/src/channel.ts +82 -53
- package/src/command/card-stop-command.ts +78 -0
- 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 +162 -465
- 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/onboarding.ts +4 -32
- package/src/reply-strategy-card.ts +85 -17
- package/src/reply-strategy-markdown.ts +123 -18
- package/src/reply-strategy.ts +5 -0
- package/src/send-service.ts +80 -17
- package/src/targeting/agent-routing.ts +11 -4
- package/src/{group-members-store.ts → targeting/group-members-store.ts} +1 -1
- package/src/types.ts +6 -1
- package/src/utils.ts +165 -0
package/src/inbound-handler.ts
CHANGED
|
@@ -1,43 +1,26 @@
|
|
|
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";
|
|
13
|
+
import { AICardStatus } from "./types";
|
|
14
|
+
import {
|
|
15
|
+
isCardRunStopRequested,
|
|
16
|
+
registerCardRun,
|
|
17
|
+
removeCardRun,
|
|
18
|
+
} from "./card/card-run-registry";
|
|
11
19
|
import {
|
|
12
|
-
applyManualTargetLearningRule,
|
|
13
|
-
applyManualTargetsLearningRule,
|
|
14
|
-
applyManualGlobalLearningRule,
|
|
15
|
-
applyManualSessionLearningNote,
|
|
16
|
-
applyTargetSetLearningRule,
|
|
17
20
|
buildLearningContextBlock,
|
|
18
|
-
createOrUpdateTargetSet,
|
|
19
|
-
deleteManualRule,
|
|
20
|
-
disableManualRule,
|
|
21
21
|
isLearningEnabled,
|
|
22
|
-
listLearningTargetSets,
|
|
23
|
-
listScopedLearningRules,
|
|
24
|
-
resolveManualForcedReply,
|
|
25
22
|
} from "./feedback-learning-service";
|
|
26
|
-
import { formatGroupMembers, noteGroupMember } from "./group-members-store";
|
|
27
|
-
import {
|
|
28
|
-
formatLearnAppliedReply,
|
|
29
|
-
formatLearnCommandHelp,
|
|
30
|
-
formatLearnDeletedReply,
|
|
31
|
-
formatLearnDisabledReply,
|
|
32
|
-
formatLearnListReply,
|
|
33
|
-
formatOwnerOnlyDeniedReply,
|
|
34
|
-
formatOwnerStatusReply,
|
|
35
|
-
formatTargetSetSavedReply,
|
|
36
|
-
formatWhereAmIReply,
|
|
37
|
-
formatWhoAmIReply,
|
|
38
|
-
isLearningOwner,
|
|
39
|
-
parseLearnCommand,
|
|
40
|
-
} from "./learning-command-service";
|
|
23
|
+
import { formatGroupMembers, noteGroupMember } from "./targeting/group-members-store";
|
|
41
24
|
import { setCurrentLogger } from "./logger-context";
|
|
42
25
|
import { prepareMediaInput, resolveOutboundMediaType } from "./media-utils";
|
|
43
26
|
import {
|
|
@@ -57,21 +40,11 @@ import {
|
|
|
57
40
|
clearProactiveRiskObservationsForTest,
|
|
58
41
|
getProactiveRiskObservationForAny,
|
|
59
42
|
} from "./proactive-risk-registry";
|
|
60
|
-
import { downloadGroupFile, getUnionIdByStaffId, resolveQuotedFile } from "./quoted-file-service";
|
|
43
|
+
import { downloadGroupFile, getUnionIdByStaffId, resolveQuotedFile } from "./messaging/quoted-file-service";
|
|
61
44
|
import { createReplyStrategy } from "./reply-strategy";
|
|
62
45
|
import type { DeliverPayload } from "./reply-strategy";
|
|
63
46
|
import { getDingTalkRuntime } from "./runtime";
|
|
64
47
|
import { sendBySession, sendMessage, sendProactiveMedia } from "./send-service";
|
|
65
|
-
import {
|
|
66
|
-
formatSessionAliasBoundReply,
|
|
67
|
-
formatSessionAliasClearedReply,
|
|
68
|
-
formatSessionAliasReply,
|
|
69
|
-
formatSessionAliasSetReply,
|
|
70
|
-
formatSessionAliasUnboundReply,
|
|
71
|
-
formatSessionAliasValidationErrorReply,
|
|
72
|
-
parseSessionCommand,
|
|
73
|
-
validateSessionAlias,
|
|
74
|
-
} from "./session-command-service";
|
|
75
48
|
import { acquireSessionLock } from "./session-lock";
|
|
76
49
|
import {
|
|
77
50
|
clearSessionPeerOverride,
|
|
@@ -83,8 +56,7 @@ import {
|
|
|
83
56
|
upsertObservedGroupTarget,
|
|
84
57
|
upsertObservedUserTarget,
|
|
85
58
|
} from "./targeting/target-directory-store";
|
|
86
|
-
import {
|
|
87
|
-
import type { DingTalkConfig, HandleDingTalkMessageParams, MediaFile } from "./types";
|
|
59
|
+
import type { DingTalkConfig, HandleDingTalkMessageParams, Logger, MediaFile } from "./types";
|
|
88
60
|
import { formatDingTalkErrorPayloadLog, getErrorMessage, getErrorResponseData, maskSensitiveData } from "./utils";
|
|
89
61
|
import { isAbortRequestText } from "openclaw/plugin-sdk/reply-runtime";
|
|
90
62
|
|
|
@@ -93,6 +65,72 @@ const MIN_THINKING_REACTION_VISIBLE_MS = 1200;
|
|
|
93
65
|
const MAX_DYNAMIC_ACK_DISPOSE_WAIT_MS = 500;
|
|
94
66
|
const ATTACHMENT_TEXT_PREFIX = "[附件内容摘录]";
|
|
95
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
|
+
}
|
|
96
134
|
|
|
97
135
|
function resolvePinnedMainDmOwner(params: {
|
|
98
136
|
dmScope?: string;
|
|
@@ -234,6 +272,7 @@ function buildGroupTurnContextPrompt(params: {
|
|
|
234
272
|
|
|
235
273
|
type ReplyStreamPayload = {
|
|
236
274
|
text?: string;
|
|
275
|
+
isReasoning?: boolean;
|
|
237
276
|
};
|
|
238
277
|
|
|
239
278
|
type ReplyChunkInfo = {
|
|
@@ -360,7 +399,7 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
|
|
|
360
399
|
const rt = getDingTalkRuntime();
|
|
361
400
|
|
|
362
401
|
// Save logger globally so shared services can log consistently without threading log everywhere.
|
|
363
|
-
setCurrentLogger(log);
|
|
402
|
+
setCurrentLogger(log, accountId);
|
|
364
403
|
|
|
365
404
|
log?.debug?.("[DingTalk] Full Inbound Data: " + JSON.stringify(maskSensitiveData(data)));
|
|
366
405
|
|
|
@@ -623,430 +662,38 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
|
|
|
623
662
|
});
|
|
624
663
|
|
|
625
664
|
const to = isDirect ? senderId : groupId;
|
|
626
|
-
const
|
|
627
|
-
const parsedSessionCommand = parseSessionCommand(extractedContent.text);
|
|
628
|
-
const isOwner = isLearningOwner({
|
|
665
|
+
const commandHandled = await handleInboundCommandDispatch({
|
|
629
666
|
cfg,
|
|
630
|
-
config: dingtalkConfig,
|
|
631
|
-
senderId,
|
|
632
|
-
rawSenderId: data.senderId,
|
|
633
|
-
});
|
|
634
|
-
if (isDirect && parsedLearnCommand.scope === "whoami") {
|
|
635
|
-
await sendBySession(
|
|
636
|
-
dingtalkConfig,
|
|
637
|
-
sessionWebhook,
|
|
638
|
-
formatWhoAmIReply({
|
|
639
|
-
senderId,
|
|
640
|
-
rawSenderId: data.senderId,
|
|
641
|
-
senderStaffId: data.senderStaffId,
|
|
642
|
-
isOwner,
|
|
643
|
-
}),
|
|
644
|
-
{ log },
|
|
645
|
-
);
|
|
646
|
-
return;
|
|
647
|
-
}
|
|
648
|
-
if (parsedLearnCommand.scope === "whereami") {
|
|
649
|
-
await sendBySession(
|
|
650
|
-
dingtalkConfig,
|
|
651
|
-
sessionWebhook,
|
|
652
|
-
formatWhereAmIReply({
|
|
653
|
-
conversationId: data.conversationId,
|
|
654
|
-
conversationType: isDirect ? "dm" : "group",
|
|
655
|
-
peerId: sessionPeer.peerId,
|
|
656
|
-
}),
|
|
657
|
-
{ log },
|
|
658
|
-
);
|
|
659
|
-
return;
|
|
660
|
-
}
|
|
661
|
-
if (isDirect && parsedLearnCommand.scope === "owner-status") {
|
|
662
|
-
await sendBySession(
|
|
663
|
-
dingtalkConfig,
|
|
664
|
-
sessionWebhook,
|
|
665
|
-
formatOwnerStatusReply({
|
|
666
|
-
senderId,
|
|
667
|
-
rawSenderId: data.senderId,
|
|
668
|
-
isOwner,
|
|
669
|
-
}),
|
|
670
|
-
{ log },
|
|
671
|
-
);
|
|
672
|
-
return;
|
|
673
|
-
}
|
|
674
|
-
if (parsedLearnCommand.scope === "help") {
|
|
675
|
-
await sendBySession(dingtalkConfig, sessionWebhook, formatLearnCommandHelp(), { log });
|
|
676
|
-
return;
|
|
677
|
-
}
|
|
678
|
-
if (
|
|
679
|
-
(parsedLearnCommand.scope === "global" ||
|
|
680
|
-
parsedLearnCommand.scope === "session" ||
|
|
681
|
-
parsedLearnCommand.scope === "here" ||
|
|
682
|
-
parsedLearnCommand.scope === "target" ||
|
|
683
|
-
parsedLearnCommand.scope === "targets" ||
|
|
684
|
-
parsedLearnCommand.scope === "list" ||
|
|
685
|
-
parsedLearnCommand.scope === "disable" ||
|
|
686
|
-
parsedLearnCommand.scope === "delete" ||
|
|
687
|
-
parsedLearnCommand.scope === "target-set-create" ||
|
|
688
|
-
parsedLearnCommand.scope === "target-set-apply" ||
|
|
689
|
-
parsedSessionCommand.scope === "session-alias-show" ||
|
|
690
|
-
parsedSessionCommand.scope === "session-alias-set" ||
|
|
691
|
-
parsedSessionCommand.scope === "session-alias-clear" ||
|
|
692
|
-
parsedSessionCommand.scope === "session-alias-bind" ||
|
|
693
|
-
parsedSessionCommand.scope === "session-alias-unbind") &&
|
|
694
|
-
!isOwner
|
|
695
|
-
) {
|
|
696
|
-
await sendBySession(dingtalkConfig, sessionWebhook, formatOwnerOnlyDeniedReply(), { log });
|
|
697
|
-
return;
|
|
698
|
-
}
|
|
699
|
-
if (isOwner) {
|
|
700
|
-
if (parsedSessionCommand.scope === "session-alias-show") {
|
|
701
|
-
await sendBySession(
|
|
702
|
-
dingtalkConfig,
|
|
703
|
-
sessionWebhook,
|
|
704
|
-
formatSessionAliasReply({
|
|
705
|
-
sourceKind: currentSessionSourceKind,
|
|
706
|
-
sourceId: currentSessionSourceId,
|
|
707
|
-
peerId: sessionPeer.peerId,
|
|
708
|
-
aliasSource: peerIdOverride ? "override" : "default",
|
|
709
|
-
}),
|
|
710
|
-
{ log },
|
|
711
|
-
);
|
|
712
|
-
return;
|
|
713
|
-
}
|
|
714
|
-
if (parsedSessionCommand.scope === "session-alias-set" && parsedSessionCommand.peerId) {
|
|
715
|
-
const aliasValidationError = validateSessionAlias(parsedSessionCommand.peerId);
|
|
716
|
-
if (aliasValidationError) {
|
|
717
|
-
await sendBySession(
|
|
718
|
-
dingtalkConfig,
|
|
719
|
-
sessionWebhook,
|
|
720
|
-
formatSessionAliasValidationErrorReply(aliasValidationError),
|
|
721
|
-
{ log },
|
|
722
|
-
);
|
|
723
|
-
return;
|
|
724
|
-
}
|
|
725
|
-
setSessionPeerOverride({
|
|
726
|
-
storePath: accountStorePath,
|
|
727
|
-
accountId,
|
|
728
|
-
sourceKind: currentSessionSourceKind,
|
|
729
|
-
sourceId: currentSessionSourceId,
|
|
730
|
-
peerId: parsedSessionCommand.peerId,
|
|
731
|
-
});
|
|
732
|
-
await sendBySession(
|
|
733
|
-
dingtalkConfig,
|
|
734
|
-
sessionWebhook,
|
|
735
|
-
formatSessionAliasSetReply({
|
|
736
|
-
sourceKind: currentSessionSourceKind,
|
|
737
|
-
sourceId: currentSessionSourceId,
|
|
738
|
-
peerId: parsedSessionCommand.peerId,
|
|
739
|
-
}),
|
|
740
|
-
{ log },
|
|
741
|
-
);
|
|
742
|
-
return;
|
|
743
|
-
}
|
|
744
|
-
if (parsedSessionCommand.scope === "session-alias-clear") {
|
|
745
|
-
clearSessionPeerOverride({
|
|
746
|
-
storePath: accountStorePath,
|
|
747
|
-
accountId,
|
|
748
|
-
sourceKind: currentSessionSourceKind,
|
|
749
|
-
sourceId: currentSessionSourceId,
|
|
750
|
-
});
|
|
751
|
-
await sendBySession(
|
|
752
|
-
dingtalkConfig,
|
|
753
|
-
sessionWebhook,
|
|
754
|
-
formatSessionAliasClearedReply({
|
|
755
|
-
sourceKind: currentSessionSourceKind,
|
|
756
|
-
sourceId: currentSessionSourceId,
|
|
757
|
-
}),
|
|
758
|
-
{ log },
|
|
759
|
-
);
|
|
760
|
-
return;
|
|
761
|
-
}
|
|
762
|
-
if (
|
|
763
|
-
parsedSessionCommand.scope === "session-alias-bind" &&
|
|
764
|
-
parsedSessionCommand.sourceKind &&
|
|
765
|
-
parsedSessionCommand.sourceId &&
|
|
766
|
-
parsedSessionCommand.peerId
|
|
767
|
-
) {
|
|
768
|
-
const aliasValidationError = validateSessionAlias(parsedSessionCommand.peerId);
|
|
769
|
-
if (aliasValidationError) {
|
|
770
|
-
await sendBySession(
|
|
771
|
-
dingtalkConfig,
|
|
772
|
-
sessionWebhook,
|
|
773
|
-
formatSessionAliasValidationErrorReply(aliasValidationError),
|
|
774
|
-
{ log },
|
|
775
|
-
);
|
|
776
|
-
return;
|
|
777
|
-
}
|
|
778
|
-
setSessionPeerOverride({
|
|
779
|
-
storePath: accountStorePath,
|
|
780
|
-
accountId,
|
|
781
|
-
sourceKind: parsedSessionCommand.sourceKind,
|
|
782
|
-
sourceId: parsedSessionCommand.sourceId,
|
|
783
|
-
peerId: parsedSessionCommand.peerId,
|
|
784
|
-
});
|
|
785
|
-
await sendBySession(
|
|
786
|
-
dingtalkConfig,
|
|
787
|
-
sessionWebhook,
|
|
788
|
-
formatSessionAliasBoundReply({
|
|
789
|
-
sourceKind: parsedSessionCommand.sourceKind,
|
|
790
|
-
sourceId: parsedSessionCommand.sourceId,
|
|
791
|
-
peerId: parsedSessionCommand.peerId,
|
|
792
|
-
}),
|
|
793
|
-
{ log },
|
|
794
|
-
);
|
|
795
|
-
return;
|
|
796
|
-
}
|
|
797
|
-
if (
|
|
798
|
-
parsedSessionCommand.scope === "session-alias-unbind" &&
|
|
799
|
-
parsedSessionCommand.sourceKind &&
|
|
800
|
-
parsedSessionCommand.sourceId
|
|
801
|
-
) {
|
|
802
|
-
const existed = clearSessionPeerOverride({
|
|
803
|
-
storePath: accountStorePath,
|
|
804
|
-
accountId,
|
|
805
|
-
sourceKind: parsedSessionCommand.sourceKind,
|
|
806
|
-
sourceId: parsedSessionCommand.sourceId,
|
|
807
|
-
});
|
|
808
|
-
await sendBySession(
|
|
809
|
-
dingtalkConfig,
|
|
810
|
-
sessionWebhook,
|
|
811
|
-
formatSessionAliasUnboundReply({
|
|
812
|
-
sourceKind: parsedSessionCommand.sourceKind,
|
|
813
|
-
sourceId: parsedSessionCommand.sourceId,
|
|
814
|
-
existed,
|
|
815
|
-
}),
|
|
816
|
-
{ log },
|
|
817
|
-
);
|
|
818
|
-
return;
|
|
819
|
-
}
|
|
820
|
-
if (parsedLearnCommand.scope === "global" && parsedLearnCommand.instruction) {
|
|
821
|
-
const applied = applyManualGlobalLearningRule({
|
|
822
|
-
storePath: accountStorePath,
|
|
823
|
-
accountId,
|
|
824
|
-
instruction: parsedLearnCommand.instruction,
|
|
825
|
-
});
|
|
826
|
-
await sendBySession(
|
|
827
|
-
dingtalkConfig,
|
|
828
|
-
sessionWebhook,
|
|
829
|
-
formatLearnAppliedReply({
|
|
830
|
-
scope: "global",
|
|
831
|
-
instruction: parsedLearnCommand.instruction,
|
|
832
|
-
ruleId: applied?.ruleId,
|
|
833
|
-
}),
|
|
834
|
-
{ log },
|
|
835
|
-
);
|
|
836
|
-
return;
|
|
837
|
-
}
|
|
838
|
-
if (parsedLearnCommand.scope === "session" && parsedLearnCommand.instruction) {
|
|
839
|
-
applyManualSessionLearningNote({
|
|
840
|
-
storePath: accountStorePath,
|
|
841
|
-
accountId,
|
|
842
|
-
targetId: data.conversationId,
|
|
843
|
-
instruction: parsedLearnCommand.instruction,
|
|
844
|
-
});
|
|
845
|
-
await sendBySession(
|
|
846
|
-
dingtalkConfig,
|
|
847
|
-
sessionWebhook,
|
|
848
|
-
formatLearnAppliedReply({
|
|
849
|
-
scope: "session",
|
|
850
|
-
instruction: parsedLearnCommand.instruction,
|
|
851
|
-
}),
|
|
852
|
-
{ log },
|
|
853
|
-
);
|
|
854
|
-
return;
|
|
855
|
-
}
|
|
856
|
-
if (parsedLearnCommand.scope === "here" && parsedLearnCommand.instruction) {
|
|
857
|
-
const applied = applyManualTargetLearningRule({
|
|
858
|
-
storePath: accountStorePath,
|
|
859
|
-
accountId,
|
|
860
|
-
targetId: data.conversationId,
|
|
861
|
-
instruction: parsedLearnCommand.instruction,
|
|
862
|
-
});
|
|
863
|
-
await sendBySession(
|
|
864
|
-
dingtalkConfig,
|
|
865
|
-
sessionWebhook,
|
|
866
|
-
formatLearnAppliedReply({
|
|
867
|
-
scope: "target",
|
|
868
|
-
targetId: data.conversationId,
|
|
869
|
-
instruction: parsedLearnCommand.instruction,
|
|
870
|
-
ruleId: applied?.ruleId,
|
|
871
|
-
}),
|
|
872
|
-
{ log },
|
|
873
|
-
);
|
|
874
|
-
return;
|
|
875
|
-
}
|
|
876
|
-
if (
|
|
877
|
-
parsedLearnCommand.scope === "target" &&
|
|
878
|
-
parsedLearnCommand.targetId &&
|
|
879
|
-
parsedLearnCommand.instruction
|
|
880
|
-
) {
|
|
881
|
-
const applied = applyManualTargetLearningRule({
|
|
882
|
-
storePath: accountStorePath,
|
|
883
|
-
accountId,
|
|
884
|
-
targetId: parsedLearnCommand.targetId,
|
|
885
|
-
instruction: parsedLearnCommand.instruction,
|
|
886
|
-
});
|
|
887
|
-
await sendBySession(
|
|
888
|
-
dingtalkConfig,
|
|
889
|
-
sessionWebhook,
|
|
890
|
-
formatLearnAppliedReply({
|
|
891
|
-
scope: "target",
|
|
892
|
-
targetId: parsedLearnCommand.targetId,
|
|
893
|
-
instruction: parsedLearnCommand.instruction,
|
|
894
|
-
ruleId: applied?.ruleId,
|
|
895
|
-
}),
|
|
896
|
-
{ log },
|
|
897
|
-
);
|
|
898
|
-
return;
|
|
899
|
-
}
|
|
900
|
-
if (
|
|
901
|
-
parsedLearnCommand.scope === "targets" &&
|
|
902
|
-
parsedLearnCommand.targetIds?.length &&
|
|
903
|
-
parsedLearnCommand.instruction
|
|
904
|
-
) {
|
|
905
|
-
const applied = applyManualTargetsLearningRule({
|
|
906
|
-
storePath: accountStorePath,
|
|
907
|
-
accountId,
|
|
908
|
-
targetIds: parsedLearnCommand.targetIds,
|
|
909
|
-
instruction: parsedLearnCommand.instruction,
|
|
910
|
-
});
|
|
911
|
-
await sendBySession(
|
|
912
|
-
dingtalkConfig,
|
|
913
|
-
sessionWebhook,
|
|
914
|
-
formatLearnAppliedReply({
|
|
915
|
-
scope: "targets",
|
|
916
|
-
targetIds: parsedLearnCommand.targetIds,
|
|
917
|
-
instruction: parsedLearnCommand.instruction,
|
|
918
|
-
ruleId: applied[0]?.ruleId,
|
|
919
|
-
}),
|
|
920
|
-
{ log },
|
|
921
|
-
);
|
|
922
|
-
return;
|
|
923
|
-
}
|
|
924
|
-
if (
|
|
925
|
-
parsedLearnCommand.scope === "target-set-create" &&
|
|
926
|
-
parsedLearnCommand.setName &&
|
|
927
|
-
parsedLearnCommand.targetIds?.length
|
|
928
|
-
) {
|
|
929
|
-
const saved = createOrUpdateTargetSet({
|
|
930
|
-
storePath: accountStorePath,
|
|
931
|
-
accountId,
|
|
932
|
-
name: parsedLearnCommand.setName,
|
|
933
|
-
targetIds: parsedLearnCommand.targetIds,
|
|
934
|
-
});
|
|
935
|
-
await sendBySession(
|
|
936
|
-
dingtalkConfig,
|
|
937
|
-
sessionWebhook,
|
|
938
|
-
saved
|
|
939
|
-
? formatTargetSetSavedReply({
|
|
940
|
-
setName: parsedLearnCommand.setName,
|
|
941
|
-
targetIds: parsedLearnCommand.targetIds,
|
|
942
|
-
})
|
|
943
|
-
: "目标组保存失败,请检查名称和目标列表。",
|
|
944
|
-
{ log },
|
|
945
|
-
);
|
|
946
|
-
return;
|
|
947
|
-
}
|
|
948
|
-
if (
|
|
949
|
-
parsedLearnCommand.scope === "target-set-apply" &&
|
|
950
|
-
parsedLearnCommand.setName &&
|
|
951
|
-
parsedLearnCommand.instruction
|
|
952
|
-
) {
|
|
953
|
-
const applied = applyTargetSetLearningRule({
|
|
954
|
-
storePath: accountStorePath,
|
|
955
|
-
accountId,
|
|
956
|
-
name: parsedLearnCommand.setName,
|
|
957
|
-
instruction: parsedLearnCommand.instruction,
|
|
958
|
-
});
|
|
959
|
-
await sendBySession(
|
|
960
|
-
dingtalkConfig,
|
|
961
|
-
sessionWebhook,
|
|
962
|
-
applied.length > 0
|
|
963
|
-
? formatLearnAppliedReply({
|
|
964
|
-
scope: "target-set",
|
|
965
|
-
setName: parsedLearnCommand.setName,
|
|
966
|
-
targetIds: applied.map((item) => item.targetId),
|
|
967
|
-
instruction: parsedLearnCommand.instruction,
|
|
968
|
-
ruleId: applied[0]?.ruleId,
|
|
969
|
-
})
|
|
970
|
-
: `未找到目标组 \`${parsedLearnCommand.setName}\`,或该目标组为空。`,
|
|
971
|
-
{ log },
|
|
972
|
-
);
|
|
973
|
-
return;
|
|
974
|
-
}
|
|
975
|
-
if (parsedLearnCommand.scope === "list") {
|
|
976
|
-
const rules = listScopedLearningRules({ storePath: accountStorePath, accountId })
|
|
977
|
-
.slice(0, 20)
|
|
978
|
-
.map((rule) => {
|
|
979
|
-
const scope = rule.scope === "target" ? `target(${rule.targetId})` : "global";
|
|
980
|
-
const status = rule.enabled ? "enabled" : "disabled";
|
|
981
|
-
return `- [${scope}] ${rule.ruleId} (${status}) => ${rule.instruction}`;
|
|
982
|
-
});
|
|
983
|
-
const targetSets = listLearningTargetSets({ storePath: accountStorePath, accountId })
|
|
984
|
-
.slice(0, 10)
|
|
985
|
-
.map(
|
|
986
|
-
(targetSet) => `- [target-set] ${targetSet.name} => ${targetSet.targetIds.join(", ")}`,
|
|
987
|
-
);
|
|
988
|
-
await sendBySession(
|
|
989
|
-
dingtalkConfig,
|
|
990
|
-
sessionWebhook,
|
|
991
|
-
formatLearnListReply([...rules, ...targetSets]),
|
|
992
|
-
{ log },
|
|
993
|
-
);
|
|
994
|
-
return;
|
|
995
|
-
}
|
|
996
|
-
if (parsedLearnCommand.scope === "disable" && parsedLearnCommand.ruleId) {
|
|
997
|
-
const result = disableManualRule({
|
|
998
|
-
storePath: accountStorePath,
|
|
999
|
-
accountId,
|
|
1000
|
-
ruleId: parsedLearnCommand.ruleId,
|
|
1001
|
-
});
|
|
1002
|
-
await sendBySession(
|
|
1003
|
-
dingtalkConfig,
|
|
1004
|
-
sessionWebhook,
|
|
1005
|
-
formatLearnDisabledReply({
|
|
1006
|
-
ruleId: parsedLearnCommand.ruleId,
|
|
1007
|
-
existed: result.existed,
|
|
1008
|
-
scope: result.scope,
|
|
1009
|
-
targetId: result.targetId,
|
|
1010
|
-
}),
|
|
1011
|
-
{ log },
|
|
1012
|
-
);
|
|
1013
|
-
return;
|
|
1014
|
-
}
|
|
1015
|
-
if (parsedLearnCommand.scope === "delete" && parsedLearnCommand.ruleId) {
|
|
1016
|
-
const result = deleteManualRule({
|
|
1017
|
-
storePath: accountStorePath,
|
|
1018
|
-
accountId,
|
|
1019
|
-
ruleId: parsedLearnCommand.ruleId,
|
|
1020
|
-
});
|
|
1021
|
-
await sendBySession(
|
|
1022
|
-
dingtalkConfig,
|
|
1023
|
-
sessionWebhook,
|
|
1024
|
-
formatLearnDeletedReply({
|
|
1025
|
-
ruleId: parsedLearnCommand.ruleId,
|
|
1026
|
-
existed: result.existed,
|
|
1027
|
-
scope: result.scope,
|
|
1028
|
-
targetId: result.targetId,
|
|
1029
|
-
}),
|
|
1030
|
-
{ log },
|
|
1031
|
-
);
|
|
1032
|
-
return;
|
|
1033
|
-
}
|
|
1034
|
-
}
|
|
1035
|
-
const manualForcedReply = resolveManualForcedReply({
|
|
1036
|
-
storePath: accountStorePath,
|
|
1037
667
|
accountId,
|
|
1038
|
-
|
|
1039
|
-
|
|
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,
|
|
1040
688
|
});
|
|
1041
|
-
if (
|
|
1042
|
-
await sendBySession(dingtalkConfig, sessionWebhook, manualForcedReply, { log });
|
|
689
|
+
if (commandHandled) {
|
|
1043
690
|
return;
|
|
1044
691
|
}
|
|
1045
692
|
// 3) Select response mode (card vs markdown).
|
|
1046
693
|
// Card creation runs BEFORE media download so the user sees immediate visual
|
|
1047
694
|
// feedback while large files are still being downloaded.
|
|
1048
695
|
let useCardMode = dingtalkConfig.messageType === "card";
|
|
1049
|
-
let currentAICard
|
|
696
|
+
let currentAICard: import("./types").AICardInstance | undefined;
|
|
1050
697
|
|
|
1051
698
|
if (useCardMode) {
|
|
1052
699
|
try {
|
|
@@ -1060,6 +707,15 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
|
|
|
1060
707
|
});
|
|
1061
708
|
if (aiCard) {
|
|
1062
709
|
currentAICard = aiCard;
|
|
710
|
+
if (aiCard.outTrackId) {
|
|
711
|
+
registerCardRun(aiCard.outTrackId, {
|
|
712
|
+
accountId,
|
|
713
|
+
sessionKey: route.sessionKey,
|
|
714
|
+
agentId: route.agentId,
|
|
715
|
+
ownerUserId: senderId,
|
|
716
|
+
card: aiCard,
|
|
717
|
+
});
|
|
718
|
+
}
|
|
1063
719
|
} else {
|
|
1064
720
|
useCardMode = false;
|
|
1065
721
|
log?.warn?.(
|
|
@@ -1620,13 +1276,11 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
|
|
|
1620
1276
|
// tryFastAbortFromMessage (inside the SDK) kill any in-flight generation immediately,
|
|
1621
1277
|
// rather than waiting for it to finish before the stop message is processed.
|
|
1622
1278
|
//
|
|
1623
|
-
//
|
|
1624
|
-
//
|
|
1625
|
-
//
|
|
1626
|
-
// in
|
|
1627
|
-
const textForAbortCheck =
|
|
1628
|
-
? inboundText.replace(/^(?:@\S+\s+)*/u, "").trim()
|
|
1629
|
-
: 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();
|
|
1630
1284
|
if (isAbortRequestText(textForAbortCheck)) {
|
|
1631
1285
|
log?.info?.(
|
|
1632
1286
|
`[DingTalk] Abort request detected, bypassing session lock for session=${route.sessionKey}`,
|
|
@@ -1798,6 +1452,7 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
|
|
|
1798
1452
|
// causes empty replies for all but the first caller.
|
|
1799
1453
|
// Each sub-agent call acquires its own lock since sub-agent sessions have
|
|
1800
1454
|
// different session keys (different agentId), so no deadlock risk.
|
|
1455
|
+
const currentOutTrackId = currentAICard?.outTrackId;
|
|
1801
1456
|
const shouldTrackDynamicAckReaction =
|
|
1802
1457
|
(normalizedAckReaction === "emoji" || normalizedAckReaction === "kaomoji")
|
|
1803
1458
|
&& shouldAttachAckReaction;
|
|
@@ -1826,22 +1481,51 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
|
|
|
1826
1481
|
if (!ackReactionAttached && shouldAttachAckReaction) {
|
|
1827
1482
|
log?.debug?.("[DingTalk] Native ack reaction unavailable; skipping fallback.");
|
|
1828
1483
|
}
|
|
1484
|
+
const isCurrentCardStopRequested = () =>
|
|
1485
|
+
Boolean(
|
|
1486
|
+
currentAICard
|
|
1487
|
+
&& (
|
|
1488
|
+
currentAICard.state === AICardStatus.STOPPED
|
|
1489
|
+
|| (currentOutTrackId && isCardRunStopRequested(currentOutTrackId))
|
|
1490
|
+
),
|
|
1491
|
+
);
|
|
1492
|
+
|
|
1493
|
+
if (isCurrentCardStopRequested()) {
|
|
1494
|
+
log?.info?.("[DingTalk][CardStop] Skip dispatch because card was already stopped before session lock was acquired");
|
|
1495
|
+
return;
|
|
1496
|
+
}
|
|
1829
1497
|
|
|
1830
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
|
+
});
|
|
1831
1506
|
const strategy = createReplyStrategy({
|
|
1832
1507
|
config: dingtalkConfig,
|
|
1833
1508
|
card: currentAICard,
|
|
1834
|
-
useCardMode:
|
|
1509
|
+
useCardMode: replyMode === "card",
|
|
1835
1510
|
to,
|
|
1836
1511
|
sessionWebhook,
|
|
1837
1512
|
senderId,
|
|
1838
1513
|
isDirect,
|
|
1839
1514
|
accountId,
|
|
1840
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
|
+
}),
|
|
1841
1524
|
groupId,
|
|
1842
1525
|
log,
|
|
1843
1526
|
replyQuotedRef,
|
|
1844
1527
|
deliverMedia: deliverMediaAttachments,
|
|
1528
|
+
isStopRequested: isCurrentCardStopRequested,
|
|
1845
1529
|
});
|
|
1846
1530
|
|
|
1847
1531
|
try {
|
|
@@ -1851,12 +1535,18 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
|
|
|
1851
1535
|
dispatcherOptions: {
|
|
1852
1536
|
responsePrefix: subAgentOptions?.responsePrefix || "",
|
|
1853
1537
|
deliver: async (payload: ReplyStreamPayload, info?: ReplyChunkInfo) => {
|
|
1538
|
+
if (isCurrentCardStopRequested()) {
|
|
1539
|
+
log?.debug?.("[DingTalk][CardStop] Ignoring reply delivery because stop was already requested");
|
|
1540
|
+
return;
|
|
1541
|
+
}
|
|
1854
1542
|
try {
|
|
1855
1543
|
const mediaUrls = extractMediaUrls(payload);
|
|
1544
|
+
const richPayload = payload as ReplyStreamPayload & { isReasoning?: boolean };
|
|
1856
1545
|
await strategy.deliver({
|
|
1857
1546
|
text: payload.text,
|
|
1858
1547
|
mediaUrls,
|
|
1859
1548
|
kind: (info?.kind as DeliverPayload["kind"]) || "block",
|
|
1549
|
+
isReasoning: richPayload.isReasoning === true,
|
|
1860
1550
|
});
|
|
1861
1551
|
} catch (err: unknown) {
|
|
1862
1552
|
log?.error?.(`[DingTalk] Reply failed: ${getErrorMessage(err)}`);
|
|
@@ -1878,6 +1568,13 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
|
|
|
1878
1568
|
|
|
1879
1569
|
await strategy.finalize();
|
|
1880
1570
|
} finally {
|
|
1571
|
+
// Only remove the registry entry if no stop was requested. When a stop is
|
|
1572
|
+
// in progress, card-stop-handler may still be running async operations
|
|
1573
|
+
// (finalize card, hide button, gateway abort) that read the record.
|
|
1574
|
+
// In that case, let the 30-minute TTL sweep handle cleanup.
|
|
1575
|
+
if (currentOutTrackId && !isCardRunStopRequested(currentOutTrackId)) {
|
|
1576
|
+
removeCardRun(currentOutTrackId);
|
|
1577
|
+
}
|
|
1881
1578
|
await waitForDynamicAckDispose({
|
|
1882
1579
|
dispose: () => dynamicAckReactionController.dispose(MIN_THINKING_REACTION_VISIBLE_MS),
|
|
1883
1580
|
log,
|