@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/dist/index.js
CHANGED
|
@@ -790,7 +790,7 @@ function cleanupOrphanedTempFiles(log) {
|
|
|
790
790
|
let cleaned = 0;
|
|
791
791
|
try {
|
|
792
792
|
const files = fs.readdirSync(tempDir);
|
|
793
|
-
const
|
|
793
|
+
const now2 = Date.now();
|
|
794
794
|
const maxAge = 24 * 60 * 60 * 1e3;
|
|
795
795
|
for (const file of files) {
|
|
796
796
|
if (!dingtalkPattern.test(file)) {
|
|
@@ -799,7 +799,7 @@ function cleanupOrphanedTempFiles(log) {
|
|
|
799
799
|
const filePath = path2.join(tempDir, file);
|
|
800
800
|
try {
|
|
801
801
|
const stats = fs.statSync(filePath);
|
|
802
|
-
if (
|
|
802
|
+
if (now2 - stats.mtime.getTime() > maxAge) {
|
|
803
803
|
fs.unlinkSync(filePath);
|
|
804
804
|
cleaned++;
|
|
805
805
|
log?.debug?.(`[DingTalk] Cleaned up orphaned temp file: ${file}`);
|
|
@@ -846,9 +846,9 @@ function getCurrentTimestamp() {
|
|
|
846
846
|
var accessTokenCache = /* @__PURE__ */ new Map();
|
|
847
847
|
async function getAccessToken(config, log) {
|
|
848
848
|
const cacheKey = config.clientId;
|
|
849
|
-
const
|
|
849
|
+
const now2 = Date.now();
|
|
850
850
|
const cached = accessTokenCache.get(cacheKey);
|
|
851
|
-
if (cached && cached.expiry >
|
|
851
|
+
if (cached && cached.expiry > now2 + 6e4) {
|
|
852
852
|
return cached.accessToken;
|
|
853
853
|
}
|
|
854
854
|
const runtimeConfig = await resolveRuntimeConfig(config, log);
|
|
@@ -863,7 +863,7 @@ async function getAccessToken(config, log) {
|
|
|
863
863
|
);
|
|
864
864
|
accessTokenCache.set(cacheKey, {
|
|
865
865
|
accessToken: response.data.accessToken,
|
|
866
|
-
expiry:
|
|
866
|
+
expiry: now2 + response.data.expireIn * 1e3
|
|
867
867
|
});
|
|
868
868
|
return response.data.accessToken;
|
|
869
869
|
},
|
|
@@ -1050,7 +1050,7 @@ function normalizeAllowFrom(list) {
|
|
|
1050
1050
|
function isSenderAllowed(params) {
|
|
1051
1051
|
const { allow, senderId } = params;
|
|
1052
1052
|
if (!allow.hasEntries) {
|
|
1053
|
-
return
|
|
1053
|
+
return false;
|
|
1054
1054
|
}
|
|
1055
1055
|
if (allow.hasWildcard) {
|
|
1056
1056
|
return true;
|
|
@@ -3365,9 +3365,9 @@ function ensureSweepTimer() {
|
|
|
3365
3365
|
return;
|
|
3366
3366
|
}
|
|
3367
3367
|
sweepTimer = setInterval(() => {
|
|
3368
|
-
const
|
|
3368
|
+
const now2 = Date.now();
|
|
3369
3369
|
for (const [key, record] of records) {
|
|
3370
|
-
if (
|
|
3370
|
+
if (now2 - record.registeredAt > CARD_RUN_TTL_MS) {
|
|
3371
3371
|
records.delete(key);
|
|
3372
3372
|
}
|
|
3373
3373
|
}
|
|
@@ -9789,10 +9789,61 @@ async function dispatchSubAgents(params) {
|
|
|
9789
9789
|
dingtalkConfig,
|
|
9790
9790
|
sessionWebhook,
|
|
9791
9791
|
extractedContent,
|
|
9792
|
+
sessionPeer,
|
|
9793
|
+
onRoutesResolved,
|
|
9792
9794
|
handleMessage,
|
|
9793
9795
|
downloadMedia: download,
|
|
9794
9796
|
log
|
|
9795
9797
|
} = params;
|
|
9798
|
+
let helperMissingWarningSent = false;
|
|
9799
|
+
const sendHelperMissingWarning = async () => {
|
|
9800
|
+
if (helperMissingWarningSent) {
|
|
9801
|
+
return;
|
|
9802
|
+
}
|
|
9803
|
+
helperMissingWarningSent = true;
|
|
9804
|
+
try {
|
|
9805
|
+
const isGroup = data.conversationType !== "1";
|
|
9806
|
+
const sendOptions = isGroup ? { atUserId: data.senderId, log } : { log };
|
|
9807
|
+
await sendBySession(
|
|
9808
|
+
dingtalkConfig,
|
|
9809
|
+
sessionWebhook,
|
|
9810
|
+
"\u26A0\uFE0F \u5F53\u524D\u5BBF\u4E3B\u7248\u672C\u4E0D\u652F\u6301 DingTalk \u5B50\u52A9\u624B\u8DEF\u7531\u6240\u9700\u7684 session helper\uFF0C\u8BF7\u5347\u7EA7 OpenClaw \u540E\u91CD\u8BD5\u3002",
|
|
9811
|
+
sendOptions
|
|
9812
|
+
);
|
|
9813
|
+
} catch (notifyError) {
|
|
9814
|
+
log?.debug?.(
|
|
9815
|
+
`[DingTalk] Failed to send sub-agent helper-missing notice: ${getErrorMessage(notifyError)}`
|
|
9816
|
+
);
|
|
9817
|
+
}
|
|
9818
|
+
};
|
|
9819
|
+
let resolvedTargets;
|
|
9820
|
+
try {
|
|
9821
|
+
const rt = getDingTalkRuntime();
|
|
9822
|
+
resolvedTargets = matchedAgents.map((agent) => ({
|
|
9823
|
+
agent,
|
|
9824
|
+
route: {
|
|
9825
|
+
agentId: agent.agentId,
|
|
9826
|
+
sessionKey: buildAgentSessionKey({
|
|
9827
|
+
rt,
|
|
9828
|
+
cfg,
|
|
9829
|
+
accountId,
|
|
9830
|
+
agentId: agent.agentId,
|
|
9831
|
+
peerKind: sessionPeer.kind,
|
|
9832
|
+
peerId: sessionPeer.peerId
|
|
9833
|
+
}),
|
|
9834
|
+
mainSessionKey: ""
|
|
9835
|
+
}
|
|
9836
|
+
}));
|
|
9837
|
+
} catch (error) {
|
|
9838
|
+
const message = getErrorMessage(error);
|
|
9839
|
+
log?.error?.(`[DingTalk] Failed to resolve sub-agent routes: ${message}`);
|
|
9840
|
+
if (error instanceof HostRoutingHelperUnavailableError) {
|
|
9841
|
+
await sendHelperMissingWarning();
|
|
9842
|
+
return;
|
|
9843
|
+
}
|
|
9844
|
+
throw error;
|
|
9845
|
+
}
|
|
9846
|
+
onRoutesResolved?.(resolvedTargets);
|
|
9796
9847
|
let preDownloadedMedia;
|
|
9797
9848
|
const robotCode = resolveRobotCode(dingtalkConfig);
|
|
9798
9849
|
if (robotCode) {
|
|
@@ -9815,8 +9866,8 @@ async function dispatchSubAgents(params) {
|
|
|
9815
9866
|
};
|
|
9816
9867
|
}
|
|
9817
9868
|
}
|
|
9818
|
-
|
|
9819
|
-
|
|
9869
|
+
for (const target of resolvedTargets) {
|
|
9870
|
+
const agentMatch = target.agent;
|
|
9820
9871
|
try {
|
|
9821
9872
|
await handleMessage({
|
|
9822
9873
|
cfg,
|
|
@@ -9825,6 +9876,7 @@ async function dispatchSubAgents(params) {
|
|
|
9825
9876
|
sessionWebhook,
|
|
9826
9877
|
log,
|
|
9827
9878
|
dingtalkConfig,
|
|
9879
|
+
routeOverride: target.route,
|
|
9828
9880
|
subAgentOptions: {
|
|
9829
9881
|
agentId: agentMatch.agentId,
|
|
9830
9882
|
responsePrefix: commandText ? "" : `> \u{1F916} **${sanitizeAgentName(agentMatch.matchedName)}**:
|
|
@@ -9838,22 +9890,8 @@ async function dispatchSubAgents(params) {
|
|
|
9838
9890
|
} catch (error) {
|
|
9839
9891
|
const message = getErrorMessage(error);
|
|
9840
9892
|
log?.error?.(`[DingTalk] Sub-agent ${agentMatch.agentId} failed: ${message}`);
|
|
9841
|
-
if (error instanceof HostRoutingHelperUnavailableError
|
|
9842
|
-
|
|
9843
|
-
try {
|
|
9844
|
-
const isGroup = data.conversationType !== "1";
|
|
9845
|
-
const sendOptions = isGroup ? { atUserId: data.senderId, log } : { log };
|
|
9846
|
-
await sendBySession(
|
|
9847
|
-
dingtalkConfig,
|
|
9848
|
-
sessionWebhook,
|
|
9849
|
-
"\u26A0\uFE0F \u5F53\u524D\u5BBF\u4E3B\u7248\u672C\u4E0D\u652F\u6301 DingTalk \u5B50\u52A9\u624B\u8DEF\u7531\u6240\u9700\u7684 session helper\uFF0C\u8BF7\u5347\u7EA7 OpenClaw \u540E\u91CD\u8BD5\u3002",
|
|
9850
|
-
sendOptions
|
|
9851
|
-
);
|
|
9852
|
-
} catch (notifyError) {
|
|
9853
|
-
log?.debug?.(
|
|
9854
|
-
`[DingTalk] Failed to send sub-agent helper-missing notice: ${getErrorMessage(notifyError)}`
|
|
9855
|
-
);
|
|
9856
|
-
}
|
|
9893
|
+
if (error instanceof HostRoutingHelperUnavailableError) {
|
|
9894
|
+
await sendHelperMissingWarning();
|
|
9857
9895
|
}
|
|
9858
9896
|
}
|
|
9859
9897
|
}
|
|
@@ -10555,6 +10593,8 @@ async function handleDingTalkMessageInner(params) {
|
|
|
10555
10593
|
sessionWebhook,
|
|
10556
10594
|
log,
|
|
10557
10595
|
dingtalkConfig,
|
|
10596
|
+
inboundOrigin = "stream",
|
|
10597
|
+
routeOverride,
|
|
10558
10598
|
subAgentOptions,
|
|
10559
10599
|
preDownloadedMedia
|
|
10560
10600
|
} = params;
|
|
@@ -10749,32 +10789,42 @@ async function handleDingTalkMessageInner(params) {
|
|
|
10749
10789
|
config: dingtalkConfig
|
|
10750
10790
|
});
|
|
10751
10791
|
const messageTarget = subAgentOptions ? null : resolveMessageTarget({ extractedContent, cfg, isGroup });
|
|
10752
|
-
|
|
10753
|
-
|
|
10754
|
-
|
|
10755
|
-
|
|
10756
|
-
|
|
10757
|
-
|
|
10758
|
-
|
|
10759
|
-
|
|
10760
|
-
|
|
10761
|
-
|
|
10762
|
-
|
|
10763
|
-
|
|
10764
|
-
|
|
10765
|
-
|
|
10766
|
-
|
|
10767
|
-
|
|
10768
|
-
|
|
10769
|
-
|
|
10770
|
-
|
|
10771
|
-
|
|
10792
|
+
const invalidateQuestionRoutes = (routes) => {
|
|
10793
|
+
if (inboundOrigin === "ask-user") {
|
|
10794
|
+
return;
|
|
10795
|
+
}
|
|
10796
|
+
const scopeKeys = new Set(
|
|
10797
|
+
routes.map((resolvedRoute) => `${accountId}:${resolvedRoute.sessionKey}:${senderId}`)
|
|
10798
|
+
);
|
|
10799
|
+
const invalidatedRecords = [];
|
|
10800
|
+
for (const questionScopeKey of scopeKeys) {
|
|
10801
|
+
try {
|
|
10802
|
+
invalidatedRecords.push(
|
|
10803
|
+
...invalidateAskUserQuestionsForScope({
|
|
10804
|
+
storePath: accountStorePath,
|
|
10805
|
+
accountId,
|
|
10806
|
+
questionScopeKey,
|
|
10807
|
+
reason: "superseded_by_message",
|
|
10808
|
+
log
|
|
10809
|
+
})
|
|
10810
|
+
);
|
|
10811
|
+
} catch (err) {
|
|
10812
|
+
log?.warn?.(
|
|
10813
|
+
`[DingTalk][AskUser] Failed to invalidate pending cards before inbound dispatch scope=${questionScopeKey}: ${String(err)}`
|
|
10814
|
+
);
|
|
10815
|
+
}
|
|
10816
|
+
}
|
|
10817
|
+
if (invalidatedRecords.length === 0) {
|
|
10818
|
+
return;
|
|
10819
|
+
}
|
|
10820
|
+
void syncInvalidatedAskUserQuestionCards({
|
|
10821
|
+
records: invalidatedRecords,
|
|
10822
|
+
config: dingtalkConfig,
|
|
10823
|
+
log
|
|
10824
|
+
}).catch((err) => {
|
|
10825
|
+
log?.warn?.(`[DingTalk][AskUser] Card invalidation sync failed: ${String(err)}`);
|
|
10772
10826
|
});
|
|
10773
|
-
}
|
|
10774
|
-
const questionContext = getDingTalkQuestionContext();
|
|
10775
|
-
if (questionContext) {
|
|
10776
|
-
questionContext.questionScopeKey = `${accountId}:${route.sessionKey}:${senderId}`;
|
|
10777
|
-
}
|
|
10827
|
+
};
|
|
10778
10828
|
if (messageTarget && messageTarget.kind !== "default") {
|
|
10779
10829
|
if (messageTarget.kind === "subagent-command") {
|
|
10780
10830
|
await dispatchSubAgents({
|
|
@@ -10786,6 +10836,8 @@ async function handleDingTalkMessageInner(params) {
|
|
|
10786
10836
|
dingtalkConfig,
|
|
10787
10837
|
sessionWebhook,
|
|
10788
10838
|
extractedContent,
|
|
10839
|
+
sessionPeer,
|
|
10840
|
+
onRoutesResolved: (targets) => invalidateQuestionRoutes(targets.map((target) => target.route)),
|
|
10789
10841
|
handleMessage: handleDingTalkMessage,
|
|
10790
10842
|
downloadMedia,
|
|
10791
10843
|
log
|
|
@@ -10814,6 +10866,8 @@ async function handleDingTalkMessageInner(params) {
|
|
|
10814
10866
|
dingtalkConfig,
|
|
10815
10867
|
sessionWebhook,
|
|
10816
10868
|
extractedContent,
|
|
10869
|
+
sessionPeer,
|
|
10870
|
+
onRoutesResolved: (targets) => invalidateQuestionRoutes(targets.map((target) => target.route)),
|
|
10817
10871
|
handleMessage: handleDingTalkMessage,
|
|
10818
10872
|
downloadMedia,
|
|
10819
10873
|
log
|
|
@@ -10821,6 +10875,44 @@ async function handleDingTalkMessageInner(params) {
|
|
|
10821
10875
|
return;
|
|
10822
10876
|
}
|
|
10823
10877
|
}
|
|
10878
|
+
let route;
|
|
10879
|
+
if (routeOverride) {
|
|
10880
|
+
route = routeOverride;
|
|
10881
|
+
} else if (subAgentOptions) {
|
|
10882
|
+
route = {
|
|
10883
|
+
agentId: subAgentOptions.agentId,
|
|
10884
|
+
sessionKey: buildAgentSessionKey({
|
|
10885
|
+
rt,
|
|
10886
|
+
cfg,
|
|
10887
|
+
accountId,
|
|
10888
|
+
agentId: subAgentOptions.agentId,
|
|
10889
|
+
peerKind: sessionPeer.kind,
|
|
10890
|
+
peerId: sessionPeer.peerId
|
|
10891
|
+
}),
|
|
10892
|
+
mainSessionKey: ""
|
|
10893
|
+
};
|
|
10894
|
+
} else {
|
|
10895
|
+
route = rt.channel.routing.resolveAgentRoute({
|
|
10896
|
+
cfg,
|
|
10897
|
+
channel: "dingtalk",
|
|
10898
|
+
accountId,
|
|
10899
|
+
peer: { kind: sessionPeer.kind, id: sessionPeer.peerId }
|
|
10900
|
+
});
|
|
10901
|
+
}
|
|
10902
|
+
const questionContext = getDingTalkQuestionContext();
|
|
10903
|
+
if (questionContext) {
|
|
10904
|
+
questionContext.resolvedRoute = route;
|
|
10905
|
+
questionContext.questionScopeKey = `${accountId}:${route.sessionKey}:${senderId}`;
|
|
10906
|
+
questionContext.storePath = accountStorePath;
|
|
10907
|
+
questionContext.continuationSubAgentOptions = subAgentOptions ? {
|
|
10908
|
+
agentId: subAgentOptions.agentId,
|
|
10909
|
+
responsePrefix: subAgentOptions.responsePrefix,
|
|
10910
|
+
matchedName: subAgentOptions.matchedName
|
|
10911
|
+
} : void 0;
|
|
10912
|
+
}
|
|
10913
|
+
if (!subAgentOptions) {
|
|
10914
|
+
invalidateQuestionRoutes([route]);
|
|
10915
|
+
}
|
|
10824
10916
|
const storePath = rt.channel.session.resolveStorePath(cfg.session?.store, {
|
|
10825
10917
|
agentId: route.agentId
|
|
10826
10918
|
});
|
|
@@ -10887,7 +10979,6 @@ async function handleDingTalkMessageInner(params) {
|
|
|
10887
10979
|
let cardFlightKey;
|
|
10888
10980
|
if (questionContext) {
|
|
10889
10981
|
questionContext.onQuestionCardSent = async ({ questionId, outTrackId }) => {
|
|
10890
|
-
questionCardTookOver = true;
|
|
10891
10982
|
if (cardFlightKey) {
|
|
10892
10983
|
cardCreationInFlight.delete(cardFlightKey);
|
|
10893
10984
|
cardFlightKey = void 0;
|
|
@@ -10908,23 +10999,34 @@ async function handleDingTalkMessageInner(params) {
|
|
|
10908
10999
|
log?.warn?.(
|
|
10909
11000
|
`[DingTalk][AskUser] Question card sent, but targeted stop failed question=${questionId} outTrackId=${outTrackId} targetSessionKey=${route.sessionKey}: ${err instanceof Error ? err.message : String(err)}`
|
|
10910
11001
|
);
|
|
11002
|
+
return false;
|
|
10911
11003
|
}
|
|
11004
|
+
questionCardTookOver = true;
|
|
10912
11005
|
if (!currentAICard) {
|
|
10913
|
-
return;
|
|
11006
|
+
return true;
|
|
10914
11007
|
}
|
|
10915
11008
|
if (isCardInTerminalState(currentAICard.state)) {
|
|
10916
|
-
return;
|
|
11009
|
+
return true;
|
|
11010
|
+
}
|
|
11011
|
+
let recalled = false;
|
|
11012
|
+
try {
|
|
11013
|
+
recalled = await recallAICardMessage(currentAICard, log);
|
|
11014
|
+
} catch (err) {
|
|
11015
|
+
log?.warn?.(
|
|
11016
|
+
`[DingTalk][AskUser] Question card took over, but AI card recall errored question=${questionId} outTrackId=${outTrackId}: ${err instanceof Error ? err.message : String(err)}`
|
|
11017
|
+
);
|
|
11018
|
+
return true;
|
|
10917
11019
|
}
|
|
10918
|
-
const recalled = await recallAICardMessage(currentAICard, log);
|
|
10919
11020
|
if (!recalled) {
|
|
10920
11021
|
log?.warn?.(
|
|
10921
11022
|
`[DingTalk][AskUser] Question card sent, but AI card recall failed; normal replies remain suppressed question=${questionId} outTrackId=${outTrackId}`
|
|
10922
11023
|
);
|
|
10923
|
-
return;
|
|
11024
|
+
return true;
|
|
10924
11025
|
}
|
|
10925
11026
|
log?.info?.(
|
|
10926
11027
|
`[DingTalk][AskUser] Recalled empty AI card after question card sent question=${questionId} outTrackId=${outTrackId}`
|
|
10927
11028
|
);
|
|
11029
|
+
return true;
|
|
10928
11030
|
};
|
|
10929
11031
|
}
|
|
10930
11032
|
if (useCardMode && !isBtwBypass) {
|
|
@@ -11975,6 +12077,194 @@ ${attachmentExtractedText}` : inboundBody;
|
|
|
11975
12077
|
}
|
|
11976
12078
|
}
|
|
11977
12079
|
|
|
12080
|
+
// src/card/ask-user-question-store.ts
|
|
12081
|
+
var ASK_USER_LIFECYCLE_NAMESPACE = "cards.ask-user.lifecycle";
|
|
12082
|
+
var ACTIVE_TTL_MS = 5 * 60 * 1e3;
|
|
12083
|
+
var TOMBSTONE_TTL_MS = 30 * 60 * 1e3;
|
|
12084
|
+
function now(options) {
|
|
12085
|
+
return options.now?.() ?? Date.now();
|
|
12086
|
+
}
|
|
12087
|
+
function emptyState(timestamp) {
|
|
12088
|
+
return { version: 1, updatedAt: timestamp, records: [] };
|
|
12089
|
+
}
|
|
12090
|
+
function isActiveState(state) {
|
|
12091
|
+
return state === "reserved" || state === "pending" || state === "dispatching";
|
|
12092
|
+
}
|
|
12093
|
+
function isAnswerableState(state) {
|
|
12094
|
+
return state === "reserved" || state === "pending";
|
|
12095
|
+
}
|
|
12096
|
+
function loadState2(options) {
|
|
12097
|
+
const timestamp = now(options);
|
|
12098
|
+
const state = readNamespaceJson(ASK_USER_LIFECYCLE_NAMESPACE, {
|
|
12099
|
+
storePath: options.storePath,
|
|
12100
|
+
scope: { accountId: options.accountId },
|
|
12101
|
+
fallback: emptyState(timestamp),
|
|
12102
|
+
log: options.log
|
|
12103
|
+
});
|
|
12104
|
+
if (state.version !== 1 || !Array.isArray(state.records)) {
|
|
12105
|
+
return emptyState(timestamp);
|
|
12106
|
+
}
|
|
12107
|
+
return state;
|
|
12108
|
+
}
|
|
12109
|
+
function persistState(options, state) {
|
|
12110
|
+
state.updatedAt = now(options);
|
|
12111
|
+
writeNamespaceJsonAtomic(ASK_USER_LIFECYCLE_NAMESPACE, {
|
|
12112
|
+
storePath: options.storePath,
|
|
12113
|
+
scope: { accountId: options.accountId },
|
|
12114
|
+
data: state,
|
|
12115
|
+
log: options.log
|
|
12116
|
+
});
|
|
12117
|
+
}
|
|
12118
|
+
function toTerminal(record, reason, timestamp) {
|
|
12119
|
+
record.state = "terminal";
|
|
12120
|
+
record.terminalReason = reason;
|
|
12121
|
+
record.updatedAt = timestamp;
|
|
12122
|
+
record.expiresAt = timestamp + TOMBSTONE_TTL_MS;
|
|
12123
|
+
return record;
|
|
12124
|
+
}
|
|
12125
|
+
function cleanupState(state, timestamp) {
|
|
12126
|
+
let changed = false;
|
|
12127
|
+
for (const record of state.records) {
|
|
12128
|
+
if (isAnswerableState(record.state) && record.expiresAt <= timestamp) {
|
|
12129
|
+
toTerminal(record, "expired", timestamp);
|
|
12130
|
+
changed = true;
|
|
12131
|
+
}
|
|
12132
|
+
}
|
|
12133
|
+
const retained = state.records.filter(
|
|
12134
|
+
(record) => record.state !== "terminal" || record.expiresAt > timestamp
|
|
12135
|
+
);
|
|
12136
|
+
if (retained.length !== state.records.length) {
|
|
12137
|
+
state.records = retained;
|
|
12138
|
+
changed = true;
|
|
12139
|
+
}
|
|
12140
|
+
return changed;
|
|
12141
|
+
}
|
|
12142
|
+
function readCleanState(options) {
|
|
12143
|
+
const state = loadState2(options);
|
|
12144
|
+
if (cleanupState(state, now(options))) {
|
|
12145
|
+
persistState(options, state);
|
|
12146
|
+
}
|
|
12147
|
+
return state;
|
|
12148
|
+
}
|
|
12149
|
+
function findRecord(state, identifier) {
|
|
12150
|
+
if (identifier.outTrackId) {
|
|
12151
|
+
const byTrackId = state.records.find((record) => record.outTrackId === identifier.outTrackId);
|
|
12152
|
+
if (byTrackId) {
|
|
12153
|
+
return byTrackId;
|
|
12154
|
+
}
|
|
12155
|
+
}
|
|
12156
|
+
if (identifier.questionId) {
|
|
12157
|
+
return state.records.find((record) => record.questionId === identifier.questionId);
|
|
12158
|
+
}
|
|
12159
|
+
return void 0;
|
|
12160
|
+
}
|
|
12161
|
+
function reserveAskUserQuestion(options, input) {
|
|
12162
|
+
const timestamp = now(options);
|
|
12163
|
+
const state = readCleanState(options);
|
|
12164
|
+
const record = {
|
|
12165
|
+
questionId: input.questionId,
|
|
12166
|
+
accountId: options.accountId,
|
|
12167
|
+
questionScopeKey: input.questionScopeKey,
|
|
12168
|
+
outTrackId: input.outTrackId,
|
|
12169
|
+
title: input.title,
|
|
12170
|
+
state: "reserved",
|
|
12171
|
+
createdAt: timestamp,
|
|
12172
|
+
updatedAt: timestamp,
|
|
12173
|
+
expiresAt: timestamp + ACTIVE_TTL_MS
|
|
12174
|
+
};
|
|
12175
|
+
state.records = state.records.filter(
|
|
12176
|
+
(item) => item.questionId !== input.questionId && item.outTrackId !== input.outTrackId
|
|
12177
|
+
);
|
|
12178
|
+
state.records.push(record);
|
|
12179
|
+
persistState(options, state);
|
|
12180
|
+
return { ...record };
|
|
12181
|
+
}
|
|
12182
|
+
function activateAskUserQuestion(options, questionId) {
|
|
12183
|
+
const timestamp = now(options);
|
|
12184
|
+
const state = readCleanState(options);
|
|
12185
|
+
const record = findRecord(state, { questionId });
|
|
12186
|
+
if (!record || record.state !== "reserved") {
|
|
12187
|
+
return { record: record ? { ...record } : void 0, superseded: [] };
|
|
12188
|
+
}
|
|
12189
|
+
const superseded = [];
|
|
12190
|
+
for (const candidate of state.records) {
|
|
12191
|
+
if (candidate !== record && isAnswerableState(candidate.state) && candidate.questionScopeKey === record.questionScopeKey) {
|
|
12192
|
+
toTerminal(candidate, "superseded_by_question", timestamp);
|
|
12193
|
+
superseded.push({ ...candidate });
|
|
12194
|
+
}
|
|
12195
|
+
}
|
|
12196
|
+
record.state = "pending";
|
|
12197
|
+
record.updatedAt = timestamp;
|
|
12198
|
+
record.expiresAt = timestamp + ACTIVE_TTL_MS;
|
|
12199
|
+
persistState(options, state);
|
|
12200
|
+
return { record: { ...record }, superseded };
|
|
12201
|
+
}
|
|
12202
|
+
function claimAskUserQuestion(options, identifier) {
|
|
12203
|
+
const timestamp = now(options);
|
|
12204
|
+
const state = readCleanState(options);
|
|
12205
|
+
const record = findRecord(state, identifier);
|
|
12206
|
+
if (!record || record.state !== "pending") {
|
|
12207
|
+
return void 0;
|
|
12208
|
+
}
|
|
12209
|
+
record.state = "dispatching";
|
|
12210
|
+
record.updatedAt = timestamp;
|
|
12211
|
+
record.expiresAt = timestamp + ACTIVE_TTL_MS;
|
|
12212
|
+
persistState(options, state);
|
|
12213
|
+
return { ...record };
|
|
12214
|
+
}
|
|
12215
|
+
function terminateAskUserQuestion(options, questionId, reason) {
|
|
12216
|
+
const timestamp = now(options);
|
|
12217
|
+
const state = readCleanState(options);
|
|
12218
|
+
const record = findRecord(state, { questionId });
|
|
12219
|
+
if (!record || !isActiveState(record.state)) {
|
|
12220
|
+
return void 0;
|
|
12221
|
+
}
|
|
12222
|
+
toTerminal(record, reason, timestamp);
|
|
12223
|
+
persistState(options, state);
|
|
12224
|
+
return { ...record };
|
|
12225
|
+
}
|
|
12226
|
+
function invalidateAskUserQuestionsInScope(options, questionScopeKey, reason) {
|
|
12227
|
+
const timestamp = now(options);
|
|
12228
|
+
const state = readCleanState(options);
|
|
12229
|
+
const invalidated = [];
|
|
12230
|
+
for (const record of state.records) {
|
|
12231
|
+
if (isAnswerableState(record.state) && record.questionScopeKey === questionScopeKey) {
|
|
12232
|
+
toTerminal(record, reason, timestamp);
|
|
12233
|
+
invalidated.push({ ...record });
|
|
12234
|
+
}
|
|
12235
|
+
}
|
|
12236
|
+
if (invalidated.length > 0) {
|
|
12237
|
+
persistState(options, state);
|
|
12238
|
+
}
|
|
12239
|
+
return invalidated;
|
|
12240
|
+
}
|
|
12241
|
+
function resolveAskUserQuestion(options, identifier) {
|
|
12242
|
+
const record = findRecord(readCleanState(options), identifier);
|
|
12243
|
+
return record ? { ...record } : void 0;
|
|
12244
|
+
}
|
|
12245
|
+
function recoverAskUserQuestionsAfterRestart(options) {
|
|
12246
|
+
const timestamp = now(options);
|
|
12247
|
+
const state = loadState2(options);
|
|
12248
|
+
const retained = state.records.filter(
|
|
12249
|
+
(record) => record.state !== "terminal" || record.expiresAt > timestamp
|
|
12250
|
+
);
|
|
12251
|
+
const removedExpiredTombstones = retained.length !== state.records.length;
|
|
12252
|
+
state.records = retained;
|
|
12253
|
+
const recovered = [];
|
|
12254
|
+
for (const record of state.records) {
|
|
12255
|
+
if (!isActiveState(record.state)) {
|
|
12256
|
+
continue;
|
|
12257
|
+
}
|
|
12258
|
+
const reason = record.state === "dispatching" ? "restart_during_dispatch" : "restart_invalidated";
|
|
12259
|
+
toTerminal(record, reason, timestamp);
|
|
12260
|
+
recovered.push({ ...record });
|
|
12261
|
+
}
|
|
12262
|
+
if (recovered.length > 0 || removedExpiredTombstones) {
|
|
12263
|
+
persistState(options, state);
|
|
12264
|
+
}
|
|
12265
|
+
return recovered;
|
|
12266
|
+
}
|
|
12267
|
+
|
|
11978
12268
|
// src/card/ask-user-question.ts
|
|
11979
12269
|
var DINGTALK_API4 = "https://api.dingtalk.com";
|
|
11980
12270
|
var PENDING_QUESTION_TTL_MS = 5 * 60 * 1e3;
|
|
@@ -12244,9 +12534,11 @@ function supersedePendingQuestionsInScope(ctx) {
|
|
|
12244
12534
|
});
|
|
12245
12535
|
}
|
|
12246
12536
|
}
|
|
12247
|
-
function storePendingQuestion(ctx) {
|
|
12537
|
+
function storePendingQuestion(ctx, options = {}) {
|
|
12248
12538
|
ctx.ownerUserId = resolvePendingQuestionOwner(ctx);
|
|
12249
|
-
|
|
12539
|
+
if (options.supersedeExisting !== false) {
|
|
12540
|
+
supersedePendingQuestionsInScope(ctx);
|
|
12541
|
+
}
|
|
12250
12542
|
pendingQuestionsByTrackId.set(ctx.outTrackId, ctx);
|
|
12251
12543
|
pendingQuestionsByQuestionId.set(ctx.questionId, ctx);
|
|
12252
12544
|
addScopeIndex(ctx);
|
|
@@ -12254,7 +12546,9 @@ function storePendingQuestion(ctx) {
|
|
|
12254
12546
|
if (!pendingQuestionsByTrackId.has(ctx.outTrackId) || ctx.submitted) {
|
|
12255
12547
|
return;
|
|
12256
12548
|
}
|
|
12257
|
-
ctx
|
|
12549
|
+
if (!claimPendingQuestionForDispatch(ctx)) {
|
|
12550
|
+
return;
|
|
12551
|
+
}
|
|
12258
12552
|
consumePendingQuestion(ctx);
|
|
12259
12553
|
addHandledQuestionTombstone(ctx, "expired");
|
|
12260
12554
|
void updateQuestionCardBestEffort(ctx, {
|
|
@@ -12262,14 +12556,12 @@ function storePendingQuestion(ctx) {
|
|
|
12262
12556
|
question_desc: "\u95EE\u9898\u5DF2\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u53D1\u8D77\u3002",
|
|
12263
12557
|
form_btn_text: "\u5DF2\u5931\u6548"
|
|
12264
12558
|
});
|
|
12265
|
-
|
|
12266
|
-
|
|
12267
|
-
|
|
12268
|
-
|
|
12269
|
-
|
|
12270
|
-
|
|
12271
|
-
}
|
|
12272
|
-
);
|
|
12559
|
+
dispatchSyntheticAnswer({
|
|
12560
|
+
ctx,
|
|
12561
|
+
text: buildExpiredAnswerMessage(ctx),
|
|
12562
|
+
suffix: "expired",
|
|
12563
|
+
successReason: "expired",
|
|
12564
|
+
log: ctx.log
|
|
12273
12565
|
});
|
|
12274
12566
|
}, PENDING_QUESTION_TTL_MS);
|
|
12275
12567
|
}
|
|
@@ -12294,6 +12586,123 @@ async function updateQuestionCardBestEffort(ctx, variables) {
|
|
|
12294
12586
|
);
|
|
12295
12587
|
}
|
|
12296
12588
|
}
|
|
12589
|
+
function getAskUserStoreOptions(params) {
|
|
12590
|
+
if (!params.storePath) {
|
|
12591
|
+
return void 0;
|
|
12592
|
+
}
|
|
12593
|
+
return {
|
|
12594
|
+
storePath: params.storePath,
|
|
12595
|
+
accountId: params.accountId,
|
|
12596
|
+
log: params.log
|
|
12597
|
+
};
|
|
12598
|
+
}
|
|
12599
|
+
function terminalCardVariables(reason) {
|
|
12600
|
+
const descriptions = {
|
|
12601
|
+
delivery_failed: "\u95EE\u9898\u5361\u7247\u53D1\u9001\u5931\u8D25\u3002",
|
|
12602
|
+
superseded_by_question: "\u5DF2\u6709\u65B0\u7684\u95EE\u9898\u5361\u7247\uFF0C\u8BF7\u56DE\u7B54\u6700\u65B0\u5361\u7247\u3002",
|
|
12603
|
+
superseded_by_message: "\u4F60\u5728\u95EE\u9898\u5361\u7247\u53D1\u51FA\u540E\u53D1\u9001\u4E86\u65B0\u6D88\u606F\uFF0C\u6B64\u5361\u5DF2\u5931\u6548\u3002\u8BF7\u91CD\u65B0\u53D1\u8D77\u9700\u8981\u586B\u5199\u7684\u95EE\u9898\u3002",
|
|
12604
|
+
expired: "\u95EE\u9898\u5DF2\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u53D1\u8D77\u3002",
|
|
12605
|
+
cancelled: "\u5DF2\u53D6\u6D88\u3002",
|
|
12606
|
+
empty: "\u5DF2\u63D0\u4EA4\uFF0C\u672A\u586B\u5199\u4EFB\u4F55\u5185\u5BB9\u3002",
|
|
12607
|
+
submitted: "\u5DF2\u63D0\u4EA4\u3002",
|
|
12608
|
+
pause_failed: "\u5F53\u524D\u4EFB\u52A1\u672A\u80FD\u6682\u505C\uFF0C\u6B64\u5361\u5DF2\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u53D1\u8D77\u3002",
|
|
12609
|
+
restart_invalidated: "\u670D\u52A1\u5DF2\u91CD\u542F\uFF0C\u539F\u95EE\u9898\u4E0A\u4E0B\u6587\u5DF2\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u53D1\u8D77\u3002",
|
|
12610
|
+
restart_during_dispatch: "\u670D\u52A1\u5728\u5904\u7406\u56DE\u7B54\u671F\u95F4\u91CD\u542F\uFF0C\u672C\u6B21\u5904\u7406\u7ED3\u679C\u53EF\u80FD\u672A\u5B8C\u6210\uFF0C\u8BF7\u53D1\u9001\u65B0\u6D88\u606F\u7EE7\u7EED\u3002",
|
|
12611
|
+
dispatch_failed: "\u56DE\u7B54\u5DF2\u6536\u5230\uFF0C\u4F46\u672A\u80FD\u7EE7\u7EED\u4F1A\u8BDD\uFF0C\u8BF7\u53D1\u9001\u4E00\u6761\u666E\u901A\u6D88\u606F\u7EE7\u7EED\u3002"
|
|
12612
|
+
};
|
|
12613
|
+
return {
|
|
12614
|
+
card_status: reason === "cancelled" ? "cancelled" : reason === "submitted" || reason === "empty" ? "submitted" : "expired",
|
|
12615
|
+
question_desc: descriptions[reason],
|
|
12616
|
+
form_btn_text: reason === "cancelled" ? "\u5DF2\u53D6\u6D88" : reason === "submitted" || reason === "empty" ? "\u5DF2\u63D0\u4EA4" : "\u5DF2\u5931\u6548"
|
|
12617
|
+
};
|
|
12618
|
+
}
|
|
12619
|
+
async function updateLifecycleRecordCardBestEffort(params) {
|
|
12620
|
+
const reason = params.record.terminalReason;
|
|
12621
|
+
if (!reason || reason === "delivery_failed") {
|
|
12622
|
+
return;
|
|
12623
|
+
}
|
|
12624
|
+
try {
|
|
12625
|
+
const token = await getAccessToken(params.config, params.log);
|
|
12626
|
+
await updateCardVariables(
|
|
12627
|
+
params.record.outTrackId,
|
|
12628
|
+
terminalCardVariables(reason),
|
|
12629
|
+
token,
|
|
12630
|
+
params.config
|
|
12631
|
+
);
|
|
12632
|
+
} catch (err) {
|
|
12633
|
+
params.log?.warn?.(
|
|
12634
|
+
`[DingTalk][AskUser] Failed to update lifecycle card ${params.record.questionId}: ${String(err)}`
|
|
12635
|
+
);
|
|
12636
|
+
}
|
|
12637
|
+
}
|
|
12638
|
+
function consumeLifecyclePendingContext(record) {
|
|
12639
|
+
const ctx = pendingQuestionsByQuestionId.get(record.questionId) ?? pendingQuestionsByTrackId.get(record.outTrackId);
|
|
12640
|
+
if (!ctx) {
|
|
12641
|
+
return void 0;
|
|
12642
|
+
}
|
|
12643
|
+
ctx.submitted = true;
|
|
12644
|
+
consumePendingQuestion(ctx);
|
|
12645
|
+
addHandledQuestionTombstone(ctx, record.terminalReason === "expired" ? "expired" : "superseded");
|
|
12646
|
+
return ctx;
|
|
12647
|
+
}
|
|
12648
|
+
function invalidateAskUserQuestionsForScope(params) {
|
|
12649
|
+
const invalidated = invalidateAskUserQuestionsInScope(
|
|
12650
|
+
{
|
|
12651
|
+
storePath: params.storePath,
|
|
12652
|
+
accountId: params.accountId,
|
|
12653
|
+
log: params.log
|
|
12654
|
+
},
|
|
12655
|
+
params.questionScopeKey,
|
|
12656
|
+
params.reason
|
|
12657
|
+
);
|
|
12658
|
+
for (const record of invalidated) {
|
|
12659
|
+
consumeLifecyclePendingContext(record);
|
|
12660
|
+
}
|
|
12661
|
+
return invalidated;
|
|
12662
|
+
}
|
|
12663
|
+
async function syncInvalidatedAskUserQuestionCards(params) {
|
|
12664
|
+
await Promise.allSettled(
|
|
12665
|
+
params.records.map(
|
|
12666
|
+
(record) => updateLifecycleRecordCardBestEffort({
|
|
12667
|
+
record,
|
|
12668
|
+
config: params.config,
|
|
12669
|
+
log: params.log
|
|
12670
|
+
})
|
|
12671
|
+
)
|
|
12672
|
+
);
|
|
12673
|
+
}
|
|
12674
|
+
async function recoverAskUserQuestionsForAccount(params) {
|
|
12675
|
+
if (!params.storePath) {
|
|
12676
|
+
return 0;
|
|
12677
|
+
}
|
|
12678
|
+
const recovered = recoverAskUserQuestionsAfterRestart({
|
|
12679
|
+
storePath: params.storePath,
|
|
12680
|
+
accountId: params.accountId,
|
|
12681
|
+
log: params.log
|
|
12682
|
+
});
|
|
12683
|
+
for (const record of recovered) {
|
|
12684
|
+
consumeLifecyclePendingContext(record);
|
|
12685
|
+
await updateLifecycleRecordCardBestEffort({
|
|
12686
|
+
record,
|
|
12687
|
+
config: params.config,
|
|
12688
|
+
log: params.log
|
|
12689
|
+
});
|
|
12690
|
+
}
|
|
12691
|
+
return recovered.length;
|
|
12692
|
+
}
|
|
12693
|
+
async function terminatePendingQuestion(params) {
|
|
12694
|
+
const storeOptions = getAskUserStoreOptions(params.ctx);
|
|
12695
|
+
if (storeOptions) {
|
|
12696
|
+
terminateAskUserQuestion(storeOptions, params.ctx.questionId, params.reason);
|
|
12697
|
+
}
|
|
12698
|
+
params.ctx.submitted = true;
|
|
12699
|
+
consumePendingQuestion(params.ctx);
|
|
12700
|
+
addHandledQuestionTombstone(
|
|
12701
|
+
params.ctx,
|
|
12702
|
+
params.reason === "cancelled" ? "cancelled" : params.reason === "empty" ? "empty" : params.reason === "submitted" ? "submitted" : params.reason === "expired" ? "expired" : "superseded"
|
|
12703
|
+
);
|
|
12704
|
+
await updateQuestionCardBestEffort(params.ctx, terminalCardVariables(params.reason));
|
|
12705
|
+
}
|
|
12297
12706
|
function parseEmbeddedJson2(value) {
|
|
12298
12707
|
if (typeof value !== "string") {
|
|
12299
12708
|
return value;
|
|
@@ -12413,7 +12822,42 @@ async function injectAnswerSyntheticMessage(ctx, text, suffix) {
|
|
|
12413
12822
|
data: syntheticData,
|
|
12414
12823
|
sessionWebhook: ctx.sessionWebhook,
|
|
12415
12824
|
log: ctx.log,
|
|
12416
|
-
dingtalkConfig: ctx.dingtalkConfig
|
|
12825
|
+
dingtalkConfig: ctx.dingtalkConfig,
|
|
12826
|
+
inboundOrigin: "ask-user",
|
|
12827
|
+
routeOverride: ctx.resolvedRoute,
|
|
12828
|
+
subAgentOptions: ctx.continuationSubAgentOptions
|
|
12829
|
+
});
|
|
12830
|
+
}
|
|
12831
|
+
function claimPendingQuestionForDispatch(ctx) {
|
|
12832
|
+
const storeOptions = getAskUserStoreOptions(ctx);
|
|
12833
|
+
if (storeOptions) {
|
|
12834
|
+
return Boolean(
|
|
12835
|
+
claimAskUserQuestion(storeOptions, {
|
|
12836
|
+
questionId: ctx.questionId,
|
|
12837
|
+
outTrackId: ctx.outTrackId
|
|
12838
|
+
})
|
|
12839
|
+
);
|
|
12840
|
+
}
|
|
12841
|
+
if (ctx.submitted) {
|
|
12842
|
+
return false;
|
|
12843
|
+
}
|
|
12844
|
+
ctx.submitted = true;
|
|
12845
|
+
return true;
|
|
12846
|
+
}
|
|
12847
|
+
function dispatchSyntheticAnswer(params) {
|
|
12848
|
+
const storeOptions = getAskUserStoreOptions(params.ctx);
|
|
12849
|
+
void injectAnswerSyntheticMessage(params.ctx, params.text, params.suffix).then(() => {
|
|
12850
|
+
if (storeOptions) {
|
|
12851
|
+
terminateAskUserQuestion(storeOptions, params.ctx.questionId, params.successReason);
|
|
12852
|
+
}
|
|
12853
|
+
}).catch((err) => {
|
|
12854
|
+
if (storeOptions) {
|
|
12855
|
+
terminateAskUserQuestion(storeOptions, params.ctx.questionId, "dispatch_failed");
|
|
12856
|
+
}
|
|
12857
|
+
void updateQuestionCardBestEffort(params.ctx, terminalCardVariables("dispatch_failed"));
|
|
12858
|
+
params.log?.error?.(
|
|
12859
|
+
`[DingTalk][AskUser] Failed to inject ${params.suffix} answer message: ${String(err)}`
|
|
12860
|
+
);
|
|
12417
12861
|
});
|
|
12418
12862
|
}
|
|
12419
12863
|
async function handleDingTalkAskUserCardCallback(params) {
|
|
@@ -12425,8 +12869,43 @@ async function handleDingTalkAskUserCardCallback(params) {
|
|
|
12425
12869
|
);
|
|
12426
12870
|
return { handled: true };
|
|
12427
12871
|
}
|
|
12872
|
+
const storeOptions = params.storePath ? {
|
|
12873
|
+
storePath: params.storePath,
|
|
12874
|
+
accountId: params.accountId,
|
|
12875
|
+
log: params.log
|
|
12876
|
+
} : void 0;
|
|
12877
|
+
const lifecycleRecord = storeOptions ? resolveAskUserQuestion(storeOptions, {
|
|
12878
|
+
questionId: parsed.actionId,
|
|
12879
|
+
outTrackId: parsed.outTrackId
|
|
12880
|
+
}) : void 0;
|
|
12881
|
+
if (lifecycleRecord?.state === "terminal") {
|
|
12882
|
+
params.log?.debug?.(
|
|
12883
|
+
`[DingTalk][AskUser] Ignoring terminal callback question=${lifecycleRecord.questionId} reason=${lifecycleRecord.terminalReason ?? "unknown"}`
|
|
12884
|
+
);
|
|
12885
|
+
await updateLifecycleRecordCardBestEffort({
|
|
12886
|
+
record: lifecycleRecord,
|
|
12887
|
+
config: params.config,
|
|
12888
|
+
log: params.log
|
|
12889
|
+
});
|
|
12890
|
+
return { handled: true };
|
|
12891
|
+
}
|
|
12428
12892
|
const ctx = (parsed.outTrackId ? pendingQuestionsByTrackId.get(parsed.outTrackId) : void 0) ?? (parsed.actionId ? pendingQuestionsByQuestionId.get(parsed.actionId) : void 0);
|
|
12429
12893
|
if (!ctx) {
|
|
12894
|
+
if (lifecycleRecord && storeOptions) {
|
|
12895
|
+
const recovered = terminateAskUserQuestion(
|
|
12896
|
+
storeOptions,
|
|
12897
|
+
lifecycleRecord.questionId,
|
|
12898
|
+
lifecycleRecord.state === "dispatching" ? "restart_during_dispatch" : "restart_invalidated"
|
|
12899
|
+
);
|
|
12900
|
+
if (recovered) {
|
|
12901
|
+
await updateLifecycleRecordCardBestEffort({
|
|
12902
|
+
record: recovered,
|
|
12903
|
+
config: params.config,
|
|
12904
|
+
log: params.log
|
|
12905
|
+
});
|
|
12906
|
+
}
|
|
12907
|
+
return { handled: true };
|
|
12908
|
+
}
|
|
12430
12909
|
return { handled: false };
|
|
12431
12910
|
}
|
|
12432
12911
|
if (!isOwnerClick(ctx, params.clickerUserId)) {
|
|
@@ -12441,13 +12920,15 @@ async function handleDingTalkAskUserCardCallback(params) {
|
|
|
12441
12920
|
);
|
|
12442
12921
|
return { handled: true };
|
|
12443
12922
|
}
|
|
12444
|
-
if (ctx.submitted) {
|
|
12923
|
+
if (ctx.submitted || lifecycleRecord?.state === "dispatching") {
|
|
12445
12924
|
params.log?.debug?.(`[DingTalk][AskUser] Duplicate submit ignored question=${ctx.questionId}`);
|
|
12446
12925
|
return { handled: true };
|
|
12447
12926
|
}
|
|
12448
12927
|
const isCancel = parseBooleanLike(parsed.params.user_cancel) === true;
|
|
12449
|
-
ctx.submitted = true;
|
|
12450
12928
|
if (isCancel) {
|
|
12929
|
+
if (!claimPendingQuestionForDispatch(ctx)) {
|
|
12930
|
+
return { handled: true };
|
|
12931
|
+
}
|
|
12451
12932
|
await updateQuestionCardBestEffort(ctx, {
|
|
12452
12933
|
card_status: "cancelled",
|
|
12453
12934
|
question_desc: "\u5DF2\u53D6\u6D88\u3002",
|
|
@@ -12455,25 +12936,25 @@ async function handleDingTalkAskUserCardCallback(params) {
|
|
|
12455
12936
|
});
|
|
12456
12937
|
consumePendingQuestion(ctx);
|
|
12457
12938
|
addHandledQuestionTombstone(ctx, "cancelled");
|
|
12458
|
-
|
|
12459
|
-
|
|
12460
|
-
|
|
12461
|
-
|
|
12462
|
-
|
|
12463
|
-
|
|
12464
|
-
}
|
|
12465
|
-
);
|
|
12939
|
+
dispatchSyntheticAnswer({
|
|
12940
|
+
ctx,
|
|
12941
|
+
text: buildCancelledAnswerMessage(ctx),
|
|
12942
|
+
suffix: "cancelled",
|
|
12943
|
+
successReason: "cancelled",
|
|
12944
|
+
log: params.log
|
|
12466
12945
|
});
|
|
12467
12946
|
return { handled: true };
|
|
12468
12947
|
}
|
|
12469
12948
|
const form = asRecord5(parsed.params.form);
|
|
12470
12949
|
if (!form) {
|
|
12471
|
-
ctx.submitted = false;
|
|
12472
12950
|
params.log?.warn?.(
|
|
12473
12951
|
`[DingTalk][AskUser] Missing form payload question=${ctx.questionId} params=${JSON.stringify(parsed.params)}`
|
|
12474
12952
|
);
|
|
12475
12953
|
return { handled: true };
|
|
12476
12954
|
}
|
|
12955
|
+
if (!claimPendingQuestionForDispatch(ctx)) {
|
|
12956
|
+
return { handled: true };
|
|
12957
|
+
}
|
|
12477
12958
|
const answers = [];
|
|
12478
12959
|
const selectedValues = [];
|
|
12479
12960
|
for (const question of ctx.questions) {
|
|
@@ -12497,12 +12978,12 @@ async function handleDingTalkAskUserCardCallback(params) {
|
|
|
12497
12978
|
});
|
|
12498
12979
|
consumePendingQuestion(ctx);
|
|
12499
12980
|
addHandledQuestionTombstone(ctx, "empty");
|
|
12500
|
-
|
|
12501
|
-
|
|
12502
|
-
|
|
12503
|
-
|
|
12504
|
-
|
|
12505
|
-
|
|
12981
|
+
dispatchSyntheticAnswer({
|
|
12982
|
+
ctx,
|
|
12983
|
+
text: buildEmptyAnswerMessage(ctx),
|
|
12984
|
+
suffix: "empty",
|
|
12985
|
+
successReason: "empty",
|
|
12986
|
+
log: params.log
|
|
12506
12987
|
});
|
|
12507
12988
|
return { handled: true };
|
|
12508
12989
|
}
|
|
@@ -12517,10 +12998,12 @@ async function handleDingTalkAskUserCardCallback(params) {
|
|
|
12517
12998
|
consumePendingQuestion(ctx);
|
|
12518
12999
|
addHandledQuestionTombstone(ctx, "submitted");
|
|
12519
13000
|
const message = buildAnswerMessage(ctx, answers);
|
|
12520
|
-
|
|
12521
|
-
|
|
12522
|
-
|
|
12523
|
-
|
|
13001
|
+
dispatchSyntheticAnswer({
|
|
13002
|
+
ctx,
|
|
13003
|
+
text: message,
|
|
13004
|
+
suffix: "submitted",
|
|
13005
|
+
successReason: "submitted",
|
|
13006
|
+
log: params.log
|
|
12524
13007
|
});
|
|
12525
13008
|
return { handled: true };
|
|
12526
13009
|
}
|
|
@@ -12687,6 +13170,16 @@ function registerDingTalkAskUserQuestionTool(api) {
|
|
|
12687
13170
|
selected_values: "[]",
|
|
12688
13171
|
form: { fields }
|
|
12689
13172
|
};
|
|
13173
|
+
const storeOptions = getAskUserStoreOptions(context);
|
|
13174
|
+
const canPersistLifecycle = Boolean(storeOptions && context.questionScopeKey);
|
|
13175
|
+
if (storeOptions && context.questionScopeKey) {
|
|
13176
|
+
reserveAskUserQuestion(storeOptions, {
|
|
13177
|
+
questionId,
|
|
13178
|
+
questionScopeKey: context.questionScopeKey,
|
|
13179
|
+
outTrackId,
|
|
13180
|
+
title
|
|
13181
|
+
});
|
|
13182
|
+
}
|
|
12690
13183
|
try {
|
|
12691
13184
|
await createAndDeliverQuestionCard({
|
|
12692
13185
|
config: context.dingtalkConfig,
|
|
@@ -12698,6 +13191,9 @@ function registerDingTalkAskUserQuestionTool(api) {
|
|
|
12698
13191
|
log: context.log
|
|
12699
13192
|
});
|
|
12700
13193
|
} catch (err) {
|
|
13194
|
+
if (storeOptions && canPersistLifecycle) {
|
|
13195
|
+
terminateAskUserQuestion(storeOptions, questionId, "delivery_failed");
|
|
13196
|
+
}
|
|
12701
13197
|
const detail = formatDingTalkErrorPayloadLog("ask_user_create", err, "[DingTalk]");
|
|
12702
13198
|
return jsonToolResult({
|
|
12703
13199
|
status: "failed",
|
|
@@ -12708,20 +13204,75 @@ function registerDingTalkAskUserQuestionTool(api) {
|
|
|
12708
13204
|
...context,
|
|
12709
13205
|
onQuestionCardSent: void 0
|
|
12710
13206
|
};
|
|
12711
|
-
|
|
13207
|
+
const pendingQuestion = {
|
|
12712
13208
|
...pendingContext,
|
|
12713
13209
|
questionId,
|
|
12714
13210
|
outTrackId,
|
|
12715
13211
|
title,
|
|
12716
13212
|
questions: parsed,
|
|
12717
13213
|
submitted: false
|
|
13214
|
+
};
|
|
13215
|
+
storePendingQuestion(pendingQuestion, {
|
|
13216
|
+
supersedeExisting: !canPersistLifecycle
|
|
12718
13217
|
});
|
|
13218
|
+
if (storeOptions && canPersistLifecycle) {
|
|
13219
|
+
const activation = activateAskUserQuestion(storeOptions, questionId);
|
|
13220
|
+
if (activation.record?.state !== "pending") {
|
|
13221
|
+
const terminalReason = activation.record?.terminalReason ?? "superseded_by_message";
|
|
13222
|
+
if (activation.record) {
|
|
13223
|
+
consumeLifecyclePendingContext(activation.record);
|
|
13224
|
+
} else {
|
|
13225
|
+
pendingQuestion.submitted = true;
|
|
13226
|
+
consumePendingQuestion(pendingQuestion);
|
|
13227
|
+
addHandledQuestionTombstone(pendingQuestion, "superseded");
|
|
13228
|
+
}
|
|
13229
|
+
await updateQuestionCardBestEffort(
|
|
13230
|
+
pendingQuestion,
|
|
13231
|
+
terminalCardVariables(terminalReason)
|
|
13232
|
+
);
|
|
13233
|
+
return jsonToolResult({
|
|
13234
|
+
status: "failed",
|
|
13235
|
+
questionId,
|
|
13236
|
+
outTrackId,
|
|
13237
|
+
error: "\u95EE\u9898\u5361\u7247\u5728\u53D1\u9001\u671F\u95F4\u5DF2\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u53D1\u8D77\u3002"
|
|
13238
|
+
});
|
|
13239
|
+
}
|
|
13240
|
+
for (const superseded of activation.superseded) {
|
|
13241
|
+
const supersededContext = consumeLifecyclePendingContext(superseded);
|
|
13242
|
+
if (supersededContext) {
|
|
13243
|
+
void updateQuestionCardBestEffort(
|
|
13244
|
+
supersededContext,
|
|
13245
|
+
terminalCardVariables("superseded_by_question")
|
|
13246
|
+
);
|
|
13247
|
+
} else {
|
|
13248
|
+
void updateLifecycleRecordCardBestEffort({
|
|
13249
|
+
record: superseded,
|
|
13250
|
+
config: context.dingtalkConfig,
|
|
13251
|
+
log: context.log
|
|
13252
|
+
});
|
|
13253
|
+
}
|
|
13254
|
+
}
|
|
13255
|
+
}
|
|
13256
|
+
let takeoverSucceeded = void 0;
|
|
12719
13257
|
try {
|
|
12720
|
-
await context.onQuestionCardSent?.({ questionId, outTrackId });
|
|
13258
|
+
takeoverSucceeded = await context.onQuestionCardSent?.({ questionId, outTrackId });
|
|
12721
13259
|
} catch (err) {
|
|
12722
13260
|
context.log?.warn?.(
|
|
12723
13261
|
`[DingTalk][AskUser] onQuestionCardSent hook failed: ${err instanceof Error ? err.message : String(err)}`
|
|
12724
13262
|
);
|
|
13263
|
+
takeoverSucceeded = false;
|
|
13264
|
+
}
|
|
13265
|
+
if (takeoverSucceeded === false) {
|
|
13266
|
+
await terminatePendingQuestion({
|
|
13267
|
+
ctx: pendingQuestion,
|
|
13268
|
+
reason: "pause_failed"
|
|
13269
|
+
});
|
|
13270
|
+
return jsonToolResult({
|
|
13271
|
+
status: "failed",
|
|
13272
|
+
questionId,
|
|
13273
|
+
outTrackId,
|
|
13274
|
+
error: "\u5F53\u524D\u4EFB\u52A1\u672A\u80FD\u6682\u505C\uFF0C\u6B64\u5361\u5DF2\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u53D1\u8D77\u3002"
|
|
13275
|
+
});
|
|
12725
13276
|
}
|
|
12726
13277
|
context.log?.info?.(
|
|
12727
13278
|
`[DingTalk][AskUser] question card sent question=${questionId} outTrackId=${outTrackId}`
|
|
@@ -12926,6 +13477,7 @@ async function handleCardAction(params) {
|
|
|
12926
13477
|
payload: params.payload,
|
|
12927
13478
|
cfg: params.cfg,
|
|
12928
13479
|
accountId: params.accountId,
|
|
13480
|
+
storePath: params.storePath,
|
|
12929
13481
|
config: params.config,
|
|
12930
13482
|
clickerUserId: params.analysis.userId,
|
|
12931
13483
|
log: params.log
|
|
@@ -13066,8 +13618,8 @@ var ConnectionManager = class _ConnectionManager {
|
|
|
13066
13618
|
`[${this.accountId}] Runtime counters (${reason}): healthUnhealthyChecks=${c.healthUnhealthyChecks}, healthTriggeredReconnects=${c.healthTriggeredReconnects}, heartbeatMisses=${c.heartbeatMisses}, heartbeatTriggeredReconnects=${c.heartbeatTriggeredReconnects}, serverDisconnectMessages=${c.serverDisconnectMessages}, socketCloseEvents=${c.socketCloseEvents}, runtimeDisconnects=${c.runtimeDisconnects}, reconnectAttempts=${c.reconnectAttempts}, reconnectSuccess=${c.reconnectSuccess}, reconnectFailures=${c.reconnectFailures}`
|
|
13067
13619
|
);
|
|
13068
13620
|
}
|
|
13069
|
-
recordSocketActivity(
|
|
13070
|
-
this.lastSocketActivityAt =
|
|
13621
|
+
recordSocketActivity(now2 = Date.now()) {
|
|
13622
|
+
this.lastSocketActivityAt = now2;
|
|
13071
13623
|
this.consecutiveHeartbeatMisses = 0;
|
|
13072
13624
|
}
|
|
13073
13625
|
setupHeartbeat(socket) {
|
|
@@ -13079,13 +13631,13 @@ var ConnectionManager = class _ConnectionManager {
|
|
|
13079
13631
|
if (socket.readyState !== 1) {
|
|
13080
13632
|
return;
|
|
13081
13633
|
}
|
|
13082
|
-
const
|
|
13083
|
-
const idleMs = this.lastSocketActivityAt !== void 0 ?
|
|
13634
|
+
const now2 = Date.now();
|
|
13635
|
+
const idleMs = this.lastSocketActivityAt !== void 0 ? now2 - this.lastSocketActivityAt : _ConnectionManager.HEARTBEAT_INTERVAL_MS;
|
|
13084
13636
|
if (idleMs >= _ConnectionManager.HEARTBEAT_INTERVAL_MS) {
|
|
13085
13637
|
this.consecutiveHeartbeatMisses += 1;
|
|
13086
13638
|
this.runtimeCounters.heartbeatMisses += 1;
|
|
13087
13639
|
if (this.consecutiveHeartbeatMisses >= _ConnectionManager.HEARTBEAT_MISS_THRESHOLD) {
|
|
13088
|
-
const lastPingAgoMs = this.lastHeartbeatPingAt !== void 0 ?
|
|
13640
|
+
const lastPingAgoMs = this.lastHeartbeatPingAt !== void 0 ? now2 - this.lastHeartbeatPingAt : void 0;
|
|
13089
13641
|
this.log?.warn?.(
|
|
13090
13642
|
`[${this.accountId}] Connection heartbeat missed ${this.consecutiveHeartbeatMisses}/${_ConnectionManager.HEARTBEAT_MISS_THRESHOLD} checks, triggering reconnection (lastPingAgoMs=${lastPingAgoMs ?? "n/a"})`
|
|
13091
13643
|
);
|
|
@@ -13099,7 +13651,7 @@ var ConnectionManager = class _ConnectionManager {
|
|
|
13099
13651
|
`[${this.accountId}] Connection heartbeat missed (${this.consecutiveHeartbeatMisses}/${_ConnectionManager.HEARTBEAT_MISS_THRESHOLD})`
|
|
13100
13652
|
);
|
|
13101
13653
|
}
|
|
13102
|
-
this.lastHeartbeatPingAt =
|
|
13654
|
+
this.lastHeartbeatPingAt = now2;
|
|
13103
13655
|
try {
|
|
13104
13656
|
socket.ping("", true);
|
|
13105
13657
|
} catch (err) {
|
|
@@ -13340,8 +13892,8 @@ var ConnectionManager = class _ConnectionManager {
|
|
|
13340
13892
|
this.consecutiveUnhealthyChecks = 0;
|
|
13341
13893
|
return;
|
|
13342
13894
|
}
|
|
13343
|
-
const
|
|
13344
|
-
const withinGraceWindow = this.connectedAt !== void 0 &&
|
|
13895
|
+
const now2 = Date.now();
|
|
13896
|
+
const withinGraceWindow = this.connectedAt !== void 0 && now2 - this.connectedAt < _ConnectionManager.HEALTH_CHECK_GRACE_MS;
|
|
13345
13897
|
if (withinGraceWindow) {
|
|
13346
13898
|
this.consecutiveUnhealthyChecks = 0;
|
|
13347
13899
|
return;
|
|
@@ -13699,12 +14251,12 @@ var MESSAGE_DEDUP_TTL = 6e4;
|
|
|
13699
14251
|
var MESSAGE_DEDUP_MAX_SIZE = 1e3;
|
|
13700
14252
|
var messageCounter = 0;
|
|
13701
14253
|
function isMessageProcessed(dedupKey) {
|
|
13702
|
-
const
|
|
14254
|
+
const now2 = Date.now();
|
|
13703
14255
|
const expiresAt = processedMessages.get(dedupKey);
|
|
13704
14256
|
if (expiresAt === void 0) {
|
|
13705
14257
|
return false;
|
|
13706
14258
|
}
|
|
13707
|
-
if (
|
|
14259
|
+
if (now2 >= expiresAt) {
|
|
13708
14260
|
processedMessages.delete(dedupKey);
|
|
13709
14261
|
return false;
|
|
13710
14262
|
}
|
|
@@ -13714,9 +14266,9 @@ function markMessageProcessed(dedupKey) {
|
|
|
13714
14266
|
const expiresAt = Date.now() + MESSAGE_DEDUP_TTL;
|
|
13715
14267
|
processedMessages.set(dedupKey, expiresAt);
|
|
13716
14268
|
if (processedMessages.size > MESSAGE_DEDUP_MAX_SIZE) {
|
|
13717
|
-
const
|
|
14269
|
+
const now2 = Date.now();
|
|
13718
14270
|
for (const [key, expiry] of processedMessages.entries()) {
|
|
13719
|
-
if (
|
|
14271
|
+
if (now2 >= expiry) {
|
|
13720
14272
|
processedMessages.delete(key);
|
|
13721
14273
|
}
|
|
13722
14274
|
}
|
|
@@ -13735,9 +14287,9 @@ function markMessageProcessed(dedupKey) {
|
|
|
13735
14287
|
messageCounter++;
|
|
13736
14288
|
if (messageCounter >= 10) {
|
|
13737
14289
|
messageCounter = 0;
|
|
13738
|
-
const
|
|
14290
|
+
const now2 = Date.now();
|
|
13739
14291
|
for (const [key, expiry] of processedMessages.entries()) {
|
|
13740
|
-
if (
|
|
14292
|
+
if (now2 >= expiry) {
|
|
13741
14293
|
processedMessages.delete(key);
|
|
13742
14294
|
}
|
|
13743
14295
|
}
|
|
@@ -13864,6 +14416,23 @@ function createDingTalkGateway() {
|
|
|
13864
14416
|
`[${account.accountId}] Failed to recover unfinished cards: ${err.message}`
|
|
13865
14417
|
);
|
|
13866
14418
|
}
|
|
14419
|
+
try {
|
|
14420
|
+
const recoveredQuestions = await recoverAskUserQuestionsForAccount({
|
|
14421
|
+
storePath: accountStorePath,
|
|
14422
|
+
accountId: account.accountId,
|
|
14423
|
+
config,
|
|
14424
|
+
log: pluginLog
|
|
14425
|
+
});
|
|
14426
|
+
if (recoveredQuestions > 0) {
|
|
14427
|
+
pluginLog?.info?.(
|
|
14428
|
+
`[${account.accountId}] Invalidated ${recoveredQuestions} unfinished Ask User card(s) from previous runtime`
|
|
14429
|
+
);
|
|
14430
|
+
}
|
|
14431
|
+
} catch (err) {
|
|
14432
|
+
pluginLog?.warn?.(
|
|
14433
|
+
`[${account.accountId}] Failed to recover Ask User cards: ${err.message}`
|
|
14434
|
+
);
|
|
14435
|
+
}
|
|
13867
14436
|
const useConnectionManager = config.useConnectionManager ?? true;
|
|
13868
14437
|
const applyStatusPatch = (patch) => {
|
|
13869
14438
|
ctx.setStatus({
|
|
@@ -14034,6 +14603,7 @@ function createDingTalkGateway() {
|
|
|
14034
14603
|
analysis,
|
|
14035
14604
|
cfg,
|
|
14036
14605
|
accountId: account.accountId,
|
|
14606
|
+
storePath: accountStorePath,
|
|
14037
14607
|
config,
|
|
14038
14608
|
log: pluginLog
|
|
14039
14609
|
});
|
|
@@ -14704,9 +15274,9 @@ async function configureDingTalkAccount(params) {
|
|
|
14704
15274
|
let lastWaitingNote = 0;
|
|
14705
15275
|
const result = await session.waitForResult({
|
|
14706
15276
|
onWaiting: () => {
|
|
14707
|
-
const
|
|
14708
|
-
if (
|
|
14709
|
-
lastWaitingNote =
|
|
15277
|
+
const now2 = Date.now();
|
|
15278
|
+
if (now2 - lastWaitingNote >= 15e3) {
|
|
15279
|
+
lastWaitingNote = now2;
|
|
14710
15280
|
prompter.note("Waiting for authorization. Please finish the scan in DingTalk...", "Polling").catch(() => {
|
|
14711
15281
|
});
|
|
14712
15282
|
}
|