@soimy/dingtalk 3.6.5 → 3.6.7
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 +2 -0
- package/dist/index.js +773 -136
- package/dist/index.js.map +4 -4
- package/dist/src/card/ask-user-question-context.d.ts +5 -2
- package/dist/src/card/ask-user-question-context.d.ts.map +1 -1
- package/dist/src/card/ask-user-question-store.d.ts +42 -0
- package/dist/src/card/ask-user-question-store.d.ts.map +1 -0
- package/dist/src/card/ask-user-question.d.ts +20 -0
- package/dist/src/card/ask-user-question.d.ts.map +1 -1
- package/dist/src/card/card-action-handler.d.ts +1 -0
- package/dist/src/card/card-action-handler.d.ts.map +1 -1
- package/dist/src/card/card-template.d.ts +1 -1
- package/dist/src/card/task-model-metadata.d.ts +11 -0
- package/dist/src/card/task-model-metadata.d.ts.map +1 -0
- package/dist/src/gateway/channel-gateway.d.ts.map +1 -1
- package/dist/src/inbound-handler.d.ts.map +1 -1
- package/dist/src/reply-strategy-card.d.ts.map +1 -1
- package/dist/src/reply-strategy-types.d.ts +1 -0
- package/dist/src/reply-strategy-types.d.ts.map +1 -1
- package/dist/src/session-state.d.ts +11 -5
- package/dist/src/session-state.d.ts.map +1 -1
- package/dist/src/targeting/agent-routing.d.ts +8 -1
- package/dist/src/targeting/agent-routing.d.ts.map +1 -1
- package/dist/src/types.d.ts +13 -0
- package/dist/src/types.d.ts.map +1 -1
- package/docs/assets/dingtalk-ask-user-card-template.json +6 -0
- package/package.json +2 -1
- package/src/access-control.ts +1 -1
- package/src/card/ask-user-question-context.ts +9 -1
- package/src/card/ask-user-question-store.ts +294 -0
- package/src/card/ask-user-question.ts +380 -34
- package/src/card/card-action-handler.ts +2 -0
- package/src/card/card-template.ts +1 -1
- package/src/card/task-model-metadata.ts +51 -0
- package/src/gateway/channel-gateway.ts +19 -0
- package/src/inbound-handler.ts +131 -49
- package/src/reply-strategy-card.ts +30 -6
- package/src/reply-strategy-types.ts +1 -0
- package/src/session-state.ts +44 -13
- package/src/targeting/agent-routing.ts +67 -19
- package/src/types.ts +14 -0
package/src/inbound-handler.ts
CHANGED
|
@@ -13,12 +13,17 @@ import {
|
|
|
13
13
|
isCardInTerminalState,
|
|
14
14
|
recallAICardMessage,
|
|
15
15
|
} from "./card-service";
|
|
16
|
+
import {
|
|
17
|
+
invalidateAskUserQuestionsForScope,
|
|
18
|
+
syncInvalidatedAskUserQuestionCards,
|
|
19
|
+
} from "./card/ask-user-question";
|
|
16
20
|
import {
|
|
17
21
|
getDingTalkQuestionContext,
|
|
18
22
|
withDingTalkQuestionContext,
|
|
19
23
|
} from "./card/ask-user-question-context";
|
|
20
24
|
import { isCardRunStopRequested, registerCardRun, removeCardRun } from "./card/card-run-registry";
|
|
21
25
|
import { renderStatusLine } from "./card/statusline-renderer";
|
|
26
|
+
import { resolveConfiguredTaskModelMetadata } from "./card/task-model-metadata";
|
|
22
27
|
import { dispatchDingTalkCardStopCommand } from "./command/card-stop-command";
|
|
23
28
|
import { handleInboundCommandDispatch } from "./command/inbound-command-dispatch-service";
|
|
24
29
|
import {
|
|
@@ -80,7 +85,13 @@ import {
|
|
|
80
85
|
upsertObservedUserTarget,
|
|
81
86
|
} from "./targeting/target-directory-store";
|
|
82
87
|
import { AICardStatus } from "./types";
|
|
83
|
-
import type {
|
|
88
|
+
import type {
|
|
89
|
+
DingTalkConfig,
|
|
90
|
+
HandleDingTalkMessageParams,
|
|
91
|
+
Logger,
|
|
92
|
+
MediaFile,
|
|
93
|
+
ResolvedDingTalkRoute,
|
|
94
|
+
} from "./types";
|
|
84
95
|
import {
|
|
85
96
|
formatDingTalkErrorPayloadLog,
|
|
86
97
|
getErrorMessage,
|
|
@@ -428,6 +439,7 @@ function buildGroupTurnContextPrompt(params: {
|
|
|
428
439
|
|
|
429
440
|
type ReplyStreamPayload = {
|
|
430
441
|
text?: string;
|
|
442
|
+
isError?: boolean;
|
|
431
443
|
isReasoning?: boolean;
|
|
432
444
|
mediaUrl?: string;
|
|
433
445
|
mediaUrls?: string[];
|
|
@@ -582,6 +594,8 @@ async function handleDingTalkMessageInner(params: HandleDingTalkMessageParams):
|
|
|
582
594
|
sessionWebhook,
|
|
583
595
|
log,
|
|
584
596
|
dingtalkConfig,
|
|
597
|
+
inboundOrigin = "stream",
|
|
598
|
+
routeOverride,
|
|
585
599
|
subAgentOptions,
|
|
586
600
|
preDownloadedMedia,
|
|
587
601
|
} = params;
|
|
@@ -808,38 +822,42 @@ async function handleDingTalkMessageInner(params: HandleDingTalkMessageParams):
|
|
|
808
822
|
? null
|
|
809
823
|
: resolveMessageTarget({ extractedContent, cfg, isGroup });
|
|
810
824
|
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
825
|
+
const invalidateQuestionRoutes = (routes: readonly ResolvedDingTalkRoute[]): void => {
|
|
826
|
+
if (inboundOrigin === "ask-user") {
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
const scopeKeys = new Set(
|
|
830
|
+
routes.map((resolvedRoute) => `${accountId}:${resolvedRoute.sessionKey}:${senderId}`),
|
|
831
|
+
);
|
|
832
|
+
const invalidatedRecords = [];
|
|
833
|
+
for (const questionScopeKey of scopeKeys) {
|
|
834
|
+
try {
|
|
835
|
+
invalidatedRecords.push(
|
|
836
|
+
...invalidateAskUserQuestionsForScope({
|
|
837
|
+
storePath: accountStorePath,
|
|
838
|
+
accountId,
|
|
839
|
+
questionScopeKey,
|
|
840
|
+
reason: "superseded_by_message",
|
|
841
|
+
log,
|
|
842
|
+
}),
|
|
843
|
+
);
|
|
844
|
+
} catch (err) {
|
|
845
|
+
log?.warn?.(
|
|
846
|
+
`[DingTalk][AskUser] Failed to invalidate pending cards before inbound dispatch scope=${questionScopeKey}: ${String(err)}`,
|
|
847
|
+
);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
if (invalidatedRecords.length === 0) {
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
void syncInvalidatedAskUserQuestionCards({
|
|
854
|
+
records: invalidatedRecords,
|
|
855
|
+
config: dingtalkConfig,
|
|
856
|
+
log,
|
|
857
|
+
}).catch((err) => {
|
|
858
|
+
log?.warn?.(`[DingTalk][AskUser] Card invalidation sync failed: ${String(err)}`);
|
|
837
859
|
});
|
|
838
|
-
}
|
|
839
|
-
const questionContext = getDingTalkQuestionContext();
|
|
840
|
-
if (questionContext) {
|
|
841
|
-
questionContext.questionScopeKey = `${accountId}:${route.sessionKey}:${senderId}`;
|
|
842
|
-
}
|
|
860
|
+
};
|
|
843
861
|
|
|
844
862
|
// @Sub-Agent routing: dispatch @mention-targeted messages to their agent(s).
|
|
845
863
|
// Both content (`@agent <message>`) and commands (`@agent /new`) re-enter
|
|
@@ -856,6 +874,9 @@ async function handleDingTalkMessageInner(params: HandleDingTalkMessageParams):
|
|
|
856
874
|
dingtalkConfig,
|
|
857
875
|
sessionWebhook,
|
|
858
876
|
extractedContent,
|
|
877
|
+
sessionPeer,
|
|
878
|
+
onRoutesResolved: (targets) =>
|
|
879
|
+
invalidateQuestionRoutes(targets.map((target) => target.route)),
|
|
859
880
|
handleMessage: handleDingTalkMessage,
|
|
860
881
|
downloadMedia,
|
|
861
882
|
log,
|
|
@@ -887,6 +908,9 @@ async function handleDingTalkMessageInner(params: HandleDingTalkMessageParams):
|
|
|
887
908
|
dingtalkConfig,
|
|
888
909
|
sessionWebhook,
|
|
889
910
|
extractedContent,
|
|
911
|
+
sessionPeer,
|
|
912
|
+
onRoutesResolved: (targets) =>
|
|
913
|
+
invalidateQuestionRoutes(targets.map((target) => target.route)),
|
|
890
914
|
handleMessage: handleDingTalkMessage,
|
|
891
915
|
downloadMedia,
|
|
892
916
|
log,
|
|
@@ -896,6 +920,47 @@ async function handleDingTalkMessageInner(params: HandleDingTalkMessageParams):
|
|
|
896
920
|
// Only invalid agent names and no match: fall through to the default route.
|
|
897
921
|
}
|
|
898
922
|
|
|
923
|
+
let route: ResolvedDingTalkRoute;
|
|
924
|
+
if (routeOverride) {
|
|
925
|
+
route = routeOverride;
|
|
926
|
+
} else if (subAgentOptions) {
|
|
927
|
+
route = {
|
|
928
|
+
agentId: subAgentOptions.agentId,
|
|
929
|
+
sessionKey: buildAgentSessionKey({
|
|
930
|
+
rt,
|
|
931
|
+
cfg,
|
|
932
|
+
accountId,
|
|
933
|
+
agentId: subAgentOptions.agentId,
|
|
934
|
+
peerKind: sessionPeer.kind,
|
|
935
|
+
peerId: sessionPeer.peerId,
|
|
936
|
+
}),
|
|
937
|
+
mainSessionKey: "",
|
|
938
|
+
};
|
|
939
|
+
} else {
|
|
940
|
+
route = rt.channel.routing.resolveAgentRoute({
|
|
941
|
+
cfg,
|
|
942
|
+
channel: "dingtalk",
|
|
943
|
+
accountId,
|
|
944
|
+
peer: { kind: sessionPeer.kind, id: sessionPeer.peerId },
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
const questionContext = getDingTalkQuestionContext();
|
|
948
|
+
if (questionContext) {
|
|
949
|
+
questionContext.resolvedRoute = route;
|
|
950
|
+
questionContext.questionScopeKey = `${accountId}:${route.sessionKey}:${senderId}`;
|
|
951
|
+
questionContext.storePath = accountStorePath;
|
|
952
|
+
questionContext.continuationSubAgentOptions = subAgentOptions
|
|
953
|
+
? {
|
|
954
|
+
agentId: subAgentOptions.agentId,
|
|
955
|
+
responsePrefix: subAgentOptions.responsePrefix,
|
|
956
|
+
matchedName: subAgentOptions.matchedName,
|
|
957
|
+
}
|
|
958
|
+
: undefined;
|
|
959
|
+
}
|
|
960
|
+
if (!subAgentOptions) {
|
|
961
|
+
invalidateQuestionRoutes([route]);
|
|
962
|
+
}
|
|
963
|
+
|
|
899
964
|
// Route resolved before media download for session context and routing metadata.
|
|
900
965
|
const storePath = rt.channel.session.resolveStorePath(cfg.session?.store, {
|
|
901
966
|
agentId: route.agentId,
|
|
@@ -936,17 +1001,27 @@ async function handleDingTalkMessageInner(params: HandleDingTalkMessageParams):
|
|
|
936
1001
|
const content = extractedContent;
|
|
937
1002
|
const isBtwBypass = isBtwRequestText(stripLeadingMentions(content.text).trim());
|
|
938
1003
|
const taskInfoConversationId = groupId || to;
|
|
939
|
-
const
|
|
1004
|
+
const agentDisplayName = getAgentDisplayName({
|
|
1005
|
+
subAgentOptions,
|
|
1006
|
+
agentId: route.agentId,
|
|
1007
|
+
agentsList: cfg.agents?.list,
|
|
1008
|
+
});
|
|
1009
|
+
const configuredTaskMetadata = resolveConfiguredTaskModelMetadata({
|
|
1010
|
+
cfg,
|
|
1011
|
+
agentId: route.agentId,
|
|
1012
|
+
});
|
|
1013
|
+
const sessionTaskStateScope = {
|
|
1014
|
+
accountId,
|
|
1015
|
+
conversationId: taskInfoConversationId,
|
|
1016
|
+
agentId: route.agentId,
|
|
1017
|
+
};
|
|
1018
|
+
const sessionTaskState = initSessionState(sessionTaskStateScope, configuredTaskMetadata);
|
|
940
1019
|
const initialStatusLine =
|
|
941
1020
|
renderStatusLine(
|
|
942
1021
|
{
|
|
943
1022
|
model: sessionTaskState.model,
|
|
944
1023
|
effort: sessionTaskState.effort,
|
|
945
|
-
agent:
|
|
946
|
-
subAgentOptions,
|
|
947
|
-
agentId: route.agentId,
|
|
948
|
-
agentsList: cfg.agents?.list,
|
|
949
|
-
}),
|
|
1024
|
+
agent: agentDisplayName,
|
|
950
1025
|
},
|
|
951
1026
|
dingtalkConfig,
|
|
952
1027
|
) || undefined;
|
|
@@ -966,7 +1041,6 @@ async function handleDingTalkMessageInner(params: HandleDingTalkMessageParams):
|
|
|
966
1041
|
let cardFlightKey: string | undefined;
|
|
967
1042
|
if (questionContext) {
|
|
968
1043
|
questionContext.onQuestionCardSent = async ({ questionId, outTrackId }) => {
|
|
969
|
-
questionCardTookOver = true;
|
|
970
1044
|
if (cardFlightKey) {
|
|
971
1045
|
cardCreationInFlight.delete(cardFlightKey);
|
|
972
1046
|
cardFlightKey = undefined;
|
|
@@ -987,23 +1061,34 @@ async function handleDingTalkMessageInner(params: HandleDingTalkMessageParams):
|
|
|
987
1061
|
log?.warn?.(
|
|
988
1062
|
`[DingTalk][AskUser] Question card sent, but targeted stop failed question=${questionId} outTrackId=${outTrackId} targetSessionKey=${route.sessionKey}: ${err instanceof Error ? err.message : String(err)}`,
|
|
989
1063
|
);
|
|
1064
|
+
return false;
|
|
990
1065
|
}
|
|
1066
|
+
questionCardTookOver = true;
|
|
991
1067
|
if (!currentAICard) {
|
|
992
|
-
return;
|
|
1068
|
+
return true;
|
|
993
1069
|
}
|
|
994
1070
|
if (isCardInTerminalState(currentAICard.state)) {
|
|
995
|
-
return;
|
|
1071
|
+
return true;
|
|
1072
|
+
}
|
|
1073
|
+
let recalled = false;
|
|
1074
|
+
try {
|
|
1075
|
+
recalled = await recallAICardMessage(currentAICard, log);
|
|
1076
|
+
} catch (err) {
|
|
1077
|
+
log?.warn?.(
|
|
1078
|
+
`[DingTalk][AskUser] Question card took over, but AI card recall errored question=${questionId} outTrackId=${outTrackId}: ${err instanceof Error ? err.message : String(err)}`,
|
|
1079
|
+
);
|
|
1080
|
+
return true;
|
|
996
1081
|
}
|
|
997
|
-
const recalled = await recallAICardMessage(currentAICard, log);
|
|
998
1082
|
if (!recalled) {
|
|
999
1083
|
log?.warn?.(
|
|
1000
1084
|
`[DingTalk][AskUser] Question card sent, but AI card recall failed; normal replies remain suppressed question=${questionId} outTrackId=${outTrackId}`,
|
|
1001
1085
|
);
|
|
1002
|
-
return;
|
|
1086
|
+
return true;
|
|
1003
1087
|
}
|
|
1004
1088
|
log?.info?.(
|
|
1005
1089
|
`[DingTalk][AskUser] Recalled empty AI card after question card sent question=${questionId} outTrackId=${outTrackId}`,
|
|
1006
1090
|
);
|
|
1091
|
+
return true;
|
|
1007
1092
|
};
|
|
1008
1093
|
}
|
|
1009
1094
|
|
|
@@ -2175,7 +2260,7 @@ async function handleDingTalkMessageInner(params: HandleDingTalkMessageParams):
|
|
|
2175
2260
|
legacyCardStreamReasoning === undefined
|
|
2176
2261
|
? dingtalkConfig
|
|
2177
2262
|
: { ...dingtalkConfig, cardStreamReasoning: legacyCardStreamReasoning };
|
|
2178
|
-
const sessionTaskState = getSessionState(
|
|
2263
|
+
const sessionTaskState = getSessionState(sessionTaskStateScope);
|
|
2179
2264
|
const taskMeta = {
|
|
2180
2265
|
model: sessionTaskState?.model,
|
|
2181
2266
|
effort: sessionTaskState?.effort,
|
|
@@ -2183,11 +2268,7 @@ async function handleDingTalkMessageInner(params: HandleDingTalkMessageParams):
|
|
|
2183
2268
|
typeof sessionTaskState?.taskStartTime === "number"
|
|
2184
2269
|
? Math.max(0, Date.now() - sessionTaskState.taskStartTime)
|
|
2185
2270
|
: undefined,
|
|
2186
|
-
agent:
|
|
2187
|
-
subAgentOptions,
|
|
2188
|
-
agentId: route.agentId,
|
|
2189
|
-
agentsList: cfg.agents?.list,
|
|
2190
|
-
}),
|
|
2271
|
+
agent: agentDisplayName,
|
|
2191
2272
|
};
|
|
2192
2273
|
|
|
2193
2274
|
const strategy = createReplyStrategy({
|
|
@@ -2250,6 +2331,7 @@ async function handleDingTalkMessageInner(params: HandleDingTalkMessageParams):
|
|
|
2250
2331
|
mediaUrls,
|
|
2251
2332
|
audioAsVoice: extractSharedAudioAsVoice(payload, inlineReplyPayload),
|
|
2252
2333
|
kind: replyKind,
|
|
2334
|
+
isError: payload.isError === true,
|
|
2253
2335
|
isReasoning: richPayload.isReasoning === true,
|
|
2254
2336
|
});
|
|
2255
2337
|
} catch (err: unknown) {
|
|
@@ -49,6 +49,14 @@ export function createCardReplyStrategy(
|
|
|
49
49
|
ctx: ReplyStrategyContext & { card: AICardInstance; isStopRequested?: () => boolean },
|
|
50
50
|
): ReplyStrategy {
|
|
51
51
|
const { card, config, log, isStopRequested } = ctx;
|
|
52
|
+
const sessionTaskStateScope =
|
|
53
|
+
card.accountId && card.conversationId && ctx.sessionAgentId
|
|
54
|
+
? {
|
|
55
|
+
accountId: card.accountId,
|
|
56
|
+
conversationId: card.contextConversationId || card.conversationId,
|
|
57
|
+
agentId: ctx.sessionAgentId,
|
|
58
|
+
}
|
|
59
|
+
: undefined;
|
|
52
60
|
|
|
53
61
|
const buildStatusLine = (): string | undefined => {
|
|
54
62
|
if (!ctx.taskMeta) {
|
|
@@ -61,8 +69,8 @@ export function createCardReplyStrategy(
|
|
|
61
69
|
? ctx.taskMeta.usage
|
|
62
70
|
: undefined;
|
|
63
71
|
|
|
64
|
-
const sessionTaskTimeSeconds =
|
|
65
|
-
? getTaskTimeSeconds(
|
|
72
|
+
const sessionTaskTimeSeconds = sessionTaskStateScope
|
|
73
|
+
? getTaskTimeSeconds(sessionTaskStateScope)
|
|
66
74
|
: undefined;
|
|
67
75
|
const cardElapsedMs = Math.max(0, Date.now() - card.createdAt);
|
|
68
76
|
const sessionElapsedMs = typeof sessionTaskTimeSeconds === "number"
|
|
@@ -439,10 +447,12 @@ export function createCardReplyStrategy(
|
|
|
439
447
|
if (!card.accountId || !card.conversationId) {
|
|
440
448
|
return;
|
|
441
449
|
}
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
450
|
+
if (sessionTaskStateScope) {
|
|
451
|
+
updateSessionState(sessionTaskStateScope, {
|
|
452
|
+
model: selected.model,
|
|
453
|
+
effort: selected.thinkLevel,
|
|
454
|
+
});
|
|
455
|
+
}
|
|
446
456
|
if (ctx.taskMeta) {
|
|
447
457
|
ctx.taskMeta.model = selected.model;
|
|
448
458
|
ctx.taskMeta.effort = selected.thinkLevel;
|
|
@@ -470,6 +480,20 @@ export function createCardReplyStrategy(
|
|
|
470
480
|
|
|
471
481
|
// ---- final: defer to finalize, just save text ----
|
|
472
482
|
if (payload.kind === "final") {
|
|
483
|
+
// OpenClaw can append a non-terminal tool warning after a valid final.
|
|
484
|
+
// Do not let that plain-text warning replace the answer already captured by the card.
|
|
485
|
+
const hasPreservableReply = Boolean(
|
|
486
|
+
finalTextForFallback?.trim() || controller.getFinalAnswerContent().trim(),
|
|
487
|
+
) || processedMediaUrls.size > 0 || pendingNonImageMedia.length > 0;
|
|
488
|
+
if (
|
|
489
|
+
payload.isError === true
|
|
490
|
+
&& sawFinalDelivery
|
|
491
|
+
&& payload.mediaUrls.length === 0
|
|
492
|
+
&& hasPreservableReply
|
|
493
|
+
) {
|
|
494
|
+
log?.debug?.("[DingTalk][Finalize] Ignoring late error final after answer delivery");
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
473
497
|
const isFirstFinalDelivery = !sawFinalDelivery;
|
|
474
498
|
lifecycleState = "final_seen";
|
|
475
499
|
await flushPendingReasoning();
|
package/src/session-state.ts
CHANGED
|
@@ -4,36 +4,67 @@ interface SessionState {
|
|
|
4
4
|
taskStartTime: number;
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
+
interface SessionStateScope {
|
|
8
|
+
accountId: string;
|
|
9
|
+
conversationId: string;
|
|
10
|
+
agentId: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type SessionStateInitialMetadata = Partial<Pick<SessionState, "model" | "effort">>;
|
|
14
|
+
|
|
7
15
|
const sessionStore = new Map<string, SessionState>();
|
|
8
16
|
|
|
9
|
-
function sessionKey(
|
|
10
|
-
return `${accountId}:${conversationId}`;
|
|
17
|
+
function sessionKey(scope: SessionStateScope): string {
|
|
18
|
+
return `${scope.accountId}:${scope.agentId}:${scope.conversationId}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function normalizeMetadataValue(value: string | undefined): string | undefined {
|
|
22
|
+
const trimmed = typeof value === "string" ? value.trim() : "";
|
|
23
|
+
return trimmed || undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function seedMissingSessionStateMetadata(
|
|
27
|
+
state: SessionState,
|
|
28
|
+
metadata?: SessionStateInitialMetadata,
|
|
29
|
+
): void {
|
|
30
|
+
const model = normalizeMetadataValue(metadata?.model);
|
|
31
|
+
const effort = normalizeMetadataValue(metadata?.effort);
|
|
32
|
+
if (state.model === undefined && model !== undefined) {
|
|
33
|
+
state.model = model;
|
|
34
|
+
}
|
|
35
|
+
if (state.effort === undefined && effort !== undefined) {
|
|
36
|
+
state.effort = effort;
|
|
37
|
+
}
|
|
11
38
|
}
|
|
12
39
|
|
|
13
|
-
export function initSessionState(
|
|
14
|
-
|
|
40
|
+
export function initSessionState(
|
|
41
|
+
scope: SessionStateScope,
|
|
42
|
+
metadata?: SessionStateInitialMetadata,
|
|
43
|
+
): SessionState {
|
|
44
|
+
const key = sessionKey(scope);
|
|
15
45
|
const existing = sessionStore.get(key);
|
|
16
46
|
if (existing) {
|
|
17
47
|
existing.taskStartTime = Date.now();
|
|
48
|
+
seedMissingSessionStateMetadata(existing, metadata);
|
|
18
49
|
return existing;
|
|
19
50
|
}
|
|
20
51
|
const state: SessionState = {
|
|
21
52
|
taskStartTime: Date.now(),
|
|
22
53
|
};
|
|
54
|
+
seedMissingSessionStateMetadata(state, metadata);
|
|
23
55
|
sessionStore.set(key, state);
|
|
24
56
|
return state;
|
|
25
57
|
}
|
|
26
58
|
|
|
27
|
-
export function getSessionState(
|
|
28
|
-
return sessionStore.get(sessionKey(
|
|
59
|
+
export function getSessionState(scope: SessionStateScope): SessionState | undefined {
|
|
60
|
+
return sessionStore.get(sessionKey(scope));
|
|
29
61
|
}
|
|
30
62
|
|
|
31
63
|
export function updateSessionState(
|
|
32
|
-
|
|
33
|
-
conversationId: string,
|
|
64
|
+
scope: SessionStateScope,
|
|
34
65
|
patch: Partial<Pick<SessionState, "model" | "effort">>,
|
|
35
66
|
): void {
|
|
36
|
-
const state = sessionStore.get(sessionKey(
|
|
67
|
+
const state = sessionStore.get(sessionKey(scope));
|
|
37
68
|
if (!state) {
|
|
38
69
|
return;
|
|
39
70
|
}
|
|
@@ -45,16 +76,16 @@ export function updateSessionState(
|
|
|
45
76
|
}
|
|
46
77
|
}
|
|
47
78
|
|
|
48
|
-
export function getTaskTimeSeconds(
|
|
49
|
-
const state = sessionStore.get(sessionKey(
|
|
79
|
+
export function getTaskTimeSeconds(scope: SessionStateScope): number | undefined {
|
|
80
|
+
const state = sessionStore.get(sessionKey(scope));
|
|
50
81
|
if (!state) {
|
|
51
82
|
return undefined;
|
|
52
83
|
}
|
|
53
84
|
return Math.round((Date.now() - state.taskStartTime) / 1000);
|
|
54
85
|
}
|
|
55
86
|
|
|
56
|
-
export function clearSessionState(
|
|
57
|
-
sessionStore.delete(sessionKey(
|
|
87
|
+
export function clearSessionState(scope: SessionStateScope): void {
|
|
88
|
+
sessionStore.delete(sessionKey(scope));
|
|
58
89
|
}
|
|
59
90
|
|
|
60
91
|
export function clearAllSessionStatesForTest(): void {
|
|
@@ -20,9 +20,11 @@ import type {
|
|
|
20
20
|
HandleDingTalkMessageParams,
|
|
21
21
|
Logger,
|
|
22
22
|
MessageContent,
|
|
23
|
+
ResolvedDingTalkRoute,
|
|
23
24
|
} from "../types";
|
|
24
25
|
import { getErrorMessage } from "../utils";
|
|
25
26
|
import { resolveAtAgents } from "./agent-name-matcher";
|
|
27
|
+
import type { ResolvedDingTalkSessionPeer } from "../session-routing";
|
|
26
28
|
|
|
27
29
|
export class HostRoutingHelperUnavailableError extends Error {
|
|
28
30
|
constructor(
|
|
@@ -84,6 +86,11 @@ export type MessageTarget =
|
|
|
84
86
|
}
|
|
85
87
|
| { kind: "subagent-command"; agent: AgentNameMatch; commandText: string };
|
|
86
88
|
|
|
89
|
+
export interface ResolvedSubAgentTarget {
|
|
90
|
+
agent: AgentNameMatch;
|
|
91
|
+
route: ResolvedDingTalkRoute;
|
|
92
|
+
}
|
|
93
|
+
|
|
87
94
|
/**
|
|
88
95
|
* Resolve how an inbound message should be routed.
|
|
89
96
|
*
|
|
@@ -198,6 +205,8 @@ export async function dispatchSubAgents(params: {
|
|
|
198
205
|
dingtalkConfig: DingTalkConfig;
|
|
199
206
|
sessionWebhook: string;
|
|
200
207
|
extractedContent: MessageContent;
|
|
208
|
+
sessionPeer: ResolvedDingTalkSessionPeer;
|
|
209
|
+
onRoutesResolved?: (targets: readonly ResolvedSubAgentTarget[]) => void;
|
|
201
210
|
handleMessage: (params: HandleDingTalkMessageParams) => Promise<void>;
|
|
202
211
|
downloadMedia: (
|
|
203
212
|
config: DingTalkConfig,
|
|
@@ -215,11 +224,64 @@ export async function dispatchSubAgents(params: {
|
|
|
215
224
|
dingtalkConfig,
|
|
216
225
|
sessionWebhook,
|
|
217
226
|
extractedContent,
|
|
227
|
+
sessionPeer,
|
|
228
|
+
onRoutesResolved,
|
|
218
229
|
handleMessage,
|
|
219
230
|
downloadMedia: download,
|
|
220
231
|
log,
|
|
221
232
|
} = params;
|
|
222
233
|
|
|
234
|
+
let helperMissingWarningSent = false;
|
|
235
|
+
const sendHelperMissingWarning = async (): Promise<void> => {
|
|
236
|
+
if (helperMissingWarningSent) {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
helperMissingWarningSent = true;
|
|
240
|
+
try {
|
|
241
|
+
const isGroup = data.conversationType !== "1";
|
|
242
|
+
const sendOptions = isGroup ? { atUserId: data.senderId, log } : { log };
|
|
243
|
+
await sendBySession(
|
|
244
|
+
dingtalkConfig,
|
|
245
|
+
sessionWebhook,
|
|
246
|
+
"⚠️ 当前宿主版本不支持 DingTalk 子助手路由所需的 session helper,请升级 OpenClaw 后重试。",
|
|
247
|
+
sendOptions,
|
|
248
|
+
);
|
|
249
|
+
} catch (notifyError: unknown) {
|
|
250
|
+
log?.debug?.(
|
|
251
|
+
`[DingTalk] Failed to send sub-agent helper-missing notice: ${getErrorMessage(notifyError)}`,
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
let resolvedTargets: ResolvedSubAgentTarget[];
|
|
257
|
+
try {
|
|
258
|
+
const rt = getDingTalkRuntime();
|
|
259
|
+
resolvedTargets = matchedAgents.map((agent) => ({
|
|
260
|
+
agent,
|
|
261
|
+
route: {
|
|
262
|
+
agentId: agent.agentId,
|
|
263
|
+
sessionKey: buildAgentSessionKey({
|
|
264
|
+
rt,
|
|
265
|
+
cfg,
|
|
266
|
+
accountId,
|
|
267
|
+
agentId: agent.agentId,
|
|
268
|
+
peerKind: sessionPeer.kind,
|
|
269
|
+
peerId: sessionPeer.peerId,
|
|
270
|
+
}),
|
|
271
|
+
mainSessionKey: "",
|
|
272
|
+
},
|
|
273
|
+
}));
|
|
274
|
+
} catch (error) {
|
|
275
|
+
const message = getErrorMessage(error);
|
|
276
|
+
log?.error?.(`[DingTalk] Failed to resolve sub-agent routes: ${message}`);
|
|
277
|
+
if (error instanceof HostRoutingHelperUnavailableError) {
|
|
278
|
+
await sendHelperMissingWarning();
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
throw error;
|
|
282
|
+
}
|
|
283
|
+
onRoutesResolved?.(resolvedTargets);
|
|
284
|
+
|
|
223
285
|
// Pre-download media once to avoid duplication across sub-agents
|
|
224
286
|
let preDownloadedMedia:
|
|
225
287
|
| {
|
|
@@ -255,9 +317,8 @@ export async function dispatchSubAgents(params: {
|
|
|
255
317
|
};
|
|
256
318
|
}
|
|
257
319
|
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
for (const agentMatch of matchedAgents) {
|
|
320
|
+
for (const target of resolvedTargets) {
|
|
321
|
+
const agentMatch = target.agent;
|
|
261
322
|
try {
|
|
262
323
|
await handleMessage({
|
|
263
324
|
cfg,
|
|
@@ -266,6 +327,7 @@ export async function dispatchSubAgents(params: {
|
|
|
266
327
|
sessionWebhook,
|
|
267
328
|
log,
|
|
268
329
|
dingtalkConfig,
|
|
330
|
+
routeOverride: target.route,
|
|
269
331
|
subAgentOptions: {
|
|
270
332
|
agentId: agentMatch.agentId,
|
|
271
333
|
responsePrefix: commandText
|
|
@@ -279,22 +341,8 @@ export async function dispatchSubAgents(params: {
|
|
|
279
341
|
} catch (error) {
|
|
280
342
|
const message = getErrorMessage(error);
|
|
281
343
|
log?.error?.(`[DingTalk] Sub-agent ${agentMatch.agentId} failed: ${message}`);
|
|
282
|
-
if (error instanceof HostRoutingHelperUnavailableError
|
|
283
|
-
|
|
284
|
-
try {
|
|
285
|
-
const isGroup = data.conversationType !== "1";
|
|
286
|
-
const sendOptions = isGroup ? { atUserId: data.senderId, log } : { log };
|
|
287
|
-
await sendBySession(
|
|
288
|
-
dingtalkConfig,
|
|
289
|
-
sessionWebhook,
|
|
290
|
-
"⚠️ 当前宿主版本不支持 DingTalk 子助手路由所需的 session helper,请升级 OpenClaw 后重试。",
|
|
291
|
-
sendOptions,
|
|
292
|
-
);
|
|
293
|
-
} catch (notifyError: unknown) {
|
|
294
|
-
log?.debug?.(
|
|
295
|
-
`[DingTalk] Failed to send sub-agent helper-missing notice: ${getErrorMessage(notifyError)}`,
|
|
296
|
-
);
|
|
297
|
-
}
|
|
344
|
+
if (error instanceof HostRoutingHelperUnavailableError) {
|
|
345
|
+
await sendHelperMissingWarning();
|
|
298
346
|
}
|
|
299
347
|
}
|
|
300
348
|
}
|
package/src/types.ts
CHANGED
|
@@ -462,6 +462,16 @@ export interface SubAgentOptions {
|
|
|
462
462
|
commandText?: string;
|
|
463
463
|
}
|
|
464
464
|
|
|
465
|
+
/**
|
|
466
|
+
* A trusted route resolved by the plugin before recursive or synthetic inbound handling.
|
|
467
|
+
* It is runtime-only and must never be populated from DingTalk callback payload fields.
|
|
468
|
+
*/
|
|
469
|
+
export interface ResolvedDingTalkRoute {
|
|
470
|
+
agentId: string;
|
|
471
|
+
sessionKey: string;
|
|
472
|
+
mainSessionKey: string;
|
|
473
|
+
}
|
|
474
|
+
|
|
465
475
|
/**
|
|
466
476
|
* Message handler parameters
|
|
467
477
|
*/
|
|
@@ -472,6 +482,10 @@ export interface HandleDingTalkMessageParams {
|
|
|
472
482
|
sessionWebhook: string;
|
|
473
483
|
log?: Logger;
|
|
474
484
|
dingtalkConfig: DingTalkConfig;
|
|
485
|
+
/** Distinguishes real Stream messages from Ask User callback reinjection. */
|
|
486
|
+
inboundOrigin?: "stream" | "ask-user";
|
|
487
|
+
/** Reuses an already-resolved trusted route for recursive or synthetic handling. */
|
|
488
|
+
routeOverride?: ResolvedDingTalkRoute;
|
|
475
489
|
/**
|
|
476
490
|
* When set, routes message to the specified sub-agent instead of main agent.
|
|
477
491
|
* This enables reuse of the main message handling logic for sub-agents.
|