@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/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;
|
|
@@ -1667,7 +1667,7 @@ var BUILTIN_DINGTALK_CARD_TEMPLATE_ID = process.env.DINGTALK_CARD_TEMPLATE_ID ||
|
|
|
1667
1667
|
var BUILTIN_DINGTALK_CARD_CONTENT_KEY = "content";
|
|
1668
1668
|
var BUILTIN_DINGTALK_CARD_BLOCK_LIST_KEY = "blockList";
|
|
1669
1669
|
var BUILTIN_DINGTALK_CARD_COPY_CONTENT_KEY = "copy_content";
|
|
1670
|
-
var BUILTIN_DINGTALK_ASK_USER_CARD_TEMPLATE_ID = "
|
|
1670
|
+
var BUILTIN_DINGTALK_ASK_USER_CARD_TEMPLATE_ID = "c2a6355b-9724-4f7e-9653-d33fcb3311bb.schema";
|
|
1671
1671
|
var DINGTALK_CARD_TEMPLATE = Object.freeze({
|
|
1672
1672
|
templateId: BUILTIN_DINGTALK_CARD_TEMPLATE_ID,
|
|
1673
1673
|
contentKey: BUILTIN_DINGTALK_CARD_CONTENT_KEY,
|
|
@@ -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
|
}
|
|
@@ -3514,6 +3514,36 @@ function renderStatusLine(data, config) {
|
|
|
3514
3514
|
return lines.join("\n");
|
|
3515
3515
|
}
|
|
3516
3516
|
|
|
3517
|
+
// src/card/task-model-metadata.ts
|
|
3518
|
+
function readPrimaryModelRef(value) {
|
|
3519
|
+
if (typeof value === "string") {
|
|
3520
|
+
return value.trim() || void 0;
|
|
3521
|
+
}
|
|
3522
|
+
if (value && typeof value === "object" && typeof value.primary === "string") {
|
|
3523
|
+
return value.primary.trim() || void 0;
|
|
3524
|
+
}
|
|
3525
|
+
return void 0;
|
|
3526
|
+
}
|
|
3527
|
+
function normalizeModelDisplayName(modelRef) {
|
|
3528
|
+
const trimmed = typeof modelRef === "string" ? modelRef.trim() : "";
|
|
3529
|
+
if (!trimmed) {
|
|
3530
|
+
return void 0;
|
|
3531
|
+
}
|
|
3532
|
+
const parts = trimmed.split("/").map((part) => part.trim()).filter(Boolean);
|
|
3533
|
+
return parts.at(-1) || void 0;
|
|
3534
|
+
}
|
|
3535
|
+
function resolveConfiguredTaskModelMetadata(params) {
|
|
3536
|
+
const agents = params.cfg.agents;
|
|
3537
|
+
const agentId = String(params.agentId || "").trim();
|
|
3538
|
+
const agent = Array.isArray(agents?.list) ? agents.list.find((entry) => String(entry.id || "").trim() === agentId) : void 0;
|
|
3539
|
+
const modelRef = readPrimaryModelRef(agent?.model) ?? readPrimaryModelRef(agents?.defaults?.model);
|
|
3540
|
+
const effort = typeof agent?.thinkingDefault === "string" && agent.thinkingDefault.trim() ? agent.thinkingDefault.trim() : typeof agents?.defaults?.thinkingDefault === "string" && agents.defaults.thinkingDefault.trim() ? agents.defaults.thinkingDefault.trim() : void 0;
|
|
3541
|
+
return {
|
|
3542
|
+
model: normalizeModelDisplayName(modelRef),
|
|
3543
|
+
effort
|
|
3544
|
+
};
|
|
3545
|
+
}
|
|
3546
|
+
|
|
3517
3547
|
// src/command/card-stop-command.ts
|
|
3518
3548
|
import { resolveNativeCommandSessionTargets } from "openclaw/plugin-sdk/command-auth";
|
|
3519
3549
|
|
|
@@ -8528,27 +8558,43 @@ function createCardDraftController(params) {
|
|
|
8528
8558
|
|
|
8529
8559
|
// src/session-state.ts
|
|
8530
8560
|
var sessionStore = /* @__PURE__ */ new Map();
|
|
8531
|
-
function sessionKey(
|
|
8532
|
-
return `${accountId}:${conversationId}`;
|
|
8561
|
+
function sessionKey(scope) {
|
|
8562
|
+
return `${scope.accountId}:${scope.agentId}:${scope.conversationId}`;
|
|
8563
|
+
}
|
|
8564
|
+
function normalizeMetadataValue(value) {
|
|
8565
|
+
const trimmed = typeof value === "string" ? value.trim() : "";
|
|
8566
|
+
return trimmed || void 0;
|
|
8533
8567
|
}
|
|
8534
|
-
function
|
|
8535
|
-
const
|
|
8568
|
+
function seedMissingSessionStateMetadata(state, metadata) {
|
|
8569
|
+
const model = normalizeMetadataValue(metadata?.model);
|
|
8570
|
+
const effort = normalizeMetadataValue(metadata?.effort);
|
|
8571
|
+
if (state.model === void 0 && model !== void 0) {
|
|
8572
|
+
state.model = model;
|
|
8573
|
+
}
|
|
8574
|
+
if (state.effort === void 0 && effort !== void 0) {
|
|
8575
|
+
state.effort = effort;
|
|
8576
|
+
}
|
|
8577
|
+
}
|
|
8578
|
+
function initSessionState(scope, metadata) {
|
|
8579
|
+
const key = sessionKey(scope);
|
|
8536
8580
|
const existing = sessionStore.get(key);
|
|
8537
8581
|
if (existing) {
|
|
8538
8582
|
existing.taskStartTime = Date.now();
|
|
8583
|
+
seedMissingSessionStateMetadata(existing, metadata);
|
|
8539
8584
|
return existing;
|
|
8540
8585
|
}
|
|
8541
8586
|
const state = {
|
|
8542
8587
|
taskStartTime: Date.now()
|
|
8543
8588
|
};
|
|
8589
|
+
seedMissingSessionStateMetadata(state, metadata);
|
|
8544
8590
|
sessionStore.set(key, state);
|
|
8545
8591
|
return state;
|
|
8546
8592
|
}
|
|
8547
|
-
function getSessionState(
|
|
8548
|
-
return sessionStore.get(sessionKey(
|
|
8593
|
+
function getSessionState(scope) {
|
|
8594
|
+
return sessionStore.get(sessionKey(scope));
|
|
8549
8595
|
}
|
|
8550
|
-
function updateSessionState(
|
|
8551
|
-
const state = sessionStore.get(sessionKey(
|
|
8596
|
+
function updateSessionState(scope, patch) {
|
|
8597
|
+
const state = sessionStore.get(sessionKey(scope));
|
|
8552
8598
|
if (!state) {
|
|
8553
8599
|
return;
|
|
8554
8600
|
}
|
|
@@ -8559,8 +8605,8 @@ function updateSessionState(accountId, conversationId, patch) {
|
|
|
8559
8605
|
state.effort = patch.effort;
|
|
8560
8606
|
}
|
|
8561
8607
|
}
|
|
8562
|
-
function getTaskTimeSeconds(
|
|
8563
|
-
const state = sessionStore.get(sessionKey(
|
|
8608
|
+
function getTaskTimeSeconds(scope) {
|
|
8609
|
+
const state = sessionStore.get(sessionKey(scope));
|
|
8564
8610
|
if (!state) {
|
|
8565
8611
|
return void 0;
|
|
8566
8612
|
}
|
|
@@ -8618,12 +8664,17 @@ var EMPTY_FINAL_REPLY = "\u2705 Done";
|
|
|
8618
8664
|
var DEFAULT_CARD_FAILED_MESSAGE = "\u56DE\u590D\u751F\u6210\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5";
|
|
8619
8665
|
function createCardReplyStrategy(ctx) {
|
|
8620
8666
|
const { card, config, log, isStopRequested } = ctx;
|
|
8667
|
+
const sessionTaskStateScope = card.accountId && card.conversationId && ctx.sessionAgentId ? {
|
|
8668
|
+
accountId: card.accountId,
|
|
8669
|
+
conversationId: card.contextConversationId || card.conversationId,
|
|
8670
|
+
agentId: ctx.sessionAgentId
|
|
8671
|
+
} : void 0;
|
|
8621
8672
|
const buildStatusLine = () => {
|
|
8622
8673
|
if (!ctx.taskMeta) {
|
|
8623
8674
|
return void 0;
|
|
8624
8675
|
}
|
|
8625
8676
|
const resolvedUsage = typeof card.dapiUsage === "number" ? card.dapiUsage : typeof ctx.taskMeta.usage === "number" ? ctx.taskMeta.usage : void 0;
|
|
8626
|
-
const sessionTaskTimeSeconds =
|
|
8677
|
+
const sessionTaskTimeSeconds = sessionTaskStateScope ? getTaskTimeSeconds(sessionTaskStateScope) : void 0;
|
|
8627
8678
|
const cardElapsedMs = Math.max(0, Date.now() - card.createdAt);
|
|
8628
8679
|
const sessionElapsedMs = typeof sessionTaskTimeSeconds === "number" ? sessionTaskTimeSeconds * 1e3 : void 0;
|
|
8629
8680
|
const metaElapsedMs = typeof ctx.taskMeta.elapsedMs === "number" && ctx.taskMeta.elapsedMs > 0 ? ctx.taskMeta.elapsedMs : void 0;
|
|
@@ -8938,10 +8989,12 @@ function createCardReplyStrategy(ctx) {
|
|
|
8938
8989
|
if (!card.accountId || !card.conversationId) {
|
|
8939
8990
|
return;
|
|
8940
8991
|
}
|
|
8941
|
-
|
|
8942
|
-
|
|
8943
|
-
|
|
8944
|
-
|
|
8992
|
+
if (sessionTaskStateScope) {
|
|
8993
|
+
updateSessionState(sessionTaskStateScope, {
|
|
8994
|
+
model: selected.model,
|
|
8995
|
+
effort: selected.thinkLevel
|
|
8996
|
+
});
|
|
8997
|
+
}
|
|
8945
8998
|
if (ctx.taskMeta) {
|
|
8946
8999
|
ctx.taskMeta.model = selected.model;
|
|
8947
9000
|
ctx.taskMeta.effort = selected.thinkLevel;
|
|
@@ -8964,6 +9017,13 @@ function createCardReplyStrategy(ctx) {
|
|
|
8964
9017
|
}
|
|
8965
9018
|
}
|
|
8966
9019
|
if (payload.kind === "final") {
|
|
9020
|
+
const hasPreservableReply = Boolean(
|
|
9021
|
+
finalTextForFallback?.trim() || controller.getFinalAnswerContent().trim()
|
|
9022
|
+
) || processedMediaUrls.size > 0 || pendingNonImageMedia.length > 0;
|
|
9023
|
+
if (payload.isError === true && sawFinalDelivery && payload.mediaUrls.length === 0 && hasPreservableReply) {
|
|
9024
|
+
log?.debug?.("[DingTalk][Finalize] Ignoring late error final after answer delivery");
|
|
9025
|
+
return;
|
|
9026
|
+
}
|
|
8967
9027
|
const isFirstFinalDelivery = !sawFinalDelivery;
|
|
8968
9028
|
lifecycleState = "final_seen";
|
|
8969
9029
|
await flushPendingReasoning();
|
|
@@ -9729,10 +9789,61 @@ async function dispatchSubAgents(params) {
|
|
|
9729
9789
|
dingtalkConfig,
|
|
9730
9790
|
sessionWebhook,
|
|
9731
9791
|
extractedContent,
|
|
9792
|
+
sessionPeer,
|
|
9793
|
+
onRoutesResolved,
|
|
9732
9794
|
handleMessage,
|
|
9733
9795
|
downloadMedia: download,
|
|
9734
9796
|
log
|
|
9735
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);
|
|
9736
9847
|
let preDownloadedMedia;
|
|
9737
9848
|
const robotCode = resolveRobotCode(dingtalkConfig);
|
|
9738
9849
|
if (robotCode) {
|
|
@@ -9755,8 +9866,8 @@ async function dispatchSubAgents(params) {
|
|
|
9755
9866
|
};
|
|
9756
9867
|
}
|
|
9757
9868
|
}
|
|
9758
|
-
|
|
9759
|
-
|
|
9869
|
+
for (const target of resolvedTargets) {
|
|
9870
|
+
const agentMatch = target.agent;
|
|
9760
9871
|
try {
|
|
9761
9872
|
await handleMessage({
|
|
9762
9873
|
cfg,
|
|
@@ -9765,6 +9876,7 @@ async function dispatchSubAgents(params) {
|
|
|
9765
9876
|
sessionWebhook,
|
|
9766
9877
|
log,
|
|
9767
9878
|
dingtalkConfig,
|
|
9879
|
+
routeOverride: target.route,
|
|
9768
9880
|
subAgentOptions: {
|
|
9769
9881
|
agentId: agentMatch.agentId,
|
|
9770
9882
|
responsePrefix: commandText ? "" : `> \u{1F916} **${sanitizeAgentName(agentMatch.matchedName)}**:
|
|
@@ -9778,22 +9890,8 @@ async function dispatchSubAgents(params) {
|
|
|
9778
9890
|
} catch (error) {
|
|
9779
9891
|
const message = getErrorMessage(error);
|
|
9780
9892
|
log?.error?.(`[DingTalk] Sub-agent ${agentMatch.agentId} failed: ${message}`);
|
|
9781
|
-
if (error instanceof HostRoutingHelperUnavailableError
|
|
9782
|
-
|
|
9783
|
-
try {
|
|
9784
|
-
const isGroup = data.conversationType !== "1";
|
|
9785
|
-
const sendOptions = isGroup ? { atUserId: data.senderId, log } : { log };
|
|
9786
|
-
await sendBySession(
|
|
9787
|
-
dingtalkConfig,
|
|
9788
|
-
sessionWebhook,
|
|
9789
|
-
"\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",
|
|
9790
|
-
sendOptions
|
|
9791
|
-
);
|
|
9792
|
-
} catch (notifyError) {
|
|
9793
|
-
log?.debug?.(
|
|
9794
|
-
`[DingTalk] Failed to send sub-agent helper-missing notice: ${getErrorMessage(notifyError)}`
|
|
9795
|
-
);
|
|
9796
|
-
}
|
|
9893
|
+
if (error instanceof HostRoutingHelperUnavailableError) {
|
|
9894
|
+
await sendHelperMissingWarning();
|
|
9797
9895
|
}
|
|
9798
9896
|
}
|
|
9799
9897
|
}
|
|
@@ -10495,6 +10593,8 @@ async function handleDingTalkMessageInner(params) {
|
|
|
10495
10593
|
sessionWebhook,
|
|
10496
10594
|
log,
|
|
10497
10595
|
dingtalkConfig,
|
|
10596
|
+
inboundOrigin = "stream",
|
|
10597
|
+
routeOverride,
|
|
10498
10598
|
subAgentOptions,
|
|
10499
10599
|
preDownloadedMedia
|
|
10500
10600
|
} = params;
|
|
@@ -10689,32 +10789,42 @@ async function handleDingTalkMessageInner(params) {
|
|
|
10689
10789
|
config: dingtalkConfig
|
|
10690
10790
|
});
|
|
10691
10791
|
const messageTarget = subAgentOptions ? null : resolveMessageTarget({ extractedContent, cfg, isGroup });
|
|
10692
|
-
|
|
10693
|
-
|
|
10694
|
-
|
|
10695
|
-
|
|
10696
|
-
|
|
10697
|
-
|
|
10698
|
-
|
|
10699
|
-
|
|
10700
|
-
|
|
10701
|
-
|
|
10702
|
-
|
|
10703
|
-
|
|
10704
|
-
|
|
10705
|
-
|
|
10706
|
-
|
|
10707
|
-
|
|
10708
|
-
|
|
10709
|
-
|
|
10710
|
-
|
|
10711
|
-
|
|
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)}`);
|
|
10712
10826
|
});
|
|
10713
|
-
}
|
|
10714
|
-
const questionContext = getDingTalkQuestionContext();
|
|
10715
|
-
if (questionContext) {
|
|
10716
|
-
questionContext.questionScopeKey = `${accountId}:${route.sessionKey}:${senderId}`;
|
|
10717
|
-
}
|
|
10827
|
+
};
|
|
10718
10828
|
if (messageTarget && messageTarget.kind !== "default") {
|
|
10719
10829
|
if (messageTarget.kind === "subagent-command") {
|
|
10720
10830
|
await dispatchSubAgents({
|
|
@@ -10726,6 +10836,8 @@ async function handleDingTalkMessageInner(params) {
|
|
|
10726
10836
|
dingtalkConfig,
|
|
10727
10837
|
sessionWebhook,
|
|
10728
10838
|
extractedContent,
|
|
10839
|
+
sessionPeer,
|
|
10840
|
+
onRoutesResolved: (targets) => invalidateQuestionRoutes(targets.map((target) => target.route)),
|
|
10729
10841
|
handleMessage: handleDingTalkMessage,
|
|
10730
10842
|
downloadMedia,
|
|
10731
10843
|
log
|
|
@@ -10754,6 +10866,8 @@ async function handleDingTalkMessageInner(params) {
|
|
|
10754
10866
|
dingtalkConfig,
|
|
10755
10867
|
sessionWebhook,
|
|
10756
10868
|
extractedContent,
|
|
10869
|
+
sessionPeer,
|
|
10870
|
+
onRoutesResolved: (targets) => invalidateQuestionRoutes(targets.map((target) => target.route)),
|
|
10757
10871
|
handleMessage: handleDingTalkMessage,
|
|
10758
10872
|
downloadMedia,
|
|
10759
10873
|
log
|
|
@@ -10761,6 +10875,44 @@ async function handleDingTalkMessageInner(params) {
|
|
|
10761
10875
|
return;
|
|
10762
10876
|
}
|
|
10763
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
|
+
}
|
|
10764
10916
|
const storePath = rt.channel.session.resolveStorePath(cfg.session?.store, {
|
|
10765
10917
|
agentId: route.agentId
|
|
10766
10918
|
});
|
|
@@ -10798,16 +10950,26 @@ async function handleDingTalkMessageInner(params) {
|
|
|
10798
10950
|
const content = extractedContent;
|
|
10799
10951
|
const isBtwBypass = isBtwRequestText(stripLeadingMentions(content.text).trim());
|
|
10800
10952
|
const taskInfoConversationId = groupId || to;
|
|
10801
|
-
const
|
|
10953
|
+
const agentDisplayName = getAgentDisplayName({
|
|
10954
|
+
subAgentOptions,
|
|
10955
|
+
agentId: route.agentId,
|
|
10956
|
+
agentsList: cfg.agents?.list
|
|
10957
|
+
});
|
|
10958
|
+
const configuredTaskMetadata = resolveConfiguredTaskModelMetadata({
|
|
10959
|
+
cfg,
|
|
10960
|
+
agentId: route.agentId
|
|
10961
|
+
});
|
|
10962
|
+
const sessionTaskStateScope = {
|
|
10963
|
+
accountId,
|
|
10964
|
+
conversationId: taskInfoConversationId,
|
|
10965
|
+
agentId: route.agentId
|
|
10966
|
+
};
|
|
10967
|
+
const sessionTaskState = initSessionState(sessionTaskStateScope, configuredTaskMetadata);
|
|
10802
10968
|
const initialStatusLine = renderStatusLine(
|
|
10803
10969
|
{
|
|
10804
10970
|
model: sessionTaskState.model,
|
|
10805
10971
|
effort: sessionTaskState.effort,
|
|
10806
|
-
agent:
|
|
10807
|
-
subAgentOptions,
|
|
10808
|
-
agentId: route.agentId,
|
|
10809
|
-
agentsList: cfg.agents?.list
|
|
10810
|
-
})
|
|
10972
|
+
agent: agentDisplayName
|
|
10811
10973
|
},
|
|
10812
10974
|
dingtalkConfig
|
|
10813
10975
|
) || void 0;
|
|
@@ -10817,7 +10979,6 @@ async function handleDingTalkMessageInner(params) {
|
|
|
10817
10979
|
let cardFlightKey;
|
|
10818
10980
|
if (questionContext) {
|
|
10819
10981
|
questionContext.onQuestionCardSent = async ({ questionId, outTrackId }) => {
|
|
10820
|
-
questionCardTookOver = true;
|
|
10821
10982
|
if (cardFlightKey) {
|
|
10822
10983
|
cardCreationInFlight.delete(cardFlightKey);
|
|
10823
10984
|
cardFlightKey = void 0;
|
|
@@ -10838,23 +10999,34 @@ async function handleDingTalkMessageInner(params) {
|
|
|
10838
10999
|
log?.warn?.(
|
|
10839
11000
|
`[DingTalk][AskUser] Question card sent, but targeted stop failed question=${questionId} outTrackId=${outTrackId} targetSessionKey=${route.sessionKey}: ${err instanceof Error ? err.message : String(err)}`
|
|
10840
11001
|
);
|
|
11002
|
+
return false;
|
|
10841
11003
|
}
|
|
11004
|
+
questionCardTookOver = true;
|
|
10842
11005
|
if (!currentAICard) {
|
|
10843
|
-
return;
|
|
11006
|
+
return true;
|
|
10844
11007
|
}
|
|
10845
11008
|
if (isCardInTerminalState(currentAICard.state)) {
|
|
10846
|
-
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;
|
|
10847
11019
|
}
|
|
10848
|
-
const recalled = await recallAICardMessage(currentAICard, log);
|
|
10849
11020
|
if (!recalled) {
|
|
10850
11021
|
log?.warn?.(
|
|
10851
11022
|
`[DingTalk][AskUser] Question card sent, but AI card recall failed; normal replies remain suppressed question=${questionId} outTrackId=${outTrackId}`
|
|
10852
11023
|
);
|
|
10853
|
-
return;
|
|
11024
|
+
return true;
|
|
10854
11025
|
}
|
|
10855
11026
|
log?.info?.(
|
|
10856
11027
|
`[DingTalk][AskUser] Recalled empty AI card after question card sent question=${questionId} outTrackId=${outTrackId}`
|
|
10857
11028
|
);
|
|
11029
|
+
return true;
|
|
10858
11030
|
};
|
|
10859
11031
|
}
|
|
10860
11032
|
if (useCardMode && !isBtwBypass) {
|
|
@@ -11768,16 +11940,12 @@ ${attachmentExtractedText}` : inboundBody;
|
|
|
11768
11940
|
accountId
|
|
11769
11941
|
});
|
|
11770
11942
|
const strategyConfig = legacyCardStreamReasoning === void 0 ? dingtalkConfig : { ...dingtalkConfig, cardStreamReasoning: legacyCardStreamReasoning };
|
|
11771
|
-
const sessionTaskState2 = getSessionState(
|
|
11943
|
+
const sessionTaskState2 = getSessionState(sessionTaskStateScope);
|
|
11772
11944
|
const taskMeta = {
|
|
11773
11945
|
model: sessionTaskState2?.model,
|
|
11774
11946
|
effort: sessionTaskState2?.effort,
|
|
11775
11947
|
elapsedMs: typeof sessionTaskState2?.taskStartTime === "number" ? Math.max(0, Date.now() - sessionTaskState2.taskStartTime) : void 0,
|
|
11776
|
-
agent:
|
|
11777
|
-
subAgentOptions,
|
|
11778
|
-
agentId: route.agentId,
|
|
11779
|
-
agentsList: cfg.agents?.list
|
|
11780
|
-
})
|
|
11948
|
+
agent: agentDisplayName
|
|
11781
11949
|
};
|
|
11782
11950
|
const strategy = createReplyStrategy({
|
|
11783
11951
|
config: strategyConfig,
|
|
@@ -11838,6 +12006,7 @@ ${attachmentExtractedText}` : inboundBody;
|
|
|
11838
12006
|
mediaUrls,
|
|
11839
12007
|
audioAsVoice: extractSharedAudioAsVoice(payload, inlineReplyPayload),
|
|
11840
12008
|
kind: replyKind,
|
|
12009
|
+
isError: payload.isError === true,
|
|
11841
12010
|
isReasoning: richPayload.isReasoning === true
|
|
11842
12011
|
});
|
|
11843
12012
|
} catch (err) {
|
|
@@ -11908,6 +12077,194 @@ ${attachmentExtractedText}` : inboundBody;
|
|
|
11908
12077
|
}
|
|
11909
12078
|
}
|
|
11910
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
|
+
|
|
11911
12268
|
// src/card/ask-user-question.ts
|
|
11912
12269
|
var DINGTALK_API4 = "https://api.dingtalk.com";
|
|
11913
12270
|
var PENDING_QUESTION_TTL_MS = 5 * 60 * 1e3;
|
|
@@ -12177,9 +12534,11 @@ function supersedePendingQuestionsInScope(ctx) {
|
|
|
12177
12534
|
});
|
|
12178
12535
|
}
|
|
12179
12536
|
}
|
|
12180
|
-
function storePendingQuestion(ctx) {
|
|
12537
|
+
function storePendingQuestion(ctx, options = {}) {
|
|
12181
12538
|
ctx.ownerUserId = resolvePendingQuestionOwner(ctx);
|
|
12182
|
-
|
|
12539
|
+
if (options.supersedeExisting !== false) {
|
|
12540
|
+
supersedePendingQuestionsInScope(ctx);
|
|
12541
|
+
}
|
|
12183
12542
|
pendingQuestionsByTrackId.set(ctx.outTrackId, ctx);
|
|
12184
12543
|
pendingQuestionsByQuestionId.set(ctx.questionId, ctx);
|
|
12185
12544
|
addScopeIndex(ctx);
|
|
@@ -12187,7 +12546,9 @@ function storePendingQuestion(ctx) {
|
|
|
12187
12546
|
if (!pendingQuestionsByTrackId.has(ctx.outTrackId) || ctx.submitted) {
|
|
12188
12547
|
return;
|
|
12189
12548
|
}
|
|
12190
|
-
ctx
|
|
12549
|
+
if (!claimPendingQuestionForDispatch(ctx)) {
|
|
12550
|
+
return;
|
|
12551
|
+
}
|
|
12191
12552
|
consumePendingQuestion(ctx);
|
|
12192
12553
|
addHandledQuestionTombstone(ctx, "expired");
|
|
12193
12554
|
void updateQuestionCardBestEffort(ctx, {
|
|
@@ -12195,14 +12556,12 @@ function storePendingQuestion(ctx) {
|
|
|
12195
12556
|
question_desc: "\u95EE\u9898\u5DF2\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u53D1\u8D77\u3002",
|
|
12196
12557
|
form_btn_text: "\u5DF2\u5931\u6548"
|
|
12197
12558
|
});
|
|
12198
|
-
|
|
12199
|
-
|
|
12200
|
-
|
|
12201
|
-
|
|
12202
|
-
|
|
12203
|
-
|
|
12204
|
-
}
|
|
12205
|
-
);
|
|
12559
|
+
dispatchSyntheticAnswer({
|
|
12560
|
+
ctx,
|
|
12561
|
+
text: buildExpiredAnswerMessage(ctx),
|
|
12562
|
+
suffix: "expired",
|
|
12563
|
+
successReason: "expired",
|
|
12564
|
+
log: ctx.log
|
|
12206
12565
|
});
|
|
12207
12566
|
}, PENDING_QUESTION_TTL_MS);
|
|
12208
12567
|
}
|
|
@@ -12227,6 +12586,123 @@ async function updateQuestionCardBestEffort(ctx, variables) {
|
|
|
12227
12586
|
);
|
|
12228
12587
|
}
|
|
12229
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
|
+
}
|
|
12230
12706
|
function parseEmbeddedJson2(value) {
|
|
12231
12707
|
if (typeof value !== "string") {
|
|
12232
12708
|
return value;
|
|
@@ -12346,7 +12822,42 @@ async function injectAnswerSyntheticMessage(ctx, text, suffix) {
|
|
|
12346
12822
|
data: syntheticData,
|
|
12347
12823
|
sessionWebhook: ctx.sessionWebhook,
|
|
12348
12824
|
log: ctx.log,
|
|
12349
|
-
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
|
+
);
|
|
12350
12861
|
});
|
|
12351
12862
|
}
|
|
12352
12863
|
async function handleDingTalkAskUserCardCallback(params) {
|
|
@@ -12358,8 +12869,43 @@ async function handleDingTalkAskUserCardCallback(params) {
|
|
|
12358
12869
|
);
|
|
12359
12870
|
return { handled: true };
|
|
12360
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
|
+
}
|
|
12361
12892
|
const ctx = (parsed.outTrackId ? pendingQuestionsByTrackId.get(parsed.outTrackId) : void 0) ?? (parsed.actionId ? pendingQuestionsByQuestionId.get(parsed.actionId) : void 0);
|
|
12362
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
|
+
}
|
|
12363
12909
|
return { handled: false };
|
|
12364
12910
|
}
|
|
12365
12911
|
if (!isOwnerClick(ctx, params.clickerUserId)) {
|
|
@@ -12374,13 +12920,15 @@ async function handleDingTalkAskUserCardCallback(params) {
|
|
|
12374
12920
|
);
|
|
12375
12921
|
return { handled: true };
|
|
12376
12922
|
}
|
|
12377
|
-
if (ctx.submitted) {
|
|
12923
|
+
if (ctx.submitted || lifecycleRecord?.state === "dispatching") {
|
|
12378
12924
|
params.log?.debug?.(`[DingTalk][AskUser] Duplicate submit ignored question=${ctx.questionId}`);
|
|
12379
12925
|
return { handled: true };
|
|
12380
12926
|
}
|
|
12381
12927
|
const isCancel = parseBooleanLike(parsed.params.user_cancel) === true;
|
|
12382
|
-
ctx.submitted = true;
|
|
12383
12928
|
if (isCancel) {
|
|
12929
|
+
if (!claimPendingQuestionForDispatch(ctx)) {
|
|
12930
|
+
return { handled: true };
|
|
12931
|
+
}
|
|
12384
12932
|
await updateQuestionCardBestEffort(ctx, {
|
|
12385
12933
|
card_status: "cancelled",
|
|
12386
12934
|
question_desc: "\u5DF2\u53D6\u6D88\u3002",
|
|
@@ -12388,25 +12936,25 @@ async function handleDingTalkAskUserCardCallback(params) {
|
|
|
12388
12936
|
});
|
|
12389
12937
|
consumePendingQuestion(ctx);
|
|
12390
12938
|
addHandledQuestionTombstone(ctx, "cancelled");
|
|
12391
|
-
|
|
12392
|
-
|
|
12393
|
-
|
|
12394
|
-
|
|
12395
|
-
|
|
12396
|
-
|
|
12397
|
-
}
|
|
12398
|
-
);
|
|
12939
|
+
dispatchSyntheticAnswer({
|
|
12940
|
+
ctx,
|
|
12941
|
+
text: buildCancelledAnswerMessage(ctx),
|
|
12942
|
+
suffix: "cancelled",
|
|
12943
|
+
successReason: "cancelled",
|
|
12944
|
+
log: params.log
|
|
12399
12945
|
});
|
|
12400
12946
|
return { handled: true };
|
|
12401
12947
|
}
|
|
12402
12948
|
const form = asRecord5(parsed.params.form);
|
|
12403
12949
|
if (!form) {
|
|
12404
|
-
ctx.submitted = false;
|
|
12405
12950
|
params.log?.warn?.(
|
|
12406
12951
|
`[DingTalk][AskUser] Missing form payload question=${ctx.questionId} params=${JSON.stringify(parsed.params)}`
|
|
12407
12952
|
);
|
|
12408
12953
|
return { handled: true };
|
|
12409
12954
|
}
|
|
12955
|
+
if (!claimPendingQuestionForDispatch(ctx)) {
|
|
12956
|
+
return { handled: true };
|
|
12957
|
+
}
|
|
12410
12958
|
const answers = [];
|
|
12411
12959
|
const selectedValues = [];
|
|
12412
12960
|
for (const question of ctx.questions) {
|
|
@@ -12430,12 +12978,12 @@ async function handleDingTalkAskUserCardCallback(params) {
|
|
|
12430
12978
|
});
|
|
12431
12979
|
consumePendingQuestion(ctx);
|
|
12432
12980
|
addHandledQuestionTombstone(ctx, "empty");
|
|
12433
|
-
|
|
12434
|
-
|
|
12435
|
-
|
|
12436
|
-
|
|
12437
|
-
|
|
12438
|
-
|
|
12981
|
+
dispatchSyntheticAnswer({
|
|
12982
|
+
ctx,
|
|
12983
|
+
text: buildEmptyAnswerMessage(ctx),
|
|
12984
|
+
suffix: "empty",
|
|
12985
|
+
successReason: "empty",
|
|
12986
|
+
log: params.log
|
|
12439
12987
|
});
|
|
12440
12988
|
return { handled: true };
|
|
12441
12989
|
}
|
|
@@ -12450,10 +12998,12 @@ async function handleDingTalkAskUserCardCallback(params) {
|
|
|
12450
12998
|
consumePendingQuestion(ctx);
|
|
12451
12999
|
addHandledQuestionTombstone(ctx, "submitted");
|
|
12452
13000
|
const message = buildAnswerMessage(ctx, answers);
|
|
12453
|
-
|
|
12454
|
-
|
|
12455
|
-
|
|
12456
|
-
|
|
13001
|
+
dispatchSyntheticAnswer({
|
|
13002
|
+
ctx,
|
|
13003
|
+
text: message,
|
|
13004
|
+
suffix: "submitted",
|
|
13005
|
+
successReason: "submitted",
|
|
13006
|
+
log: params.log
|
|
12457
13007
|
});
|
|
12458
13008
|
return { handled: true };
|
|
12459
13009
|
}
|
|
@@ -12620,6 +13170,16 @@ function registerDingTalkAskUserQuestionTool(api) {
|
|
|
12620
13170
|
selected_values: "[]",
|
|
12621
13171
|
form: { fields }
|
|
12622
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
|
+
}
|
|
12623
13183
|
try {
|
|
12624
13184
|
await createAndDeliverQuestionCard({
|
|
12625
13185
|
config: context.dingtalkConfig,
|
|
@@ -12631,6 +13191,9 @@ function registerDingTalkAskUserQuestionTool(api) {
|
|
|
12631
13191
|
log: context.log
|
|
12632
13192
|
});
|
|
12633
13193
|
} catch (err) {
|
|
13194
|
+
if (storeOptions && canPersistLifecycle) {
|
|
13195
|
+
terminateAskUserQuestion(storeOptions, questionId, "delivery_failed");
|
|
13196
|
+
}
|
|
12634
13197
|
const detail = formatDingTalkErrorPayloadLog("ask_user_create", err, "[DingTalk]");
|
|
12635
13198
|
return jsonToolResult({
|
|
12636
13199
|
status: "failed",
|
|
@@ -12641,20 +13204,75 @@ function registerDingTalkAskUserQuestionTool(api) {
|
|
|
12641
13204
|
...context,
|
|
12642
13205
|
onQuestionCardSent: void 0
|
|
12643
13206
|
};
|
|
12644
|
-
|
|
13207
|
+
const pendingQuestion = {
|
|
12645
13208
|
...pendingContext,
|
|
12646
13209
|
questionId,
|
|
12647
13210
|
outTrackId,
|
|
12648
13211
|
title,
|
|
12649
13212
|
questions: parsed,
|
|
12650
13213
|
submitted: false
|
|
13214
|
+
};
|
|
13215
|
+
storePendingQuestion(pendingQuestion, {
|
|
13216
|
+
supersedeExisting: !canPersistLifecycle
|
|
12651
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;
|
|
12652
13257
|
try {
|
|
12653
|
-
await context.onQuestionCardSent?.({ questionId, outTrackId });
|
|
13258
|
+
takeoverSucceeded = await context.onQuestionCardSent?.({ questionId, outTrackId });
|
|
12654
13259
|
} catch (err) {
|
|
12655
13260
|
context.log?.warn?.(
|
|
12656
13261
|
`[DingTalk][AskUser] onQuestionCardSent hook failed: ${err instanceof Error ? err.message : String(err)}`
|
|
12657
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
|
+
});
|
|
12658
13276
|
}
|
|
12659
13277
|
context.log?.info?.(
|
|
12660
13278
|
`[DingTalk][AskUser] question card sent question=${questionId} outTrackId=${outTrackId}`
|
|
@@ -12859,6 +13477,7 @@ async function handleCardAction(params) {
|
|
|
12859
13477
|
payload: params.payload,
|
|
12860
13478
|
cfg: params.cfg,
|
|
12861
13479
|
accountId: params.accountId,
|
|
13480
|
+
storePath: params.storePath,
|
|
12862
13481
|
config: params.config,
|
|
12863
13482
|
clickerUserId: params.analysis.userId,
|
|
12864
13483
|
log: params.log
|
|
@@ -12999,8 +13618,8 @@ var ConnectionManager = class _ConnectionManager {
|
|
|
12999
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}`
|
|
13000
13619
|
);
|
|
13001
13620
|
}
|
|
13002
|
-
recordSocketActivity(
|
|
13003
|
-
this.lastSocketActivityAt =
|
|
13621
|
+
recordSocketActivity(now2 = Date.now()) {
|
|
13622
|
+
this.lastSocketActivityAt = now2;
|
|
13004
13623
|
this.consecutiveHeartbeatMisses = 0;
|
|
13005
13624
|
}
|
|
13006
13625
|
setupHeartbeat(socket) {
|
|
@@ -13012,13 +13631,13 @@ var ConnectionManager = class _ConnectionManager {
|
|
|
13012
13631
|
if (socket.readyState !== 1) {
|
|
13013
13632
|
return;
|
|
13014
13633
|
}
|
|
13015
|
-
const
|
|
13016
|
-
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;
|
|
13017
13636
|
if (idleMs >= _ConnectionManager.HEARTBEAT_INTERVAL_MS) {
|
|
13018
13637
|
this.consecutiveHeartbeatMisses += 1;
|
|
13019
13638
|
this.runtimeCounters.heartbeatMisses += 1;
|
|
13020
13639
|
if (this.consecutiveHeartbeatMisses >= _ConnectionManager.HEARTBEAT_MISS_THRESHOLD) {
|
|
13021
|
-
const lastPingAgoMs = this.lastHeartbeatPingAt !== void 0 ?
|
|
13640
|
+
const lastPingAgoMs = this.lastHeartbeatPingAt !== void 0 ? now2 - this.lastHeartbeatPingAt : void 0;
|
|
13022
13641
|
this.log?.warn?.(
|
|
13023
13642
|
`[${this.accountId}] Connection heartbeat missed ${this.consecutiveHeartbeatMisses}/${_ConnectionManager.HEARTBEAT_MISS_THRESHOLD} checks, triggering reconnection (lastPingAgoMs=${lastPingAgoMs ?? "n/a"})`
|
|
13024
13643
|
);
|
|
@@ -13032,7 +13651,7 @@ var ConnectionManager = class _ConnectionManager {
|
|
|
13032
13651
|
`[${this.accountId}] Connection heartbeat missed (${this.consecutiveHeartbeatMisses}/${_ConnectionManager.HEARTBEAT_MISS_THRESHOLD})`
|
|
13033
13652
|
);
|
|
13034
13653
|
}
|
|
13035
|
-
this.lastHeartbeatPingAt =
|
|
13654
|
+
this.lastHeartbeatPingAt = now2;
|
|
13036
13655
|
try {
|
|
13037
13656
|
socket.ping("", true);
|
|
13038
13657
|
} catch (err) {
|
|
@@ -13273,8 +13892,8 @@ var ConnectionManager = class _ConnectionManager {
|
|
|
13273
13892
|
this.consecutiveUnhealthyChecks = 0;
|
|
13274
13893
|
return;
|
|
13275
13894
|
}
|
|
13276
|
-
const
|
|
13277
|
-
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;
|
|
13278
13897
|
if (withinGraceWindow) {
|
|
13279
13898
|
this.consecutiveUnhealthyChecks = 0;
|
|
13280
13899
|
return;
|
|
@@ -13632,12 +14251,12 @@ var MESSAGE_DEDUP_TTL = 6e4;
|
|
|
13632
14251
|
var MESSAGE_DEDUP_MAX_SIZE = 1e3;
|
|
13633
14252
|
var messageCounter = 0;
|
|
13634
14253
|
function isMessageProcessed(dedupKey) {
|
|
13635
|
-
const
|
|
14254
|
+
const now2 = Date.now();
|
|
13636
14255
|
const expiresAt = processedMessages.get(dedupKey);
|
|
13637
14256
|
if (expiresAt === void 0) {
|
|
13638
14257
|
return false;
|
|
13639
14258
|
}
|
|
13640
|
-
if (
|
|
14259
|
+
if (now2 >= expiresAt) {
|
|
13641
14260
|
processedMessages.delete(dedupKey);
|
|
13642
14261
|
return false;
|
|
13643
14262
|
}
|
|
@@ -13647,9 +14266,9 @@ function markMessageProcessed(dedupKey) {
|
|
|
13647
14266
|
const expiresAt = Date.now() + MESSAGE_DEDUP_TTL;
|
|
13648
14267
|
processedMessages.set(dedupKey, expiresAt);
|
|
13649
14268
|
if (processedMessages.size > MESSAGE_DEDUP_MAX_SIZE) {
|
|
13650
|
-
const
|
|
14269
|
+
const now2 = Date.now();
|
|
13651
14270
|
for (const [key, expiry] of processedMessages.entries()) {
|
|
13652
|
-
if (
|
|
14271
|
+
if (now2 >= expiry) {
|
|
13653
14272
|
processedMessages.delete(key);
|
|
13654
14273
|
}
|
|
13655
14274
|
}
|
|
@@ -13668,9 +14287,9 @@ function markMessageProcessed(dedupKey) {
|
|
|
13668
14287
|
messageCounter++;
|
|
13669
14288
|
if (messageCounter >= 10) {
|
|
13670
14289
|
messageCounter = 0;
|
|
13671
|
-
const
|
|
14290
|
+
const now2 = Date.now();
|
|
13672
14291
|
for (const [key, expiry] of processedMessages.entries()) {
|
|
13673
|
-
if (
|
|
14292
|
+
if (now2 >= expiry) {
|
|
13674
14293
|
processedMessages.delete(key);
|
|
13675
14294
|
}
|
|
13676
14295
|
}
|
|
@@ -13797,6 +14416,23 @@ function createDingTalkGateway() {
|
|
|
13797
14416
|
`[${account.accountId}] Failed to recover unfinished cards: ${err.message}`
|
|
13798
14417
|
);
|
|
13799
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
|
+
}
|
|
13800
14436
|
const useConnectionManager = config.useConnectionManager ?? true;
|
|
13801
14437
|
const applyStatusPatch = (patch) => {
|
|
13802
14438
|
ctx.setStatus({
|
|
@@ -13967,6 +14603,7 @@ function createDingTalkGateway() {
|
|
|
13967
14603
|
analysis,
|
|
13968
14604
|
cfg,
|
|
13969
14605
|
accountId: account.accountId,
|
|
14606
|
+
storePath: accountStorePath,
|
|
13970
14607
|
config,
|
|
13971
14608
|
log: pluginLog
|
|
13972
14609
|
});
|
|
@@ -14637,9 +15274,9 @@ async function configureDingTalkAccount(params) {
|
|
|
14637
15274
|
let lastWaitingNote = 0;
|
|
14638
15275
|
const result = await session.waitForResult({
|
|
14639
15276
|
onWaiting: () => {
|
|
14640
|
-
const
|
|
14641
|
-
if (
|
|
14642
|
-
lastWaitingNote =
|
|
15277
|
+
const now2 = Date.now();
|
|
15278
|
+
if (now2 - lastWaitingNote >= 15e3) {
|
|
15279
|
+
lastWaitingNote = now2;
|
|
14643
15280
|
prompter.note("Waiting for authorization. Please finish the scan in DingTalk...", "Polling").catch(() => {
|
|
14644
15281
|
});
|
|
14645
15282
|
}
|