@soimy/dingtalk 3.6.6 → 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/dist/index.js +678 -108
- 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/gateway/channel-gateway.d.ts.map +1 -1
- package/dist/src/inbound-handler.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/package.json +1 -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/gateway/channel-gateway.ts +19 -0
- package/src/inbound-handler.ts +110 -37
- package/src/targeting/agent-routing.ts +67 -19
- package/src/types.ts +14 -0
package/src/inbound-handler.ts
CHANGED
|
@@ -13,6 +13,10 @@ 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,
|
|
@@ -81,7 +85,13 @@ import {
|
|
|
81
85
|
upsertObservedUserTarget,
|
|
82
86
|
} from "./targeting/target-directory-store";
|
|
83
87
|
import { AICardStatus } from "./types";
|
|
84
|
-
import type {
|
|
88
|
+
import type {
|
|
89
|
+
DingTalkConfig,
|
|
90
|
+
HandleDingTalkMessageParams,
|
|
91
|
+
Logger,
|
|
92
|
+
MediaFile,
|
|
93
|
+
ResolvedDingTalkRoute,
|
|
94
|
+
} from "./types";
|
|
85
95
|
import {
|
|
86
96
|
formatDingTalkErrorPayloadLog,
|
|
87
97
|
getErrorMessage,
|
|
@@ -584,6 +594,8 @@ async function handleDingTalkMessageInner(params: HandleDingTalkMessageParams):
|
|
|
584
594
|
sessionWebhook,
|
|
585
595
|
log,
|
|
586
596
|
dingtalkConfig,
|
|
597
|
+
inboundOrigin = "stream",
|
|
598
|
+
routeOverride,
|
|
587
599
|
subAgentOptions,
|
|
588
600
|
preDownloadedMedia,
|
|
589
601
|
} = params;
|
|
@@ -810,38 +822,42 @@ async function handleDingTalkMessageInner(params: HandleDingTalkMessageParams):
|
|
|
810
822
|
? null
|
|
811
823
|
: resolveMessageTarget({ extractedContent, cfg, isGroup });
|
|
812
824
|
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
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)}`);
|
|
839
859
|
});
|
|
840
|
-
}
|
|
841
|
-
const questionContext = getDingTalkQuestionContext();
|
|
842
|
-
if (questionContext) {
|
|
843
|
-
questionContext.questionScopeKey = `${accountId}:${route.sessionKey}:${senderId}`;
|
|
844
|
-
}
|
|
860
|
+
};
|
|
845
861
|
|
|
846
862
|
// @Sub-Agent routing: dispatch @mention-targeted messages to their agent(s).
|
|
847
863
|
// Both content (`@agent <message>`) and commands (`@agent /new`) re-enter
|
|
@@ -858,6 +874,9 @@ async function handleDingTalkMessageInner(params: HandleDingTalkMessageParams):
|
|
|
858
874
|
dingtalkConfig,
|
|
859
875
|
sessionWebhook,
|
|
860
876
|
extractedContent,
|
|
877
|
+
sessionPeer,
|
|
878
|
+
onRoutesResolved: (targets) =>
|
|
879
|
+
invalidateQuestionRoutes(targets.map((target) => target.route)),
|
|
861
880
|
handleMessage: handleDingTalkMessage,
|
|
862
881
|
downloadMedia,
|
|
863
882
|
log,
|
|
@@ -889,6 +908,9 @@ async function handleDingTalkMessageInner(params: HandleDingTalkMessageParams):
|
|
|
889
908
|
dingtalkConfig,
|
|
890
909
|
sessionWebhook,
|
|
891
910
|
extractedContent,
|
|
911
|
+
sessionPeer,
|
|
912
|
+
onRoutesResolved: (targets) =>
|
|
913
|
+
invalidateQuestionRoutes(targets.map((target) => target.route)),
|
|
892
914
|
handleMessage: handleDingTalkMessage,
|
|
893
915
|
downloadMedia,
|
|
894
916
|
log,
|
|
@@ -898,6 +920,47 @@ async function handleDingTalkMessageInner(params: HandleDingTalkMessageParams):
|
|
|
898
920
|
// Only invalid agent names and no match: fall through to the default route.
|
|
899
921
|
}
|
|
900
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
|
+
|
|
901
964
|
// Route resolved before media download for session context and routing metadata.
|
|
902
965
|
const storePath = rt.channel.session.resolveStorePath(cfg.session?.store, {
|
|
903
966
|
agentId: route.agentId,
|
|
@@ -978,7 +1041,6 @@ async function handleDingTalkMessageInner(params: HandleDingTalkMessageParams):
|
|
|
978
1041
|
let cardFlightKey: string | undefined;
|
|
979
1042
|
if (questionContext) {
|
|
980
1043
|
questionContext.onQuestionCardSent = async ({ questionId, outTrackId }) => {
|
|
981
|
-
questionCardTookOver = true;
|
|
982
1044
|
if (cardFlightKey) {
|
|
983
1045
|
cardCreationInFlight.delete(cardFlightKey);
|
|
984
1046
|
cardFlightKey = undefined;
|
|
@@ -999,23 +1061,34 @@ async function handleDingTalkMessageInner(params: HandleDingTalkMessageParams):
|
|
|
999
1061
|
log?.warn?.(
|
|
1000
1062
|
`[DingTalk][AskUser] Question card sent, but targeted stop failed question=${questionId} outTrackId=${outTrackId} targetSessionKey=${route.sessionKey}: ${err instanceof Error ? err.message : String(err)}`,
|
|
1001
1063
|
);
|
|
1064
|
+
return false;
|
|
1002
1065
|
}
|
|
1066
|
+
questionCardTookOver = true;
|
|
1003
1067
|
if (!currentAICard) {
|
|
1004
|
-
return;
|
|
1068
|
+
return true;
|
|
1005
1069
|
}
|
|
1006
1070
|
if (isCardInTerminalState(currentAICard.state)) {
|
|
1007
|
-
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;
|
|
1008
1081
|
}
|
|
1009
|
-
const recalled = await recallAICardMessage(currentAICard, log);
|
|
1010
1082
|
if (!recalled) {
|
|
1011
1083
|
log?.warn?.(
|
|
1012
1084
|
`[DingTalk][AskUser] Question card sent, but AI card recall failed; normal replies remain suppressed question=${questionId} outTrackId=${outTrackId}`,
|
|
1013
1085
|
);
|
|
1014
|
-
return;
|
|
1086
|
+
return true;
|
|
1015
1087
|
}
|
|
1016
1088
|
log?.info?.(
|
|
1017
1089
|
`[DingTalk][AskUser] Recalled empty AI card after question card sent question=${questionId} outTrackId=${outTrackId}`,
|
|
1018
1090
|
);
|
|
1091
|
+
return true;
|
|
1019
1092
|
};
|
|
1020
1093
|
}
|
|
1021
1094
|
|
|
@@ -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.
|