@soimy/dingtalk 3.6.6 → 3.6.8
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 +1433 -344
- 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-service.d.ts +4 -1
- package/dist/src/card-service.d.ts.map +1 -1
- package/dist/src/gateway/channel-gateway.d.ts.map +1 -1
- package/dist/src/gateway/inbound-session-queue-dispatcher.d.ts +23 -0
- package/dist/src/gateway/inbound-session-queue-dispatcher.d.ts.map +1 -0
- package/dist/src/gateway/inbound-session-queue.d.ts +46 -0
- package/dist/src/gateway/inbound-session-queue.d.ts.map +1 -0
- package/dist/src/gateway/reply-session-conflict.d.ts +22 -0
- package/dist/src/gateway/reply-session-conflict.d.ts.map +1 -0
- package/dist/src/inbound-handler.d.ts.map +1 -1
- package/dist/src/onboarding.d.ts.map +1 -1
- package/dist/src/targeting/agent-routing.d.ts +17 -1
- package/dist/src/targeting/agent-routing.d.ts.map +1 -1
- package/dist/src/types.d.ts +35 -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 +398 -37
- package/src/card/card-action-handler.ts +2 -0
- package/src/card-service.ts +55 -3
- package/src/gateway/channel-gateway.ts +51 -30
- package/src/gateway/inbound-session-queue-dispatcher.ts +304 -0
- package/src/gateway/inbound-session-queue.ts +244 -0
- package/src/gateway/reply-session-conflict.ts +82 -0
- package/src/inbound-handler.ts +254 -57
- package/src/onboarding.ts +6 -2
- package/src/targeting/agent-routing.ts +84 -19
- package/src/types.ts +36 -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;
|
|
@@ -2681,7 +2681,7 @@ function normalizePendingState(parsed) {
|
|
|
2681
2681
|
updatedAt: typeof parsed.updatedAt === "number" ? parsed.updatedAt : Date.now(),
|
|
2682
2682
|
pendingCards: records2.filter(
|
|
2683
2683
|
(entry) => Boolean(
|
|
2684
|
-
entry && typeof entry.accountId === "string" && typeof entry.cardInstanceId === "string" && (entry.outTrackId === void 0 || typeof entry.outTrackId === "string") && typeof entry.conversationId === "string" && (entry.lastContent === void 0 || typeof entry.lastContent === "string") && (entry.lastBlockListJson === void 0 || typeof entry.lastBlockListJson === "string") && (entry.streamLifecycleOpened === void 0 || typeof entry.streamLifecycleOpened === "boolean")
|
|
2684
|
+
entry && typeof entry.accountId === "string" && typeof entry.cardInstanceId === "string" && (entry.outTrackId === void 0 || typeof entry.outTrackId === "string") && (entry.processQueryKey === void 0 || typeof entry.processQueryKey === "string") && typeof entry.conversationId === "string" && (entry.lastContent === void 0 || typeof entry.lastContent === "string") && (entry.lastBlockListJson === void 0 || typeof entry.lastBlockListJson === "string") && (entry.streamLifecycleOpened === void 0 || typeof entry.streamLifecycleOpened === "boolean") && (entry.recoveryAction === void 0 || entry.recoveryAction === "finalize" || entry.recoveryAction === "recall")
|
|
2685
2685
|
)
|
|
2686
2686
|
)
|
|
2687
2687
|
};
|
|
@@ -2738,6 +2738,7 @@ function upsertPendingCard(card, storePath, log) {
|
|
|
2738
2738
|
accountId: card.accountId,
|
|
2739
2739
|
cardInstanceId: card.cardInstanceId,
|
|
2740
2740
|
outTrackId: card.outTrackId,
|
|
2741
|
+
processQueryKey: card.processQueryKey,
|
|
2741
2742
|
conversationId: card.conversationId,
|
|
2742
2743
|
contextConversationId: card.contextConversationId,
|
|
2743
2744
|
createdAt: card.createdAt,
|
|
@@ -2800,6 +2801,28 @@ function removePendingCardById(cardInstanceId, storePath, log) {
|
|
|
2800
2801
|
state.updatedAt = Date.now();
|
|
2801
2802
|
writePendingCardState(state, storePath, log);
|
|
2802
2803
|
}
|
|
2804
|
+
function retainPendingCardForRecovery(card, recoveryAction, log) {
|
|
2805
|
+
if (!card.accountId || !card.storePath) {
|
|
2806
|
+
return;
|
|
2807
|
+
}
|
|
2808
|
+
const state = readPendingCardState(card.storePath, log);
|
|
2809
|
+
const index = state.pendingCards.findIndex((item) => item.cardInstanceId === card.cardInstanceId);
|
|
2810
|
+
if (index < 0) {
|
|
2811
|
+
return;
|
|
2812
|
+
}
|
|
2813
|
+
const existing = state.pendingCards[index];
|
|
2814
|
+
state.pendingCards[index] = {
|
|
2815
|
+
...existing,
|
|
2816
|
+
state: AICardStatus.FAILED,
|
|
2817
|
+
lastUpdated: Date.now(),
|
|
2818
|
+
lastContent: card.lastStreamedContent ?? existing.lastContent,
|
|
2819
|
+
lastBlockListJson: card.lastBlockListJson ?? existing.lastBlockListJson,
|
|
2820
|
+
streamLifecycleOpened: card.streamLifecycleOpened,
|
|
2821
|
+
recoveryAction
|
|
2822
|
+
};
|
|
2823
|
+
state.updatedAt = Date.now();
|
|
2824
|
+
writePendingCardState(state, card.storePath, log);
|
|
2825
|
+
}
|
|
2803
2826
|
function listPendingCardsByAccount(accountId, storePath, log) {
|
|
2804
2827
|
const state = readPendingCardState(storePath, log);
|
|
2805
2828
|
return state.pendingCards.filter((item) => item.accountId === accountId);
|
|
@@ -2827,6 +2850,37 @@ async function ensureFreshToken(card, log) {
|
|
|
2827
2850
|
}
|
|
2828
2851
|
}
|
|
2829
2852
|
}
|
|
2853
|
+
async function sendTemplateMismatchNotification(card, text, log) {
|
|
2854
|
+
const config = card.config;
|
|
2855
|
+
if (!config) {
|
|
2856
|
+
return;
|
|
2857
|
+
}
|
|
2858
|
+
try {
|
|
2859
|
+
const token = await getAccessToken(config, log);
|
|
2860
|
+
const { targetId, isExplicitUser } = stripTargetPrefix(card.conversationId);
|
|
2861
|
+
const resolvedTarget = resolveOriginalPeerId(targetId);
|
|
2862
|
+
const isGroup = !isExplicitUser && resolvedTarget.startsWith("cid");
|
|
2863
|
+
const url = isGroup ? "https://api.dingtalk.com/v1.0/robot/groupMessages/send" : "https://api.dingtalk.com/v1.0/robot/oToMessages/batchSend";
|
|
2864
|
+
const payload = {
|
|
2865
|
+
robotCode: resolveRobotCode(config),
|
|
2866
|
+
msgKey: "sampleMarkdown",
|
|
2867
|
+
msgParam: JSON.stringify({ title: "OpenClaw \u63D0\u9192", text })
|
|
2868
|
+
};
|
|
2869
|
+
if (isGroup) {
|
|
2870
|
+
payload.openConversationId = resolvedTarget;
|
|
2871
|
+
} else {
|
|
2872
|
+
payload.userIds = [resolvedTarget];
|
|
2873
|
+
}
|
|
2874
|
+
await http_client_default({
|
|
2875
|
+
url,
|
|
2876
|
+
method: "POST",
|
|
2877
|
+
data: payload,
|
|
2878
|
+
headers: { "x-acs-dingtalk-access-token": token, "Content-Type": "application/json" }
|
|
2879
|
+
});
|
|
2880
|
+
} catch (sendErr) {
|
|
2881
|
+
log?.warn?.(`[DingTalk][AICard] Failed to send error notification to user: ${sendErr.message}`);
|
|
2882
|
+
}
|
|
2883
|
+
}
|
|
2830
2884
|
async function sendProactiveCardText(config, conversationId, content, log) {
|
|
2831
2885
|
try {
|
|
2832
2886
|
const card = await createAICard(config, conversationId, log, { persistPending: false });
|
|
@@ -2867,7 +2921,7 @@ async function finalizePendingCardsByAccount(config, accountId, reason, storePat
|
|
|
2867
2921
|
return 0;
|
|
2868
2922
|
}
|
|
2869
2923
|
const pendingCards = listPendingCardsByAccount(accountId, storePath, log).filter(
|
|
2870
|
-
(item) => !isCardInTerminalState(item.state)
|
|
2924
|
+
(item) => item.recoveryAction === "recall" && mode === "recover" || item.recoveryAction !== "recall" && (!isCardInTerminalState(item.state) || item.recoveryAction === "finalize")
|
|
2871
2925
|
);
|
|
2872
2926
|
if (pendingCards.length === 0) {
|
|
2873
2927
|
return 0;
|
|
@@ -2892,6 +2946,7 @@ async function finalizePendingCardsByAccount(config, accountId, reason, storePat
|
|
|
2892
2946
|
accountId: entry.accountId,
|
|
2893
2947
|
storePath,
|
|
2894
2948
|
outTrackId: entry.outTrackId,
|
|
2949
|
+
processQueryKey: entry.processQueryKey,
|
|
2895
2950
|
createdAt: entry.createdAt || Date.now(),
|
|
2896
2951
|
lastUpdated: entry.lastUpdated || Date.now(),
|
|
2897
2952
|
state: normalizeRecoveredState(entry.state),
|
|
@@ -2901,6 +2956,12 @@ async function finalizePendingCardsByAccount(config, accountId, reason, storePat
|
|
|
2901
2956
|
streamLifecycleOpened: entry.streamLifecycleOpened
|
|
2902
2957
|
};
|
|
2903
2958
|
try {
|
|
2959
|
+
if (entry.recoveryAction === "recall") {
|
|
2960
|
+
if (await recallAICardMessage(card, log)) {
|
|
2961
|
+
finalizedCount += 1;
|
|
2962
|
+
}
|
|
2963
|
+
continue;
|
|
2964
|
+
}
|
|
2904
2965
|
await finalizeStoppedAICard(card, {
|
|
2905
2966
|
reason,
|
|
2906
2967
|
previousContent: entry.lastContent,
|
|
@@ -3177,6 +3238,9 @@ async function commitAICardBlocks(card, options, log) {
|
|
|
3177
3238
|
} catch (err) {
|
|
3178
3239
|
const message = err instanceof Error ? err.message : String(err);
|
|
3179
3240
|
log?.error?.(`[DingTalk][AICard] Finalize via instances API failed: ${message}`);
|
|
3241
|
+
card.state = AICardStatus.FAILED;
|
|
3242
|
+
card.lastUpdated = Date.now();
|
|
3243
|
+
retainPendingCardForRecovery(card, "finalize", log);
|
|
3180
3244
|
throw err;
|
|
3181
3245
|
}
|
|
3182
3246
|
if (card.conversationId && options.content.trim() && card.accountId && card.processQueryKey) {
|
|
@@ -3196,6 +3260,35 @@ async function commitAICardBlocks(card, options, log) {
|
|
|
3196
3260
|
removePendingCard(card, log);
|
|
3197
3261
|
log?.info?.(`[DingTalk][AICard] Card finalized: outTrackId=${card.outTrackId || card.cardInstanceId} state=FINISHED`);
|
|
3198
3262
|
}
|
|
3263
|
+
async function streamAICard(card, content, finished = false, log, options = {}) {
|
|
3264
|
+
if (isCardInTerminalState(card.state)) {
|
|
3265
|
+
log?.debug?.(
|
|
3266
|
+
`[DingTalk][AICard] Skip stream update because card already terminal: outTrackId=${card.cardInstanceId} state=${card.state}`
|
|
3267
|
+
);
|
|
3268
|
+
return;
|
|
3269
|
+
}
|
|
3270
|
+
const template = DINGTALK_CARD_TEMPLATE;
|
|
3271
|
+
try {
|
|
3272
|
+
await putAICardStreamingField(card, template.contentKey, content, finished, log);
|
|
3273
|
+
card.lastStreamedContent = content;
|
|
3274
|
+
if (finished) {
|
|
3275
|
+
card.state = AICardStatus.FINISHED;
|
|
3276
|
+
removePendingCard(card, log);
|
|
3277
|
+
} else if (card.state === AICardStatus.PROCESSING) {
|
|
3278
|
+
card.state = AICardStatus.INPUTING;
|
|
3279
|
+
upsertPendingCard(card, card.storePath, log);
|
|
3280
|
+
}
|
|
3281
|
+
} catch (err) {
|
|
3282
|
+
card.state = AICardStatus.FAILED;
|
|
3283
|
+
card.lastUpdated = Date.now();
|
|
3284
|
+
retainPendingCardForRecovery(card, options.recoveryAction ?? "finalize", log);
|
|
3285
|
+
if (err.response?.status === 500 && err.response?.data?.code === "unknownError") {
|
|
3286
|
+
const errorMsg = "\u26A0\uFE0F **[DingTalk] AI Card \u4E32\u6D41\u66F4\u65B0\u5931\u8D25 (500 unknownError)**\n\n\u8FD9\u901A\u5E38\u8868\u793A\u5F53\u524D\u5185\u7F6E\u6A21\u677F\u5951\u7EA6\u4E0E\u9489\u9489\u4FA7\u6A21\u677F\u5B57\u6BB5\u4E0D\u4E00\u81F4\uFF0C\u5F53\u524D\u53CA\u540E\u7EED\u6D88\u606F\u5C06\u81EA\u52A8\u56DE\u9000\u4E3A Markdown \u53D1\u9001\u3002";
|
|
3287
|
+
await sendTemplateMismatchNotification(card, errorMsg, log);
|
|
3288
|
+
}
|
|
3289
|
+
throw err;
|
|
3290
|
+
}
|
|
3291
|
+
}
|
|
3199
3292
|
function getCardRecallTarget(card) {
|
|
3200
3293
|
const { targetId, isExplicitUser } = stripTargetPrefix(card.conversationId);
|
|
3201
3294
|
const resolvedTarget = resolveOriginalPeerId(targetId);
|
|
@@ -3365,9 +3458,9 @@ function ensureSweepTimer() {
|
|
|
3365
3458
|
return;
|
|
3366
3459
|
}
|
|
3367
3460
|
sweepTimer = setInterval(() => {
|
|
3368
|
-
const
|
|
3461
|
+
const now2 = Date.now();
|
|
3369
3462
|
for (const [key, record] of records) {
|
|
3370
|
-
if (
|
|
3463
|
+
if (now2 - record.registeredAt > CARD_RUN_TTL_MS) {
|
|
3371
3464
|
records.delete(key);
|
|
3372
3465
|
}
|
|
3373
3466
|
}
|
|
@@ -4944,6 +5037,130 @@ async function handleInboundCommandDispatch(params) {
|
|
|
4944
5037
|
return false;
|
|
4945
5038
|
}
|
|
4946
5039
|
|
|
5040
|
+
// src/gateway/inbound-session-queue.ts
|
|
5041
|
+
var SESSION_QUEUE_TTL_MS = 5 * 60 * 1e3;
|
|
5042
|
+
var SESSION_QUEUE_CLEANUP_INTERVAL_MS = 60 * 1e3;
|
|
5043
|
+
var MAX_INBOUND_SESSION_QUEUE_DEPTH = 8;
|
|
5044
|
+
var MAX_INBOUND_SESSION_QUEUE_WAIT_MS = 15 * 60 * 1e3;
|
|
5045
|
+
var sessionQueues = /* @__PURE__ */ new Map();
|
|
5046
|
+
var sessionLastActivity = /* @__PURE__ */ new Map();
|
|
5047
|
+
var sessionQueueDepths = /* @__PURE__ */ new Map();
|
|
5048
|
+
var cleanupTimer = null;
|
|
5049
|
+
var InboundSessionQueueWaitTimeoutError = class extends Error {
|
|
5050
|
+
constructor(queueKey) {
|
|
5051
|
+
super(`Inbound session queue wait timed out for ${queueKey}`);
|
|
5052
|
+
this.name = "InboundSessionQueueWaitTimeoutError";
|
|
5053
|
+
}
|
|
5054
|
+
};
|
|
5055
|
+
function ensureCleanupTimer() {
|
|
5056
|
+
if (cleanupTimer) {
|
|
5057
|
+
return;
|
|
5058
|
+
}
|
|
5059
|
+
cleanupTimer = setInterval(() => {
|
|
5060
|
+
const now2 = Date.now();
|
|
5061
|
+
for (const [key, lastSeen] of sessionLastActivity) {
|
|
5062
|
+
if (now2 - lastSeen > SESSION_QUEUE_TTL_MS && !sessionQueues.has(key)) {
|
|
5063
|
+
sessionLastActivity.delete(key);
|
|
5064
|
+
}
|
|
5065
|
+
}
|
|
5066
|
+
}, SESSION_QUEUE_CLEANUP_INTERVAL_MS);
|
|
5067
|
+
if (typeof cleanupTimer?.unref === "function") {
|
|
5068
|
+
cleanupTimer.unref();
|
|
5069
|
+
}
|
|
5070
|
+
}
|
|
5071
|
+
function isInboundSessionQueueBusy(queueKey) {
|
|
5072
|
+
return sessionQueues.has(queueKey);
|
|
5073
|
+
}
|
|
5074
|
+
function getInboundSessionQueueDepth(queueKey) {
|
|
5075
|
+
return sessionQueueDepths.get(queueKey) ?? 0;
|
|
5076
|
+
}
|
|
5077
|
+
function chainInboundSessionTask(queueKey, task, options = {}) {
|
|
5078
|
+
const hadPriorTask = sessionQueues.has(queueKey);
|
|
5079
|
+
const previousTail = sessionQueues.get(queueKey) ?? Promise.resolve();
|
|
5080
|
+
sessionLastActivity.set(queueKey, Date.now());
|
|
5081
|
+
sessionQueueDepths.set(queueKey, getInboundSessionQueueDepth(queueKey) + 1);
|
|
5082
|
+
ensureCleanupTimer();
|
|
5083
|
+
let timedOut = false;
|
|
5084
|
+
let timeout;
|
|
5085
|
+
let resolveWaitingCaller;
|
|
5086
|
+
let rejectWaitingCaller;
|
|
5087
|
+
const maxQueueWaitMs = Math.max(0, options.maxQueueWaitMs ?? 0);
|
|
5088
|
+
const caller = maxQueueWaitMs > 0 && hadPriorTask ? new Promise((resolve3, reject) => {
|
|
5089
|
+
resolveWaitingCaller = resolve3;
|
|
5090
|
+
rejectWaitingCaller = reject;
|
|
5091
|
+
timeout = setTimeout(() => {
|
|
5092
|
+
timedOut = true;
|
|
5093
|
+
reject(new InboundSessionQueueWaitTimeoutError(queueKey));
|
|
5094
|
+
}, maxQueueWaitMs);
|
|
5095
|
+
if (typeof timeout.unref === "function") {
|
|
5096
|
+
timeout.unref();
|
|
5097
|
+
}
|
|
5098
|
+
}) : void 0;
|
|
5099
|
+
if (caller) {
|
|
5100
|
+
void caller.catch(() => void 0);
|
|
5101
|
+
}
|
|
5102
|
+
const current = previousTail.then(() => {
|
|
5103
|
+
if (timeout) {
|
|
5104
|
+
clearTimeout(timeout);
|
|
5105
|
+
timeout = void 0;
|
|
5106
|
+
}
|
|
5107
|
+
if (timedOut) {
|
|
5108
|
+
throw new InboundSessionQueueWaitTimeoutError(queueKey);
|
|
5109
|
+
}
|
|
5110
|
+
return task();
|
|
5111
|
+
});
|
|
5112
|
+
const tail = current.then(
|
|
5113
|
+
() => void 0,
|
|
5114
|
+
() => {
|
|
5115
|
+
}
|
|
5116
|
+
);
|
|
5117
|
+
sessionQueues.set(queueKey, tail);
|
|
5118
|
+
const cleanup = () => {
|
|
5119
|
+
const nextDepth = Math.max(0, getInboundSessionQueueDepth(queueKey) - 1);
|
|
5120
|
+
if (nextDepth) {
|
|
5121
|
+
sessionQueueDepths.set(queueKey, nextDepth);
|
|
5122
|
+
} else {
|
|
5123
|
+
sessionQueueDepths.delete(queueKey);
|
|
5124
|
+
}
|
|
5125
|
+
if (sessionQueues.get(queueKey) === tail) {
|
|
5126
|
+
sessionQueues.delete(queueKey);
|
|
5127
|
+
sessionLastActivity.delete(queueKey);
|
|
5128
|
+
}
|
|
5129
|
+
};
|
|
5130
|
+
void current.then(cleanup, cleanup);
|
|
5131
|
+
if (!caller) {
|
|
5132
|
+
return current;
|
|
5133
|
+
}
|
|
5134
|
+
void current.then(
|
|
5135
|
+
(value) => {
|
|
5136
|
+
if (timeout) {
|
|
5137
|
+
clearTimeout(timeout);
|
|
5138
|
+
}
|
|
5139
|
+
resolveWaitingCaller?.(value);
|
|
5140
|
+
},
|
|
5141
|
+
(error) => {
|
|
5142
|
+
if (timeout) {
|
|
5143
|
+
clearTimeout(timeout);
|
|
5144
|
+
}
|
|
5145
|
+
rejectWaitingCaller?.(error);
|
|
5146
|
+
}
|
|
5147
|
+
);
|
|
5148
|
+
return caller;
|
|
5149
|
+
}
|
|
5150
|
+
var QUEUE_BUSY_ACK_PHRASES = [
|
|
5151
|
+
"\u4E0A\u4E00\u6761\u8FD8\u6CA1\u7ED3\u675F\uFF0C\u8FD9\u6761\u6211\u5DF2\u7ECF\u8BB0\u4E0B\uFF0C\u7A0D\u540E\u6309\u987A\u5E8F\u7EE7\u7EED\u5904\u7406\u3002",
|
|
5152
|
+
"\u5F53\u524D\u8FD8\u5728\u5FD9\uFF0C\u4F60\u7684\u65B0\u6D88\u606F\u5DF2\u7ECF\u6392\u961F\uFF0C\u4E0A\u4E00\u6761\u5B8C\u6210\u540E\u6211\u9A6C\u4E0A\u7EE7\u7EED\u3002",
|
|
5153
|
+
"\u6211\u8FD9\u8FB9\u8FD8\u5728\u5904\u7406\u4E0A\u4E00\u6761\uFF0C\u8FD9\u6761\u5DF2\u52A0\u5165\u961F\u5217\uFF0C\u5B8C\u6210\u540E\u7EE7\u7EED\u5904\u7406\u3002"
|
|
5154
|
+
];
|
|
5155
|
+
function pickQueueBusyAckPhrase(seed) {
|
|
5156
|
+
const list = QUEUE_BUSY_ACK_PHRASES;
|
|
5157
|
+
const index = seed === void 0 ? Math.floor(Math.random() * list.length) : seed % list.length;
|
|
5158
|
+
return list[index];
|
|
5159
|
+
}
|
|
5160
|
+
|
|
5161
|
+
// src/send-service.ts
|
|
5162
|
+
import * as path6 from "node:path";
|
|
5163
|
+
|
|
4947
5164
|
// src/media-utils.ts
|
|
4948
5165
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
4949
5166
|
import * as os4 from "node:os";
|
|
@@ -6217,96 +6434,6 @@ function extractMessageContent(data) {
|
|
|
6217
6434
|
};
|
|
6218
6435
|
}
|
|
6219
6436
|
|
|
6220
|
-
// src/messaging/attachment-text-extractor.ts
|
|
6221
|
-
import fs4 from "node:fs/promises";
|
|
6222
|
-
import path6 from "node:path";
|
|
6223
|
-
var MAX_EXTRACTED_TEXT_CHARS = 6e3;
|
|
6224
|
-
var MAX_ATTACHMENT_EXTRACT_BYTES = 2 * 1024 * 1024;
|
|
6225
|
-
async function isFileTooLarge(filePath) {
|
|
6226
|
-
const stat = await fs4.stat(filePath);
|
|
6227
|
-
return stat.size > MAX_ATTACHMENT_EXTRACT_BYTES;
|
|
6228
|
-
}
|
|
6229
|
-
function isTextLikeMimeType(mimeType) {
|
|
6230
|
-
if (!mimeType) {
|
|
6231
|
-
return false;
|
|
6232
|
-
}
|
|
6233
|
-
return mimeType.startsWith("text/") || mimeType === "application/json" || mimeType === "application/xml" || mimeType === "application/javascript";
|
|
6234
|
-
}
|
|
6235
|
-
function normalizeWhitespace(text) {
|
|
6236
|
-
return text.replace(/\r\n/g, "\n").split("\0").join("").replace(/[ \t]{2,}/g, " ").replace(/[ \t]+\n/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
6237
|
-
}
|
|
6238
|
-
function limitExtractedText(text) {
|
|
6239
|
-
const normalized = normalizeWhitespace(text);
|
|
6240
|
-
if (!normalized) {
|
|
6241
|
-
return null;
|
|
6242
|
-
}
|
|
6243
|
-
const truncated = normalized.length > MAX_EXTRACTED_TEXT_CHARS;
|
|
6244
|
-
const limited = truncated ? `${normalized.slice(0, MAX_EXTRACTED_TEXT_CHARS)}
|
|
6245
|
-
|
|
6246
|
-
[\u5185\u5BB9\u5DF2\u622A\u65AD]` : normalized;
|
|
6247
|
-
return {
|
|
6248
|
-
text: limited,
|
|
6249
|
-
truncated,
|
|
6250
|
-
sourceType: "text"
|
|
6251
|
-
};
|
|
6252
|
-
}
|
|
6253
|
-
function stripHtml(html) {
|
|
6254
|
-
return html.replace(/<script[\s\S]*?<\/script>/gi, " ").replace(/<style[\s\S]*?<\/style>/gi, " ").replace(/<[^>]+>/g, " ").replace(/ /gi, " ").replace(/</gi, "<").replace(/>/gi, ">").replace(/&/gi, "&").replace(/'/gi, "'").replace(/"/gi, '"');
|
|
6255
|
-
}
|
|
6256
|
-
async function extractTextLikeFile(filePath) {
|
|
6257
|
-
const raw = await fs4.readFile(filePath, "utf8");
|
|
6258
|
-
return limitExtractedText(raw);
|
|
6259
|
-
}
|
|
6260
|
-
async function extractPdf(filePath) {
|
|
6261
|
-
const pdfParseModule = await import("pdf-parse");
|
|
6262
|
-
const fileBuffer = await fs4.readFile(filePath);
|
|
6263
|
-
const parser = new pdfParseModule.PDFParse({ data: new Uint8Array(fileBuffer) });
|
|
6264
|
-
try {
|
|
6265
|
-
const result = await parser.getText();
|
|
6266
|
-
const limited = limitExtractedText(result.text || "");
|
|
6267
|
-
return limited ? { ...limited, sourceType: "pdf" } : null;
|
|
6268
|
-
} finally {
|
|
6269
|
-
await parser.destroy();
|
|
6270
|
-
}
|
|
6271
|
-
}
|
|
6272
|
-
async function extractDocx(filePath) {
|
|
6273
|
-
const mammothModule = await import("mammoth");
|
|
6274
|
-
const result = await mammothModule.extractRawText({ path: filePath });
|
|
6275
|
-
const limited = limitExtractedText(result.value || "");
|
|
6276
|
-
return limited ? { ...limited, sourceType: "docx" } : null;
|
|
6277
|
-
}
|
|
6278
|
-
async function extractHtml(filePath) {
|
|
6279
|
-
const raw = await fs4.readFile(filePath, "utf8");
|
|
6280
|
-
const limited = limitExtractedText(stripHtml(raw));
|
|
6281
|
-
return limited ? { ...limited, sourceType: "html" } : null;
|
|
6282
|
-
}
|
|
6283
|
-
async function extractAttachmentText(input) {
|
|
6284
|
-
const mimeType = input.mimeType?.toLowerCase();
|
|
6285
|
-
const fileName = (input.fileName || path6.basename(input.path)).toLowerCase();
|
|
6286
|
-
if (mimeType?.startsWith("image/") || mimeType?.startsWith("audio/") || mimeType?.startsWith("video/")) {
|
|
6287
|
-
return null;
|
|
6288
|
-
}
|
|
6289
|
-
if (await isFileTooLarge(input.path)) {
|
|
6290
|
-
return null;
|
|
6291
|
-
}
|
|
6292
|
-
if (mimeType === "application/vnd.openxmlformats-officedocument.wordprocessingml.document" || fileName.endsWith(".docx")) {
|
|
6293
|
-
return extractDocx(input.path);
|
|
6294
|
-
}
|
|
6295
|
-
if (mimeType === "application/pdf" || fileName.endsWith(".pdf")) {
|
|
6296
|
-
return extractPdf(input.path);
|
|
6297
|
-
}
|
|
6298
|
-
if (mimeType === "text/html" || mimeType === "application/xhtml+xml" || fileName.endsWith(".html") || fileName.endsWith(".htm")) {
|
|
6299
|
-
return extractHtml(input.path);
|
|
6300
|
-
}
|
|
6301
|
-
if (isTextLikeMimeType(mimeType) || fileName.endsWith(".txt") || fileName.endsWith(".md") || fileName.endsWith(".markdown") || fileName.endsWith(".csv") || fileName.endsWith(".json") || fileName.endsWith(".xml") || fileName.endsWith(".log")) {
|
|
6302
|
-
return extractTextLikeFile(input.path);
|
|
6303
|
-
}
|
|
6304
|
-
return null;
|
|
6305
|
-
}
|
|
6306
|
-
|
|
6307
|
-
// src/send-service.ts
|
|
6308
|
-
import * as path7 from "node:path";
|
|
6309
|
-
|
|
6310
6437
|
// src/proactive-risk-registry.ts
|
|
6311
6438
|
var DEFAULT_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
6312
6439
|
var store = /* @__PURE__ */ new Map();
|
|
@@ -6694,9 +6821,9 @@ async function sendProactiveMedia(config, target, mediaPath, mediaType, options
|
|
|
6694
6821
|
const durationMs = uploadedDurationMs ?? await getVoiceDurationMs(mediaPath, mediaType, log, { preReadBuffer: buffer });
|
|
6695
6822
|
msgParam = JSON.stringify({ mediaId, duration: String(durationMs) });
|
|
6696
6823
|
} else {
|
|
6697
|
-
const filename =
|
|
6824
|
+
const filename = path6.basename(mediaPath);
|
|
6698
6825
|
const defaultExt = mediaType === "video" ? "mp4" : "file";
|
|
6699
|
-
const ext =
|
|
6826
|
+
const ext = path6.extname(mediaPath).slice(1) || defaultExt;
|
|
6700
6827
|
msgKey = "sampleFile";
|
|
6701
6828
|
msgParam = JSON.stringify({ mediaId, fileName: filename, fileType: ext });
|
|
6702
6829
|
}
|
|
@@ -6799,7 +6926,7 @@ async function sendMedia(config, target, mediaInput, options = {}) {
|
|
|
6799
6926
|
let preparedMedia;
|
|
6800
6927
|
try {
|
|
6801
6928
|
preparedMedia = await prepareMediaInput(mediaInput, log, config.mediaUrlAllowlist);
|
|
6802
|
-
const mediaPath = preparedMedia.cleanup ? preparedMedia.path :
|
|
6929
|
+
const mediaPath = preparedMedia.cleanup ? preparedMedia.path : path6.resolve(process.cwd(), preparedMedia.path);
|
|
6803
6930
|
const mediaType = resolveOutboundMediaType({
|
|
6804
6931
|
mediaType: options.mediaType,
|
|
6805
6932
|
mediaPath,
|
|
@@ -6855,7 +6982,7 @@ async function sendBySession(config, sessionWebhook, text, options = {}) {
|
|
|
6855
6982
|
mediaLocalRoots: options.mediaLocalRoots
|
|
6856
6983
|
});
|
|
6857
6984
|
if (uploadResult) {
|
|
6858
|
-
const imageMarkdown = ``;
|
|
6859
6986
|
text = text ? `${text}
|
|
6860
6987
|
|
|
6861
6988
|
${imageMarkdown}` : imageMarkdown;
|
|
@@ -7038,70 +7165,348 @@ async function sendMessage(config, conversationId, text, options = {}) {
|
|
|
7038
7165
|
}
|
|
7039
7166
|
}
|
|
7040
7167
|
|
|
7041
|
-
// src/
|
|
7042
|
-
var
|
|
7043
|
-
var
|
|
7044
|
-
|
|
7045
|
-
|
|
7046
|
-
|
|
7047
|
-
function
|
|
7048
|
-
|
|
7049
|
-
|
|
7050
|
-
|
|
7051
|
-
const
|
|
7052
|
-
|
|
7053
|
-
|
|
7054
|
-
|
|
7168
|
+
// src/gateway/inbound-session-queue-dispatcher.ts
|
|
7169
|
+
var QUEUE_FULL_ACK = "\u5F53\u524D\u6D88\u606F\u8F83\u591A\uFF0C\u5DF2\u8FBE\u5230\u672C\u4F1A\u8BDD\u6392\u961F\u4E0A\u9650\uFF1B\u8BF7\u7B49\u5F85\u4E0A\u4E00\u8F6E\u5B8C\u6210\u540E\u518D\u53D1\u9001\u3002";
|
|
7170
|
+
var QUEUE_WAIT_TIMEOUT_ACK = "\u4E0A\u4E00\u8F6E\u5904\u7406\u65F6\u95F4\u8F83\u957F\uFF0C\u8FD9\u6761\u6D88\u606F\u672A\u6267\u884C\uFF1B\u8BF7\u7A0D\u540E\u91CD\u65B0\u53D1\u9001\u3002";
|
|
7171
|
+
var QUEUE_HANDLER_FAILURE_ACK = "\u672C\u6B21\u5904\u7406\u5F02\u5E38\uFF0C\u672A\u80FD\u5B8C\u6210\uFF1B\u8BF7\u7A0D\u540E\u91CD\u65B0\u53D1\u9001\u3002";
|
|
7172
|
+
var MIN_QUEUE_ACK_CARD_VISIBLE_MS = 750;
|
|
7173
|
+
var queuedAckVisibleAt = /* @__PURE__ */ new WeakMap();
|
|
7174
|
+
function shouldPrepareQueueAckCard(input) {
|
|
7175
|
+
return input.dingtalkConfig.messageType === "card";
|
|
7176
|
+
}
|
|
7177
|
+
async function keepQueueAckCardVisible(card) {
|
|
7178
|
+
const visibleAt = queuedAckVisibleAt.get(card);
|
|
7179
|
+
if (!visibleAt) {
|
|
7180
|
+
return;
|
|
7181
|
+
}
|
|
7182
|
+
const remainingMs = MIN_QUEUE_ACK_CARD_VISIBLE_MS - (Date.now() - visibleAt);
|
|
7183
|
+
if (remainingMs > 0) {
|
|
7184
|
+
await new Promise((resolve3) => setTimeout(resolve3, remainingMs));
|
|
7185
|
+
}
|
|
7055
7186
|
}
|
|
7056
|
-
async function
|
|
7057
|
-
|
|
7058
|
-
|
|
7187
|
+
async function settleUnusedQueueAckCard(input, card) {
|
|
7188
|
+
if (isCardInTerminalState(card.state)) {
|
|
7189
|
+
return;
|
|
7190
|
+
}
|
|
7059
7191
|
try {
|
|
7060
|
-
|
|
7061
|
-
|
|
7062
|
-
accountId: args.accountId,
|
|
7063
|
-
storePath: args.storePath,
|
|
7064
|
-
conversationId: args.conversationId,
|
|
7065
|
-
sessionWebhook: args.sessionWebhook,
|
|
7066
|
-
forceMarkdown: true
|
|
7067
|
-
});
|
|
7068
|
-
if (!result.ok) {
|
|
7069
|
-
args.log?.warn?.(
|
|
7070
|
-
`[DingTalk] BTW reply delivery returned not-ok: ${result.error ?? "unknown"}`
|
|
7071
|
-
);
|
|
7192
|
+
if (await recallAICardMessage(card, input.log)) {
|
|
7193
|
+
return;
|
|
7072
7194
|
}
|
|
7073
|
-
return { ok: result.ok, error: result.error };
|
|
7074
7195
|
} catch (err) {
|
|
7075
|
-
|
|
7076
|
-
|
|
7077
|
-
|
|
7196
|
+
input.log?.warn?.(
|
|
7197
|
+
`[DingTalk] Failed to recall unused queue acknowledgement card: ${err instanceof Error ? err.message : String(err)}`
|
|
7198
|
+
);
|
|
7078
7199
|
}
|
|
7200
|
+
await sendQueueTerminalAck(input, "\u5DF2\u7ED3\u675F\u6392\u961F\u786E\u8BA4\uFF0C\u8BF7\u4EE5\u672C\u6B21\u5B9E\u9645\u56DE\u590D\u4E3A\u51C6\u3002", card);
|
|
7079
7201
|
}
|
|
7080
|
-
|
|
7081
|
-
|
|
7082
|
-
|
|
7083
|
-
for (const candidate of candidates) {
|
|
7084
|
-
if (typeof candidate === "string" && candidate.trim()) {
|
|
7085
|
-
return candidate.trim();
|
|
7086
|
-
}
|
|
7202
|
+
async function settleFailedQueueAckCard(input, card) {
|
|
7203
|
+
if (isCardInTerminalState(card.state)) {
|
|
7204
|
+
return;
|
|
7087
7205
|
}
|
|
7088
|
-
|
|
7206
|
+
await sendQueueTerminalAck(input, QUEUE_HANDLER_FAILURE_ACK, card);
|
|
7089
7207
|
}
|
|
7090
|
-
function
|
|
7091
|
-
|
|
7092
|
-
|
|
7093
|
-
|
|
7208
|
+
async function dispatchInboundViaSessionQueue(input, handler) {
|
|
7209
|
+
const queueKey = input.sessionKey;
|
|
7210
|
+
if (!queueKey) {
|
|
7211
|
+
return handler(void 0);
|
|
7212
|
+
}
|
|
7213
|
+
const wasBusy = isInboundSessionQueueBusy(queueKey);
|
|
7214
|
+
if (getInboundSessionQueueDepth(queueKey) >= MAX_INBOUND_SESSION_QUEUE_DEPTH) {
|
|
7215
|
+
await sendQueueTerminalAck(input, QUEUE_FULL_ACK);
|
|
7216
|
+
return void 0;
|
|
7217
|
+
}
|
|
7218
|
+
let queuedAckState = "queued";
|
|
7219
|
+
const preCreatedCardPromise = wasBusy && shouldPrepareQueueAckCard(input) ? tryPrepareQueueAckCard(
|
|
7220
|
+
input,
|
|
7221
|
+
() => queuedAckState === "timed-out" ? { content: QUEUE_WAIT_TIMEOUT_ACK, finished: true } : { content: pickQueueBusyAckPhrase(), finished: false }
|
|
7222
|
+
) : void 0;
|
|
7223
|
+
try {
|
|
7224
|
+
return await chainInboundSessionTask(
|
|
7225
|
+
queueKey,
|
|
7226
|
+
async () => {
|
|
7227
|
+
const preCreatedCard = preCreatedCardPromise ? await preCreatedCardPromise : void 0;
|
|
7228
|
+
if (!preCreatedCard) {
|
|
7229
|
+
return handler(void 0);
|
|
7230
|
+
}
|
|
7231
|
+
await keepQueueAckCardVisible(preCreatedCard);
|
|
7232
|
+
let handlerFailed = false;
|
|
7233
|
+
try {
|
|
7234
|
+
return await handler(preCreatedCard);
|
|
7235
|
+
} catch (err) {
|
|
7236
|
+
handlerFailed = true;
|
|
7237
|
+
await settleFailedQueueAckCard(input, preCreatedCard);
|
|
7238
|
+
throw err;
|
|
7239
|
+
} finally {
|
|
7240
|
+
if (!handlerFailed && !isCardInTerminalState(preCreatedCard.state)) {
|
|
7241
|
+
await settleUnusedQueueAckCard(input, preCreatedCard);
|
|
7242
|
+
}
|
|
7243
|
+
}
|
|
7244
|
+
},
|
|
7245
|
+
{
|
|
7246
|
+
maxQueueWaitMs: wasBusy ? MAX_INBOUND_SESSION_QUEUE_WAIT_MS : void 0
|
|
7247
|
+
}
|
|
7248
|
+
);
|
|
7249
|
+
} catch (err) {
|
|
7250
|
+
if (err instanceof InboundSessionQueueWaitTimeoutError) {
|
|
7251
|
+
queuedAckState = "timed-out";
|
|
7252
|
+
await sendQueueTerminalAck(
|
|
7253
|
+
input,
|
|
7254
|
+
QUEUE_WAIT_TIMEOUT_ACK,
|
|
7255
|
+
preCreatedCardPromise ? await preCreatedCardPromise : void 0
|
|
7256
|
+
);
|
|
7257
|
+
return void 0;
|
|
7094
7258
|
}
|
|
7259
|
+
throw err;
|
|
7095
7260
|
}
|
|
7096
|
-
return void 0;
|
|
7097
7261
|
}
|
|
7098
|
-
function
|
|
7099
|
-
const
|
|
7100
|
-
|
|
7101
|
-
|
|
7102
|
-
|
|
7103
|
-
|
|
7104
|
-
|
|
7262
|
+
async function tryPrepareQueueAckCard(input, ack) {
|
|
7263
|
+
const { dingtalkConfig, data, log, to, storePath, quoteContent } = input;
|
|
7264
|
+
if (!data) {
|
|
7265
|
+
return void 0;
|
|
7266
|
+
}
|
|
7267
|
+
if (!to) {
|
|
7268
|
+
return void 0;
|
|
7269
|
+
}
|
|
7270
|
+
let card = null;
|
|
7271
|
+
let ackFinished = false;
|
|
7272
|
+
try {
|
|
7273
|
+
card = await createAICard(dingtalkConfig, to, log, {
|
|
7274
|
+
accountId: input.accountId,
|
|
7275
|
+
storePath,
|
|
7276
|
+
quoteContent
|
|
7277
|
+
});
|
|
7278
|
+
if (!card) {
|
|
7279
|
+
return void 0;
|
|
7280
|
+
}
|
|
7281
|
+
const { content, finished } = ack();
|
|
7282
|
+
ackFinished = finished;
|
|
7283
|
+
await streamAICard(card, content, finished, log, {
|
|
7284
|
+
recoveryAction: finished ? "finalize" : "recall"
|
|
7285
|
+
});
|
|
7286
|
+
if (!finished) {
|
|
7287
|
+
queuedAckVisibleAt.set(card, Date.now());
|
|
7288
|
+
}
|
|
7289
|
+
if (finished) {
|
|
7290
|
+
return card;
|
|
7291
|
+
}
|
|
7292
|
+
void attachNativeAckReaction(
|
|
7293
|
+
dingtalkConfig,
|
|
7294
|
+
{ msgId: data.msgId, conversationId: data.conversationId },
|
|
7295
|
+
log
|
|
7296
|
+
).catch((err) => {
|
|
7297
|
+
log?.debug?.(
|
|
7298
|
+
`[DingTalk] Queue-busy ack reaction attach failed: ${err instanceof Error ? err.message : String(err)}`
|
|
7299
|
+
);
|
|
7300
|
+
});
|
|
7301
|
+
log?.info?.(
|
|
7302
|
+
`[DingTalk] Inbound message queued behind active run for session=${input.sessionKey}; pre-created ACK card outTrackId=${card.cardInstanceId}.`
|
|
7303
|
+
);
|
|
7304
|
+
return card;
|
|
7305
|
+
} catch (err) {
|
|
7306
|
+
if (card && !ackFinished) {
|
|
7307
|
+
try {
|
|
7308
|
+
await recallAICardMessage(card, log);
|
|
7309
|
+
} catch (recallErr) {
|
|
7310
|
+
log?.warn?.(
|
|
7311
|
+
`[DingTalk] Failed to recall queue ACK card after prepare failure: ${recallErr instanceof Error ? recallErr.message : String(recallErr)}`
|
|
7312
|
+
);
|
|
7313
|
+
}
|
|
7314
|
+
}
|
|
7315
|
+
log?.warn?.(
|
|
7316
|
+
`[DingTalk] Queue-busy ACK card prepare failed: ${err instanceof Error ? err.message : String(err)}`
|
|
7317
|
+
);
|
|
7318
|
+
return void 0;
|
|
7319
|
+
}
|
|
7320
|
+
}
|
|
7321
|
+
async function sendQueueTerminalAck(input, content, preCreatedCard) {
|
|
7322
|
+
const { dingtalkConfig, data, log, to, storePath } = input;
|
|
7323
|
+
try {
|
|
7324
|
+
if (preCreatedCard) {
|
|
7325
|
+
try {
|
|
7326
|
+
await streamAICard(preCreatedCard, content, true, log);
|
|
7327
|
+
return;
|
|
7328
|
+
} catch (err) {
|
|
7329
|
+
log?.warn?.(
|
|
7330
|
+
`[DingTalk] Queue acknowledgement card finalization failed; falling back to text: ${err instanceof Error ? err.message : String(err)}`
|
|
7331
|
+
);
|
|
7332
|
+
}
|
|
7333
|
+
} else {
|
|
7334
|
+
const card = await tryPrepareQueueAckCard(input, () => ({ content, finished: true }));
|
|
7335
|
+
if (card) {
|
|
7336
|
+
return;
|
|
7337
|
+
}
|
|
7338
|
+
}
|
|
7339
|
+
if (!to) {
|
|
7340
|
+
return;
|
|
7341
|
+
}
|
|
7342
|
+
const result = await sendMessage(dingtalkConfig, to, content, {
|
|
7343
|
+
sessionWebhook: data.sessionWebhook,
|
|
7344
|
+
log,
|
|
7345
|
+
accountId: input.accountId,
|
|
7346
|
+
storePath,
|
|
7347
|
+
conversationId: data.conversationId
|
|
7348
|
+
});
|
|
7349
|
+
if (!result.ok) {
|
|
7350
|
+
log?.warn?.(`[DingTalk] Queue terminal acknowledgement failed: ${result.error || "unknown"}`);
|
|
7351
|
+
}
|
|
7352
|
+
} catch (err) {
|
|
7353
|
+
log?.warn?.(
|
|
7354
|
+
`[DingTalk] Queue terminal acknowledgement delivery failed: ${err instanceof Error ? err.message : String(err)}`
|
|
7355
|
+
);
|
|
7356
|
+
}
|
|
7357
|
+
}
|
|
7358
|
+
|
|
7359
|
+
// src/messaging/attachment-text-extractor.ts
|
|
7360
|
+
import fs4 from "node:fs/promises";
|
|
7361
|
+
import path7 from "node:path";
|
|
7362
|
+
var MAX_EXTRACTED_TEXT_CHARS = 6e3;
|
|
7363
|
+
var MAX_ATTACHMENT_EXTRACT_BYTES = 2 * 1024 * 1024;
|
|
7364
|
+
async function isFileTooLarge(filePath) {
|
|
7365
|
+
const stat = await fs4.stat(filePath);
|
|
7366
|
+
return stat.size > MAX_ATTACHMENT_EXTRACT_BYTES;
|
|
7367
|
+
}
|
|
7368
|
+
function isTextLikeMimeType(mimeType) {
|
|
7369
|
+
if (!mimeType) {
|
|
7370
|
+
return false;
|
|
7371
|
+
}
|
|
7372
|
+
return mimeType.startsWith("text/") || mimeType === "application/json" || mimeType === "application/xml" || mimeType === "application/javascript";
|
|
7373
|
+
}
|
|
7374
|
+
function normalizeWhitespace(text) {
|
|
7375
|
+
return text.replace(/\r\n/g, "\n").split("\0").join("").replace(/[ \t]{2,}/g, " ").replace(/[ \t]+\n/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
7376
|
+
}
|
|
7377
|
+
function limitExtractedText(text) {
|
|
7378
|
+
const normalized = normalizeWhitespace(text);
|
|
7379
|
+
if (!normalized) {
|
|
7380
|
+
return null;
|
|
7381
|
+
}
|
|
7382
|
+
const truncated = normalized.length > MAX_EXTRACTED_TEXT_CHARS;
|
|
7383
|
+
const limited = truncated ? `${normalized.slice(0, MAX_EXTRACTED_TEXT_CHARS)}
|
|
7384
|
+
|
|
7385
|
+
[\u5185\u5BB9\u5DF2\u622A\u65AD]` : normalized;
|
|
7386
|
+
return {
|
|
7387
|
+
text: limited,
|
|
7388
|
+
truncated,
|
|
7389
|
+
sourceType: "text"
|
|
7390
|
+
};
|
|
7391
|
+
}
|
|
7392
|
+
function stripHtml(html) {
|
|
7393
|
+
return html.replace(/<script[\s\S]*?<\/script>/gi, " ").replace(/<style[\s\S]*?<\/style>/gi, " ").replace(/<[^>]+>/g, " ").replace(/ /gi, " ").replace(/</gi, "<").replace(/>/gi, ">").replace(/&/gi, "&").replace(/'/gi, "'").replace(/"/gi, '"');
|
|
7394
|
+
}
|
|
7395
|
+
async function extractTextLikeFile(filePath) {
|
|
7396
|
+
const raw = await fs4.readFile(filePath, "utf8");
|
|
7397
|
+
return limitExtractedText(raw);
|
|
7398
|
+
}
|
|
7399
|
+
async function extractPdf(filePath) {
|
|
7400
|
+
const pdfParseModule = await import("pdf-parse");
|
|
7401
|
+
const fileBuffer = await fs4.readFile(filePath);
|
|
7402
|
+
const parser = new pdfParseModule.PDFParse({ data: new Uint8Array(fileBuffer) });
|
|
7403
|
+
try {
|
|
7404
|
+
const result = await parser.getText();
|
|
7405
|
+
const limited = limitExtractedText(result.text || "");
|
|
7406
|
+
return limited ? { ...limited, sourceType: "pdf" } : null;
|
|
7407
|
+
} finally {
|
|
7408
|
+
await parser.destroy();
|
|
7409
|
+
}
|
|
7410
|
+
}
|
|
7411
|
+
async function extractDocx(filePath) {
|
|
7412
|
+
const mammothModule = await import("mammoth");
|
|
7413
|
+
const result = await mammothModule.extractRawText({ path: filePath });
|
|
7414
|
+
const limited = limitExtractedText(result.value || "");
|
|
7415
|
+
return limited ? { ...limited, sourceType: "docx" } : null;
|
|
7416
|
+
}
|
|
7417
|
+
async function extractHtml(filePath) {
|
|
7418
|
+
const raw = await fs4.readFile(filePath, "utf8");
|
|
7419
|
+
const limited = limitExtractedText(stripHtml(raw));
|
|
7420
|
+
return limited ? { ...limited, sourceType: "html" } : null;
|
|
7421
|
+
}
|
|
7422
|
+
async function extractAttachmentText(input) {
|
|
7423
|
+
const mimeType = input.mimeType?.toLowerCase();
|
|
7424
|
+
const fileName = (input.fileName || path7.basename(input.path)).toLowerCase();
|
|
7425
|
+
if (mimeType?.startsWith("image/") || mimeType?.startsWith("audio/") || mimeType?.startsWith("video/")) {
|
|
7426
|
+
return null;
|
|
7427
|
+
}
|
|
7428
|
+
if (await isFileTooLarge(input.path)) {
|
|
7429
|
+
return null;
|
|
7430
|
+
}
|
|
7431
|
+
if (mimeType === "application/vnd.openxmlformats-officedocument.wordprocessingml.document" || fileName.endsWith(".docx")) {
|
|
7432
|
+
return extractDocx(input.path);
|
|
7433
|
+
}
|
|
7434
|
+
if (mimeType === "application/pdf" || fileName.endsWith(".pdf")) {
|
|
7435
|
+
return extractPdf(input.path);
|
|
7436
|
+
}
|
|
7437
|
+
if (mimeType === "text/html" || mimeType === "application/xhtml+xml" || fileName.endsWith(".html") || fileName.endsWith(".htm")) {
|
|
7438
|
+
return extractHtml(input.path);
|
|
7439
|
+
}
|
|
7440
|
+
if (isTextLikeMimeType(mimeType) || fileName.endsWith(".txt") || fileName.endsWith(".md") || fileName.endsWith(".markdown") || fileName.endsWith(".csv") || fileName.endsWith(".json") || fileName.endsWith(".xml") || fileName.endsWith(".log")) {
|
|
7441
|
+
return extractTextLikeFile(input.path);
|
|
7442
|
+
}
|
|
7443
|
+
return null;
|
|
7444
|
+
}
|
|
7445
|
+
|
|
7446
|
+
// src/messaging/btw-deliver.ts
|
|
7447
|
+
var MAX_QUESTION_LENGTH = 80;
|
|
7448
|
+
var LEADING_MENTIONS_RE = /^(?:@\S+\s+)*/u;
|
|
7449
|
+
function stripLeadingMentions(text) {
|
|
7450
|
+
return text.replace(LEADING_MENTIONS_RE, "");
|
|
7451
|
+
}
|
|
7452
|
+
function buildBtwBlockquote(senderName, rawQuestion) {
|
|
7453
|
+
const stripped = stripLeadingMentions(rawQuestion);
|
|
7454
|
+
const codePoints = Array.from(stripped);
|
|
7455
|
+
const truncated = codePoints.length > MAX_QUESTION_LENGTH ? `${codePoints.slice(0, MAX_QUESTION_LENGTH).join("")}\u2026` : stripped;
|
|
7456
|
+
const senderPrefix = senderName ? `${senderName}: ` : "";
|
|
7457
|
+
return `> ${senderPrefix}${truncated}
|
|
7458
|
+
|
|
7459
|
+
`;
|
|
7460
|
+
}
|
|
7461
|
+
async function deliverBtwReply(args) {
|
|
7462
|
+
const blockquote = buildBtwBlockquote(args.senderName, args.rawQuestion);
|
|
7463
|
+
const fullText = `${blockquote}${args.replyText}`;
|
|
7464
|
+
try {
|
|
7465
|
+
const result = await sendMessage(args.config, args.to, fullText, {
|
|
7466
|
+
log: args.log,
|
|
7467
|
+
accountId: args.accountId,
|
|
7468
|
+
storePath: args.storePath,
|
|
7469
|
+
conversationId: args.conversationId,
|
|
7470
|
+
sessionWebhook: args.sessionWebhook,
|
|
7471
|
+
forceMarkdown: true
|
|
7472
|
+
});
|
|
7473
|
+
if (!result.ok) {
|
|
7474
|
+
args.log?.warn?.(
|
|
7475
|
+
`[DingTalk] BTW reply delivery returned not-ok: ${result.error ?? "unknown"}`
|
|
7476
|
+
);
|
|
7477
|
+
}
|
|
7478
|
+
return { ok: result.ok, error: result.error };
|
|
7479
|
+
} catch (err) {
|
|
7480
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
7481
|
+
args.log?.warn?.(`[DingTalk] BTW reply delivery threw: ${error}`);
|
|
7482
|
+
return { ok: false, error };
|
|
7483
|
+
}
|
|
7484
|
+
}
|
|
7485
|
+
|
|
7486
|
+
// src/messaging/quoted-ref.ts
|
|
7487
|
+
function firstTrimmedString3(...candidates) {
|
|
7488
|
+
for (const candidate of candidates) {
|
|
7489
|
+
if (typeof candidate === "string" && candidate.trim()) {
|
|
7490
|
+
return candidate.trim();
|
|
7491
|
+
}
|
|
7492
|
+
}
|
|
7493
|
+
return void 0;
|
|
7494
|
+
}
|
|
7495
|
+
function firstFiniteNumber(...candidates) {
|
|
7496
|
+
for (const candidate of candidates) {
|
|
7497
|
+
if (typeof candidate === "number" && Number.isFinite(candidate)) {
|
|
7498
|
+
return candidate;
|
|
7499
|
+
}
|
|
7500
|
+
}
|
|
7501
|
+
return void 0;
|
|
7502
|
+
}
|
|
7503
|
+
function buildInboundQuotedRef(data, content) {
|
|
7504
|
+
const repliedMsg = data.text?.repliedMsg;
|
|
7505
|
+
const repliedMsgId = firstTrimmedString3(repliedMsg?.msgId, data.originalMsgId, content.quoted?.msgId);
|
|
7506
|
+
const fallbackCreatedAt = firstFiniteNumber(
|
|
7507
|
+
repliedMsg?.createdAt,
|
|
7508
|
+
content.quoted?.cardCreatedAt,
|
|
7509
|
+
content.quoted?.fileCreatedAt
|
|
7105
7510
|
);
|
|
7106
7511
|
const isOutboundQuoted = firstTrimmedString3(data.originalProcessQueryKey) !== void 0 || repliedMsg?.senderId === data.chatbotUserId || content.quoted?.isQuotedCard === true;
|
|
7107
7512
|
if (isOutboundQuoted) {
|
|
@@ -7622,6 +8027,47 @@ async function resolveQuotedFile(config, params, log) {
|
|
|
7622
8027
|
}
|
|
7623
8028
|
}
|
|
7624
8029
|
|
|
8030
|
+
// src/gateway/reply-session-conflict.ts
|
|
8031
|
+
var REPLY_SESSION_CONFLICT_PATTERN = /reply session initialization conflicted/i;
|
|
8032
|
+
function readErrorMessage(error) {
|
|
8033
|
+
if (error instanceof Error) {
|
|
8034
|
+
return error.message;
|
|
8035
|
+
}
|
|
8036
|
+
if (typeof error === "string") {
|
|
8037
|
+
return error;
|
|
8038
|
+
}
|
|
8039
|
+
if (error && typeof error === "object" && "message" in error) {
|
|
8040
|
+
const msg = error.message;
|
|
8041
|
+
return typeof msg === "string" ? msg : "";
|
|
8042
|
+
}
|
|
8043
|
+
return "";
|
|
8044
|
+
}
|
|
8045
|
+
function isReplySessionConflictError(error) {
|
|
8046
|
+
return REPLY_SESSION_CONFLICT_PATTERN.test(readErrorMessage(error));
|
|
8047
|
+
}
|
|
8048
|
+
var sleep = (ms) => new Promise((resolve3) => {
|
|
8049
|
+
setTimeout(resolve3, ms);
|
|
8050
|
+
});
|
|
8051
|
+
async function withReplySessionConflictRetry(fn, options = {}) {
|
|
8052
|
+
const maxRetries = options.maxRetries ?? 3;
|
|
8053
|
+
const baseDelayMs = options.baseDelayMs ?? 1500;
|
|
8054
|
+
const sessionLabel = options.sessionKey ?? "?";
|
|
8055
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
8056
|
+
try {
|
|
8057
|
+
return await fn();
|
|
8058
|
+
} catch (error) {
|
|
8059
|
+
if (!isReplySessionConflictError(error) || attempt >= maxRetries) {
|
|
8060
|
+
throw error;
|
|
8061
|
+
}
|
|
8062
|
+
const delay = baseDelayMs * (attempt + 1);
|
|
8063
|
+
options.log?.warn?.(
|
|
8064
|
+
`[DingTalk] Reply session initialization conflicted for session=${sessionLabel}; active run still draining. Retry ${attempt + 1}/${maxRetries} after ${delay}ms.`
|
|
8065
|
+
);
|
|
8066
|
+
await sleep(delay);
|
|
8067
|
+
}
|
|
8068
|
+
}
|
|
8069
|
+
}
|
|
8070
|
+
|
|
7625
8071
|
// src/card/reasoning-answer-split.ts
|
|
7626
8072
|
var THINKING_TAG_RE = /<\s*(\/?)\s*(?:think(?:ing)?|thought|antthinking)\b[^<>]*>/gi;
|
|
7627
8073
|
function isWrappedReasoningLine(line) {
|
|
@@ -9789,10 +10235,62 @@ async function dispatchSubAgents(params) {
|
|
|
9789
10235
|
dingtalkConfig,
|
|
9790
10236
|
sessionWebhook,
|
|
9791
10237
|
extractedContent,
|
|
10238
|
+
sessionPeer,
|
|
10239
|
+
onRoutesResolved,
|
|
9792
10240
|
handleMessage,
|
|
9793
10241
|
downloadMedia: download,
|
|
9794
|
-
log
|
|
10242
|
+
log,
|
|
10243
|
+
inboundQueueEligible
|
|
9795
10244
|
} = params;
|
|
10245
|
+
let helperMissingWarningSent = false;
|
|
10246
|
+
const sendHelperMissingWarning = async () => {
|
|
10247
|
+
if (helperMissingWarningSent) {
|
|
10248
|
+
return;
|
|
10249
|
+
}
|
|
10250
|
+
helperMissingWarningSent = true;
|
|
10251
|
+
try {
|
|
10252
|
+
const isGroup = data.conversationType !== "1";
|
|
10253
|
+
const sendOptions = isGroup ? { atUserId: data.senderId, log } : { log };
|
|
10254
|
+
await sendBySession(
|
|
10255
|
+
dingtalkConfig,
|
|
10256
|
+
sessionWebhook,
|
|
10257
|
+
"\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",
|
|
10258
|
+
sendOptions
|
|
10259
|
+
);
|
|
10260
|
+
} catch (notifyError) {
|
|
10261
|
+
log?.debug?.(
|
|
10262
|
+
`[DingTalk] Failed to send sub-agent helper-missing notice: ${getErrorMessage(notifyError)}`
|
|
10263
|
+
);
|
|
10264
|
+
}
|
|
10265
|
+
};
|
|
10266
|
+
let resolvedTargets;
|
|
10267
|
+
try {
|
|
10268
|
+
const rt = getDingTalkRuntime();
|
|
10269
|
+
resolvedTargets = matchedAgents.map((agent) => ({
|
|
10270
|
+
agent,
|
|
10271
|
+
route: {
|
|
10272
|
+
agentId: agent.agentId,
|
|
10273
|
+
sessionKey: buildAgentSessionKey({
|
|
10274
|
+
rt,
|
|
10275
|
+
cfg,
|
|
10276
|
+
accountId,
|
|
10277
|
+
agentId: agent.agentId,
|
|
10278
|
+
peerKind: sessionPeer.kind,
|
|
10279
|
+
peerId: sessionPeer.peerId
|
|
10280
|
+
}),
|
|
10281
|
+
mainSessionKey: ""
|
|
10282
|
+
}
|
|
10283
|
+
}));
|
|
10284
|
+
} catch (error) {
|
|
10285
|
+
const message = getErrorMessage(error);
|
|
10286
|
+
log?.error?.(`[DingTalk] Failed to resolve sub-agent routes: ${message}`);
|
|
10287
|
+
if (error instanceof HostRoutingHelperUnavailableError) {
|
|
10288
|
+
await sendHelperMissingWarning();
|
|
10289
|
+
return;
|
|
10290
|
+
}
|
|
10291
|
+
throw error;
|
|
10292
|
+
}
|
|
10293
|
+
onRoutesResolved?.(resolvedTargets);
|
|
9796
10294
|
let preDownloadedMedia;
|
|
9797
10295
|
const robotCode = resolveRobotCode(dingtalkConfig);
|
|
9798
10296
|
if (robotCode) {
|
|
@@ -9815,8 +10313,8 @@ async function dispatchSubAgents(params) {
|
|
|
9815
10313
|
};
|
|
9816
10314
|
}
|
|
9817
10315
|
}
|
|
9818
|
-
|
|
9819
|
-
|
|
10316
|
+
for (const target of resolvedTargets) {
|
|
10317
|
+
const agentMatch = target.agent;
|
|
9820
10318
|
try {
|
|
9821
10319
|
await handleMessage({
|
|
9822
10320
|
cfg,
|
|
@@ -9825,6 +10323,7 @@ async function dispatchSubAgents(params) {
|
|
|
9825
10323
|
sessionWebhook,
|
|
9826
10324
|
log,
|
|
9827
10325
|
dingtalkConfig,
|
|
10326
|
+
routeOverride: target.route,
|
|
9828
10327
|
subAgentOptions: {
|
|
9829
10328
|
agentId: agentMatch.agentId,
|
|
9830
10329
|
responsePrefix: commandText ? "" : `> \u{1F916} **${sanitizeAgentName(agentMatch.matchedName)}**:
|
|
@@ -9833,27 +10332,20 @@ async function dispatchSubAgents(params) {
|
|
|
9833
10332
|
matchedName: agentMatch.matchedName,
|
|
9834
10333
|
commandText
|
|
9835
10334
|
},
|
|
9836
|
-
preDownloadedMedia
|
|
10335
|
+
preDownloadedMedia,
|
|
10336
|
+
// Propagate queue eligibility so each recursive sub-agent handler
|
|
10337
|
+
// enters the handler-owned queue on its own `target.route.sessionKey`
|
|
10338
|
+
// instead of bypassing it (which previously left @子Agent messages
|
|
10339
|
+
// exposed to reply-session conflicts when the sub-agent session was
|
|
10340
|
+
// already busy). Synthetic callers without this flag keep the legacy
|
|
10341
|
+
// direct-dispatch behavior.
|
|
10342
|
+
inboundQueueEligible
|
|
9837
10343
|
});
|
|
9838
10344
|
} catch (error) {
|
|
9839
10345
|
const message = getErrorMessage(error);
|
|
9840
10346
|
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
|
-
}
|
|
10347
|
+
if (error instanceof HostRoutingHelperUnavailableError) {
|
|
10348
|
+
await sendHelperMissingWarning();
|
|
9857
10349
|
}
|
|
9858
10350
|
}
|
|
9859
10351
|
}
|
|
@@ -10555,6 +11047,8 @@ async function handleDingTalkMessageInner(params) {
|
|
|
10555
11047
|
sessionWebhook,
|
|
10556
11048
|
log,
|
|
10557
11049
|
dingtalkConfig,
|
|
11050
|
+
inboundOrigin = "stream",
|
|
11051
|
+
routeOverride,
|
|
10558
11052
|
subAgentOptions,
|
|
10559
11053
|
preDownloadedMedia
|
|
10560
11054
|
} = params;
|
|
@@ -10749,32 +11243,42 @@ async function handleDingTalkMessageInner(params) {
|
|
|
10749
11243
|
config: dingtalkConfig
|
|
10750
11244
|
});
|
|
10751
11245
|
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
|
-
|
|
11246
|
+
const invalidateQuestionRoutes = (routes) => {
|
|
11247
|
+
if (inboundOrigin === "ask-user") {
|
|
11248
|
+
return;
|
|
11249
|
+
}
|
|
11250
|
+
const scopeKeys = new Set(
|
|
11251
|
+
routes.map((resolvedRoute) => `${accountId}:${resolvedRoute.sessionKey}:${senderId}`)
|
|
11252
|
+
);
|
|
11253
|
+
const invalidatedRecords = [];
|
|
11254
|
+
for (const questionScopeKey of scopeKeys) {
|
|
11255
|
+
try {
|
|
11256
|
+
invalidatedRecords.push(
|
|
11257
|
+
...invalidateAskUserQuestionsForScope({
|
|
11258
|
+
storePath: accountStorePath,
|
|
11259
|
+
accountId,
|
|
11260
|
+
questionScopeKey,
|
|
11261
|
+
reason: "superseded_by_message",
|
|
11262
|
+
log
|
|
11263
|
+
})
|
|
11264
|
+
);
|
|
11265
|
+
} catch (err) {
|
|
11266
|
+
log?.warn?.(
|
|
11267
|
+
`[DingTalk][AskUser] Failed to invalidate pending cards before inbound dispatch scope=${questionScopeKey}: ${String(err)}`
|
|
11268
|
+
);
|
|
11269
|
+
}
|
|
11270
|
+
}
|
|
11271
|
+
if (invalidatedRecords.length === 0) {
|
|
11272
|
+
return;
|
|
11273
|
+
}
|
|
11274
|
+
void syncInvalidatedAskUserQuestionCards({
|
|
11275
|
+
records: invalidatedRecords,
|
|
11276
|
+
config: dingtalkConfig,
|
|
11277
|
+
log
|
|
11278
|
+
}).catch((err) => {
|
|
11279
|
+
log?.warn?.(`[DingTalk][AskUser] Card invalidation sync failed: ${String(err)}`);
|
|
10772
11280
|
});
|
|
10773
|
-
}
|
|
10774
|
-
const questionContext = getDingTalkQuestionContext();
|
|
10775
|
-
if (questionContext) {
|
|
10776
|
-
questionContext.questionScopeKey = `${accountId}:${route.sessionKey}:${senderId}`;
|
|
10777
|
-
}
|
|
11281
|
+
};
|
|
10778
11282
|
if (messageTarget && messageTarget.kind !== "default") {
|
|
10779
11283
|
if (messageTarget.kind === "subagent-command") {
|
|
10780
11284
|
await dispatchSubAgents({
|
|
@@ -10786,9 +11290,12 @@ async function handleDingTalkMessageInner(params) {
|
|
|
10786
11290
|
dingtalkConfig,
|
|
10787
11291
|
sessionWebhook,
|
|
10788
11292
|
extractedContent,
|
|
11293
|
+
sessionPeer,
|
|
11294
|
+
onRoutesResolved: (targets) => invalidateQuestionRoutes(targets.map((target) => target.route)),
|
|
10789
11295
|
handleMessage: handleDingTalkMessage,
|
|
10790
11296
|
downloadMedia,
|
|
10791
|
-
log
|
|
11297
|
+
log,
|
|
11298
|
+
inboundQueueEligible: params.inboundQueueEligible
|
|
10792
11299
|
});
|
|
10793
11300
|
return;
|
|
10794
11301
|
}
|
|
@@ -10814,13 +11321,54 @@ async function handleDingTalkMessageInner(params) {
|
|
|
10814
11321
|
dingtalkConfig,
|
|
10815
11322
|
sessionWebhook,
|
|
10816
11323
|
extractedContent,
|
|
11324
|
+
sessionPeer,
|
|
11325
|
+
onRoutesResolved: (targets) => invalidateQuestionRoutes(targets.map((target) => target.route)),
|
|
10817
11326
|
handleMessage: handleDingTalkMessage,
|
|
10818
11327
|
downloadMedia,
|
|
10819
|
-
log
|
|
11328
|
+
log,
|
|
11329
|
+
inboundQueueEligible: params.inboundQueueEligible
|
|
10820
11330
|
});
|
|
10821
11331
|
return;
|
|
10822
11332
|
}
|
|
10823
11333
|
}
|
|
11334
|
+
let route;
|
|
11335
|
+
if (routeOverride) {
|
|
11336
|
+
route = routeOverride;
|
|
11337
|
+
} else if (subAgentOptions) {
|
|
11338
|
+
route = {
|
|
11339
|
+
agentId: subAgentOptions.agentId,
|
|
11340
|
+
sessionKey: buildAgentSessionKey({
|
|
11341
|
+
rt,
|
|
11342
|
+
cfg,
|
|
11343
|
+
accountId,
|
|
11344
|
+
agentId: subAgentOptions.agentId,
|
|
11345
|
+
peerKind: sessionPeer.kind,
|
|
11346
|
+
peerId: sessionPeer.peerId
|
|
11347
|
+
}),
|
|
11348
|
+
mainSessionKey: ""
|
|
11349
|
+
};
|
|
11350
|
+
} else {
|
|
11351
|
+
route = rt.channel.routing.resolveAgentRoute({
|
|
11352
|
+
cfg,
|
|
11353
|
+
channel: "dingtalk",
|
|
11354
|
+
accountId,
|
|
11355
|
+
peer: { kind: sessionPeer.kind, id: sessionPeer.peerId }
|
|
11356
|
+
});
|
|
11357
|
+
}
|
|
11358
|
+
const questionContext = getDingTalkQuestionContext();
|
|
11359
|
+
if (questionContext) {
|
|
11360
|
+
questionContext.resolvedRoute = route;
|
|
11361
|
+
questionContext.questionScopeKey = `${accountId}:${route.sessionKey}:${senderId}`;
|
|
11362
|
+
questionContext.storePath = accountStorePath;
|
|
11363
|
+
questionContext.continuationSubAgentOptions = subAgentOptions ? {
|
|
11364
|
+
agentId: subAgentOptions.agentId,
|
|
11365
|
+
responsePrefix: subAgentOptions.responsePrefix,
|
|
11366
|
+
matchedName: subAgentOptions.matchedName
|
|
11367
|
+
} : void 0;
|
|
11368
|
+
}
|
|
11369
|
+
if (!subAgentOptions) {
|
|
11370
|
+
invalidateQuestionRoutes([route]);
|
|
11371
|
+
}
|
|
10824
11372
|
const storePath = rt.channel.session.resolveStorePath(cfg.session?.store, {
|
|
10825
11373
|
agentId: route.agentId
|
|
10826
11374
|
});
|
|
@@ -10856,7 +11404,29 @@ async function handleDingTalkMessageInner(params) {
|
|
|
10856
11404
|
const quotedRef = buildInboundQuotedRef(data, extractedContent);
|
|
10857
11405
|
const replyQuotedRef = createReplyQuotedRef(data.msgId);
|
|
10858
11406
|
const content = extractedContent;
|
|
10859
|
-
const
|
|
11407
|
+
const controlText = stripLeadingMentions(content.text).trim();
|
|
11408
|
+
const isBtwBypass = isBtwRequestText(controlText);
|
|
11409
|
+
const isAbortBypass = isAbortRequestText(controlText);
|
|
11410
|
+
if (params.inboundQueueEligible && !params.inboundQueueHandled && inboundOrigin !== "ask-user" && !isBtwBypass && !isAbortBypass) {
|
|
11411
|
+
await dispatchInboundViaSessionQueue(
|
|
11412
|
+
{
|
|
11413
|
+
accountId,
|
|
11414
|
+
data,
|
|
11415
|
+
dingtalkConfig,
|
|
11416
|
+
sessionKey: route.sessionKey,
|
|
11417
|
+
to,
|
|
11418
|
+
storePath: accountStorePath,
|
|
11419
|
+
quoteContent: rawInboundText.slice(0, 200),
|
|
11420
|
+
log
|
|
11421
|
+
},
|
|
11422
|
+
(preCreatedCard) => handleDingTalkMessage({
|
|
11423
|
+
...params,
|
|
11424
|
+
preCreatedCard,
|
|
11425
|
+
inboundQueueHandled: true
|
|
11426
|
+
})
|
|
11427
|
+
);
|
|
11428
|
+
return;
|
|
11429
|
+
}
|
|
10860
11430
|
const taskInfoConversationId = groupId || to;
|
|
10861
11431
|
const agentDisplayName = getAgentDisplayName({
|
|
10862
11432
|
subAgentOptions,
|
|
@@ -10887,7 +11457,6 @@ async function handleDingTalkMessageInner(params) {
|
|
|
10887
11457
|
let cardFlightKey;
|
|
10888
11458
|
if (questionContext) {
|
|
10889
11459
|
questionContext.onQuestionCardSent = async ({ questionId, outTrackId }) => {
|
|
10890
|
-
questionCardTookOver = true;
|
|
10891
11460
|
if (cardFlightKey) {
|
|
10892
11461
|
cardCreationInFlight.delete(cardFlightKey);
|
|
10893
11462
|
cardFlightKey = void 0;
|
|
@@ -10908,26 +11477,37 @@ async function handleDingTalkMessageInner(params) {
|
|
|
10908
11477
|
log?.warn?.(
|
|
10909
11478
|
`[DingTalk][AskUser] Question card sent, but targeted stop failed question=${questionId} outTrackId=${outTrackId} targetSessionKey=${route.sessionKey}: ${err instanceof Error ? err.message : String(err)}`
|
|
10910
11479
|
);
|
|
11480
|
+
return false;
|
|
10911
11481
|
}
|
|
11482
|
+
questionCardTookOver = true;
|
|
10912
11483
|
if (!currentAICard) {
|
|
10913
|
-
return;
|
|
11484
|
+
return true;
|
|
10914
11485
|
}
|
|
10915
11486
|
if (isCardInTerminalState(currentAICard.state)) {
|
|
10916
|
-
return;
|
|
11487
|
+
return true;
|
|
11488
|
+
}
|
|
11489
|
+
let recalled = false;
|
|
11490
|
+
try {
|
|
11491
|
+
recalled = await recallAICardMessage(currentAICard, log);
|
|
11492
|
+
} catch (err) {
|
|
11493
|
+
log?.warn?.(
|
|
11494
|
+
`[DingTalk][AskUser] Question card took over, but AI card recall errored question=${questionId} outTrackId=${outTrackId}: ${err instanceof Error ? err.message : String(err)}`
|
|
11495
|
+
);
|
|
11496
|
+
return true;
|
|
10917
11497
|
}
|
|
10918
|
-
const recalled = await recallAICardMessage(currentAICard, log);
|
|
10919
11498
|
if (!recalled) {
|
|
10920
11499
|
log?.warn?.(
|
|
10921
11500
|
`[DingTalk][AskUser] Question card sent, but AI card recall failed; normal replies remain suppressed question=${questionId} outTrackId=${outTrackId}`
|
|
10922
11501
|
);
|
|
10923
|
-
return;
|
|
11502
|
+
return true;
|
|
10924
11503
|
}
|
|
10925
11504
|
log?.info?.(
|
|
10926
11505
|
`[DingTalk][AskUser] Recalled empty AI card after question card sent question=${questionId} outTrackId=${outTrackId}`
|
|
10927
11506
|
);
|
|
11507
|
+
return true;
|
|
10928
11508
|
};
|
|
10929
11509
|
}
|
|
10930
|
-
if (useCardMode && !isBtwBypass) {
|
|
11510
|
+
if (useCardMode && !isBtwBypass && !params.preCreatedCard) {
|
|
10931
11511
|
const key = `${accountId}:${to}`;
|
|
10932
11512
|
if (cardCreationInFlight.has(key)) {
|
|
10933
11513
|
useCardMode = false;
|
|
@@ -10945,7 +11525,7 @@ async function handleDingTalkMessageInner(params) {
|
|
|
10945
11525
|
`[DingTalk][AICard] conversationType=${data.conversationType}, conversationId=${to}`
|
|
10946
11526
|
);
|
|
10947
11527
|
const inboundQuoteText = rawInboundText.slice(0, 200);
|
|
10948
|
-
const aiCard = await createAICard(dingtalkConfig, to, log, {
|
|
11528
|
+
const aiCard = params.preCreatedCard ?? await createAICard(dingtalkConfig, to, log, {
|
|
10949
11529
|
accountId,
|
|
10950
11530
|
storePath: accountStorePath,
|
|
10951
11531
|
contextConversationId: groupId,
|
|
@@ -11609,8 +12189,7 @@ ${attachmentExtractedText}` : inboundBody;
|
|
|
11609
12189
|
}
|
|
11610
12190
|
});
|
|
11611
12191
|
log?.info?.(`[DingTalk] Inbound: from=${senderName} text="${content.text.slice(0, 50)}..."`);
|
|
11612
|
-
|
|
11613
|
-
if (isAbortRequestText(textForAbortCheck)) {
|
|
12192
|
+
if (isAbortBypass) {
|
|
11614
12193
|
log?.info?.(
|
|
11615
12194
|
`[DingTalk] Abort request detected, bypassing session lock for session=${route.sessionKey}`
|
|
11616
12195
|
);
|
|
@@ -11871,53 +12450,57 @@ ${attachmentExtractedText}` : inboundBody;
|
|
|
11871
12450
|
inboundText: rawInboundText,
|
|
11872
12451
|
taskMeta
|
|
11873
12452
|
});
|
|
11874
|
-
|
|
11875
|
-
|
|
11876
|
-
|
|
11877
|
-
|
|
11878
|
-
|
|
11879
|
-
|
|
11880
|
-
|
|
11881
|
-
|
|
11882
|
-
|
|
11883
|
-
|
|
11884
|
-
|
|
12453
|
+
let deliveredFinalCount = 0;
|
|
12454
|
+
const runDispatch = () => rt.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
12455
|
+
ctx,
|
|
12456
|
+
cfg,
|
|
12457
|
+
dispatcherOptions: {
|
|
12458
|
+
responsePrefix: subAgentOptions?.responsePrefix || "",
|
|
12459
|
+
deliver: async (payload, info) => {
|
|
12460
|
+
if (isCurrentCardStopRequested()) {
|
|
12461
|
+
log?.debug?.(
|
|
12462
|
+
"[DingTalk][CardStop] Ignoring reply delivery because stop was already requested"
|
|
12463
|
+
);
|
|
12464
|
+
return;
|
|
12465
|
+
}
|
|
12466
|
+
try {
|
|
12467
|
+
if (info?.kind === "final") {
|
|
12468
|
+
deliveredFinalCount += 1;
|
|
12469
|
+
}
|
|
12470
|
+
const inlineReplyPayload = parseInlineReplyPayloadText(payload.text);
|
|
12471
|
+
const mediaUrls = extractMediaUrls(payload, inlineReplyPayload);
|
|
12472
|
+
const richPayload = payload;
|
|
12473
|
+
const replyKind = info?.kind || "block";
|
|
12474
|
+
if (questionCardTookOver) {
|
|
12475
|
+
log?.info?.(
|
|
12476
|
+
`[DingTalk][AskUser] Suppressed ${replyKind} reply after question card took over`
|
|
11885
12477
|
);
|
|
11886
12478
|
return;
|
|
11887
12479
|
}
|
|
11888
|
-
|
|
11889
|
-
|
|
11890
|
-
|
|
11891
|
-
|
|
11892
|
-
|
|
11893
|
-
|
|
11894
|
-
|
|
11895
|
-
|
|
11896
|
-
|
|
11897
|
-
|
|
11898
|
-
|
|
11899
|
-
|
|
11900
|
-
|
|
11901
|
-
}
|
|
11902
|
-
await strategy.deliver({
|
|
11903
|
-
text: inlineReplyPayload.text,
|
|
11904
|
-
mediaUrls,
|
|
11905
|
-
audioAsVoice: extractSharedAudioAsVoice(payload, inlineReplyPayload),
|
|
11906
|
-
kind: replyKind,
|
|
11907
|
-
isError: payload.isError === true,
|
|
11908
|
-
isReasoning: richPayload.isReasoning === true
|
|
11909
|
-
});
|
|
11910
|
-
} catch (err) {
|
|
11911
|
-
log?.error?.(`[DingTalk] Reply failed: ${getErrorMessage(err)}`);
|
|
11912
|
-
const responseData = getErrorResponseData(err);
|
|
11913
|
-
if (responseData !== void 0) {
|
|
11914
|
-
log?.error?.(formatDingTalkErrorPayloadLog("inbound.replyDeliver", responseData));
|
|
11915
|
-
}
|
|
11916
|
-
throw err;
|
|
12480
|
+
await strategy.deliver({
|
|
12481
|
+
text: inlineReplyPayload.text,
|
|
12482
|
+
mediaUrls,
|
|
12483
|
+
audioAsVoice: extractSharedAudioAsVoice(payload, inlineReplyPayload),
|
|
12484
|
+
kind: replyKind,
|
|
12485
|
+
isError: payload.isError === true,
|
|
12486
|
+
isReasoning: richPayload.isReasoning === true
|
|
12487
|
+
});
|
|
12488
|
+
} catch (err) {
|
|
12489
|
+
log?.error?.(`[DingTalk] Reply failed: ${getErrorMessage(err)}`);
|
|
12490
|
+
const responseData = getErrorResponseData(err);
|
|
12491
|
+
if (responseData !== void 0) {
|
|
12492
|
+
log?.error?.(formatDingTalkErrorPayloadLog("inbound.replyDeliver", responseData));
|
|
11917
12493
|
}
|
|
12494
|
+
throw err;
|
|
11918
12495
|
}
|
|
11919
|
-
}
|
|
11920
|
-
|
|
12496
|
+
}
|
|
12497
|
+
},
|
|
12498
|
+
replyOptions: strategy.getReplyOptions()
|
|
12499
|
+
});
|
|
12500
|
+
try {
|
|
12501
|
+
const dispatchResult = await withReplySessionConflictRetry(runDispatch, {
|
|
12502
|
+
log,
|
|
12503
|
+
sessionKey: route.sessionKey
|
|
11921
12504
|
});
|
|
11922
12505
|
const bufferedFinal = dispatchResult && typeof dispatchResult === "object" && "queuedFinal" in dispatchResult ? dispatchResult.queuedFinal : void 0;
|
|
11923
12506
|
const finalCount = dispatchResult && typeof dispatchResult === "object" && "counts" in dispatchResult ? dispatchResult.counts?.final : void 0;
|
|
@@ -11947,6 +12530,51 @@ ${attachmentExtractedText}` : inboundBody;
|
|
|
11947
12530
|
}
|
|
11948
12531
|
} catch (dispatchErr) {
|
|
11949
12532
|
const error = dispatchErr instanceof Error ? dispatchErr : new Error(getErrorMessage(dispatchErr));
|
|
12533
|
+
if (isReplySessionConflictError(error)) {
|
|
12534
|
+
log?.warn?.(
|
|
12535
|
+
`[DingTalk] Reply session still conflicted after retries for session=${route.sessionKey}; sending "processing" acknowledgement instead of dropping the message.`
|
|
12536
|
+
);
|
|
12537
|
+
const ackText = "\u6536\u5230\uFF0C\u4E0A\u4E00\u8F6E\u8FD8\u5728\u5904\u7406\u4E2D\uFF0C\u8BF7\u7A0D\u5019\u518D\u8BD5\u3002";
|
|
12538
|
+
if (replyMode === "card") {
|
|
12539
|
+
try {
|
|
12540
|
+
await strategy.deliver({
|
|
12541
|
+
text: ackText,
|
|
12542
|
+
mediaUrls: [],
|
|
12543
|
+
kind: "final",
|
|
12544
|
+
isReasoning: false
|
|
12545
|
+
});
|
|
12546
|
+
await strategy.finalize();
|
|
12547
|
+
return;
|
|
12548
|
+
} catch (cardAckErr) {
|
|
12549
|
+
log?.warn?.(
|
|
12550
|
+
`[DingTalk] Processing acknowledgement card finalize failed: ${getErrorMessage(cardAckErr)}`
|
|
12551
|
+
);
|
|
12552
|
+
return;
|
|
12553
|
+
}
|
|
12554
|
+
}
|
|
12555
|
+
try {
|
|
12556
|
+
if (sessionWebhook) {
|
|
12557
|
+
await sendBySession(dingtalkConfig, sessionWebhook, ackText, {
|
|
12558
|
+
log,
|
|
12559
|
+
accountId,
|
|
12560
|
+
storePath: accountStorePath
|
|
12561
|
+
});
|
|
12562
|
+
} else {
|
|
12563
|
+
await sendMessage(dingtalkConfig, to, ackText, {
|
|
12564
|
+
log,
|
|
12565
|
+
accountId,
|
|
12566
|
+
storePath: accountStorePath,
|
|
12567
|
+
conversationId: groupId
|
|
12568
|
+
});
|
|
12569
|
+
}
|
|
12570
|
+
} catch (ackErr) {
|
|
12571
|
+
log?.warn?.(
|
|
12572
|
+
`[DingTalk] Processing acknowledgement delivery failed: ${getErrorMessage(ackErr)}`
|
|
12573
|
+
);
|
|
12574
|
+
}
|
|
12575
|
+
await strategy.abort(error);
|
|
12576
|
+
return;
|
|
12577
|
+
}
|
|
11950
12578
|
await strategy.abort(error);
|
|
11951
12579
|
throw dispatchErr;
|
|
11952
12580
|
}
|
|
@@ -11975,6 +12603,194 @@ ${attachmentExtractedText}` : inboundBody;
|
|
|
11975
12603
|
}
|
|
11976
12604
|
}
|
|
11977
12605
|
|
|
12606
|
+
// src/card/ask-user-question-store.ts
|
|
12607
|
+
var ASK_USER_LIFECYCLE_NAMESPACE = "cards.ask-user.lifecycle";
|
|
12608
|
+
var ACTIVE_TTL_MS = 5 * 60 * 1e3;
|
|
12609
|
+
var TOMBSTONE_TTL_MS = 30 * 60 * 1e3;
|
|
12610
|
+
function now(options) {
|
|
12611
|
+
return options.now?.() ?? Date.now();
|
|
12612
|
+
}
|
|
12613
|
+
function emptyState(timestamp) {
|
|
12614
|
+
return { version: 1, updatedAt: timestamp, records: [] };
|
|
12615
|
+
}
|
|
12616
|
+
function isActiveState(state) {
|
|
12617
|
+
return state === "reserved" || state === "pending" || state === "dispatching";
|
|
12618
|
+
}
|
|
12619
|
+
function isAnswerableState(state) {
|
|
12620
|
+
return state === "reserved" || state === "pending";
|
|
12621
|
+
}
|
|
12622
|
+
function loadState2(options) {
|
|
12623
|
+
const timestamp = now(options);
|
|
12624
|
+
const state = readNamespaceJson(ASK_USER_LIFECYCLE_NAMESPACE, {
|
|
12625
|
+
storePath: options.storePath,
|
|
12626
|
+
scope: { accountId: options.accountId },
|
|
12627
|
+
fallback: emptyState(timestamp),
|
|
12628
|
+
log: options.log
|
|
12629
|
+
});
|
|
12630
|
+
if (state.version !== 1 || !Array.isArray(state.records)) {
|
|
12631
|
+
return emptyState(timestamp);
|
|
12632
|
+
}
|
|
12633
|
+
return state;
|
|
12634
|
+
}
|
|
12635
|
+
function persistState(options, state) {
|
|
12636
|
+
state.updatedAt = now(options);
|
|
12637
|
+
writeNamespaceJsonAtomic(ASK_USER_LIFECYCLE_NAMESPACE, {
|
|
12638
|
+
storePath: options.storePath,
|
|
12639
|
+
scope: { accountId: options.accountId },
|
|
12640
|
+
data: state,
|
|
12641
|
+
log: options.log
|
|
12642
|
+
});
|
|
12643
|
+
}
|
|
12644
|
+
function toTerminal(record, reason, timestamp) {
|
|
12645
|
+
record.state = "terminal";
|
|
12646
|
+
record.terminalReason = reason;
|
|
12647
|
+
record.updatedAt = timestamp;
|
|
12648
|
+
record.expiresAt = timestamp + TOMBSTONE_TTL_MS;
|
|
12649
|
+
return record;
|
|
12650
|
+
}
|
|
12651
|
+
function cleanupState(state, timestamp) {
|
|
12652
|
+
let changed = false;
|
|
12653
|
+
for (const record of state.records) {
|
|
12654
|
+
if (isAnswerableState(record.state) && record.expiresAt <= timestamp) {
|
|
12655
|
+
toTerminal(record, "expired", timestamp);
|
|
12656
|
+
changed = true;
|
|
12657
|
+
}
|
|
12658
|
+
}
|
|
12659
|
+
const retained = state.records.filter(
|
|
12660
|
+
(record) => record.state !== "terminal" || record.expiresAt > timestamp
|
|
12661
|
+
);
|
|
12662
|
+
if (retained.length !== state.records.length) {
|
|
12663
|
+
state.records = retained;
|
|
12664
|
+
changed = true;
|
|
12665
|
+
}
|
|
12666
|
+
return changed;
|
|
12667
|
+
}
|
|
12668
|
+
function readCleanState(options) {
|
|
12669
|
+
const state = loadState2(options);
|
|
12670
|
+
if (cleanupState(state, now(options))) {
|
|
12671
|
+
persistState(options, state);
|
|
12672
|
+
}
|
|
12673
|
+
return state;
|
|
12674
|
+
}
|
|
12675
|
+
function findRecord(state, identifier) {
|
|
12676
|
+
if (identifier.outTrackId) {
|
|
12677
|
+
const byTrackId = state.records.find((record) => record.outTrackId === identifier.outTrackId);
|
|
12678
|
+
if (byTrackId) {
|
|
12679
|
+
return byTrackId;
|
|
12680
|
+
}
|
|
12681
|
+
}
|
|
12682
|
+
if (identifier.questionId) {
|
|
12683
|
+
return state.records.find((record) => record.questionId === identifier.questionId);
|
|
12684
|
+
}
|
|
12685
|
+
return void 0;
|
|
12686
|
+
}
|
|
12687
|
+
function reserveAskUserQuestion(options, input) {
|
|
12688
|
+
const timestamp = now(options);
|
|
12689
|
+
const state = readCleanState(options);
|
|
12690
|
+
const record = {
|
|
12691
|
+
questionId: input.questionId,
|
|
12692
|
+
accountId: options.accountId,
|
|
12693
|
+
questionScopeKey: input.questionScopeKey,
|
|
12694
|
+
outTrackId: input.outTrackId,
|
|
12695
|
+
title: input.title,
|
|
12696
|
+
state: "reserved",
|
|
12697
|
+
createdAt: timestamp,
|
|
12698
|
+
updatedAt: timestamp,
|
|
12699
|
+
expiresAt: timestamp + ACTIVE_TTL_MS
|
|
12700
|
+
};
|
|
12701
|
+
state.records = state.records.filter(
|
|
12702
|
+
(item) => item.questionId !== input.questionId && item.outTrackId !== input.outTrackId
|
|
12703
|
+
);
|
|
12704
|
+
state.records.push(record);
|
|
12705
|
+
persistState(options, state);
|
|
12706
|
+
return { ...record };
|
|
12707
|
+
}
|
|
12708
|
+
function activateAskUserQuestion(options, questionId) {
|
|
12709
|
+
const timestamp = now(options);
|
|
12710
|
+
const state = readCleanState(options);
|
|
12711
|
+
const record = findRecord(state, { questionId });
|
|
12712
|
+
if (!record || record.state !== "reserved") {
|
|
12713
|
+
return { record: record ? { ...record } : void 0, superseded: [] };
|
|
12714
|
+
}
|
|
12715
|
+
const superseded = [];
|
|
12716
|
+
for (const candidate of state.records) {
|
|
12717
|
+
if (candidate !== record && isAnswerableState(candidate.state) && candidate.questionScopeKey === record.questionScopeKey) {
|
|
12718
|
+
toTerminal(candidate, "superseded_by_question", timestamp);
|
|
12719
|
+
superseded.push({ ...candidate });
|
|
12720
|
+
}
|
|
12721
|
+
}
|
|
12722
|
+
record.state = "pending";
|
|
12723
|
+
record.updatedAt = timestamp;
|
|
12724
|
+
record.expiresAt = timestamp + ACTIVE_TTL_MS;
|
|
12725
|
+
persistState(options, state);
|
|
12726
|
+
return { record: { ...record }, superseded };
|
|
12727
|
+
}
|
|
12728
|
+
function claimAskUserQuestion(options, identifier) {
|
|
12729
|
+
const timestamp = now(options);
|
|
12730
|
+
const state = readCleanState(options);
|
|
12731
|
+
const record = findRecord(state, identifier);
|
|
12732
|
+
if (!record || record.state !== "pending") {
|
|
12733
|
+
return void 0;
|
|
12734
|
+
}
|
|
12735
|
+
record.state = "dispatching";
|
|
12736
|
+
record.updatedAt = timestamp;
|
|
12737
|
+
record.expiresAt = timestamp + ACTIVE_TTL_MS;
|
|
12738
|
+
persistState(options, state);
|
|
12739
|
+
return { ...record };
|
|
12740
|
+
}
|
|
12741
|
+
function terminateAskUserQuestion(options, questionId, reason) {
|
|
12742
|
+
const timestamp = now(options);
|
|
12743
|
+
const state = readCleanState(options);
|
|
12744
|
+
const record = findRecord(state, { questionId });
|
|
12745
|
+
if (!record || !isActiveState(record.state)) {
|
|
12746
|
+
return void 0;
|
|
12747
|
+
}
|
|
12748
|
+
toTerminal(record, reason, timestamp);
|
|
12749
|
+
persistState(options, state);
|
|
12750
|
+
return { ...record };
|
|
12751
|
+
}
|
|
12752
|
+
function invalidateAskUserQuestionsInScope(options, questionScopeKey, reason) {
|
|
12753
|
+
const timestamp = now(options);
|
|
12754
|
+
const state = readCleanState(options);
|
|
12755
|
+
const invalidated = [];
|
|
12756
|
+
for (const record of state.records) {
|
|
12757
|
+
if (isAnswerableState(record.state) && record.questionScopeKey === questionScopeKey) {
|
|
12758
|
+
toTerminal(record, reason, timestamp);
|
|
12759
|
+
invalidated.push({ ...record });
|
|
12760
|
+
}
|
|
12761
|
+
}
|
|
12762
|
+
if (invalidated.length > 0) {
|
|
12763
|
+
persistState(options, state);
|
|
12764
|
+
}
|
|
12765
|
+
return invalidated;
|
|
12766
|
+
}
|
|
12767
|
+
function resolveAskUserQuestion(options, identifier) {
|
|
12768
|
+
const record = findRecord(readCleanState(options), identifier);
|
|
12769
|
+
return record ? { ...record } : void 0;
|
|
12770
|
+
}
|
|
12771
|
+
function recoverAskUserQuestionsAfterRestart(options) {
|
|
12772
|
+
const timestamp = now(options);
|
|
12773
|
+
const state = loadState2(options);
|
|
12774
|
+
const retained = state.records.filter(
|
|
12775
|
+
(record) => record.state !== "terminal" || record.expiresAt > timestamp
|
|
12776
|
+
);
|
|
12777
|
+
const removedExpiredTombstones = retained.length !== state.records.length;
|
|
12778
|
+
state.records = retained;
|
|
12779
|
+
const recovered = [];
|
|
12780
|
+
for (const record of state.records) {
|
|
12781
|
+
if (!isActiveState(record.state)) {
|
|
12782
|
+
continue;
|
|
12783
|
+
}
|
|
12784
|
+
const reason = record.state === "dispatching" ? "restart_during_dispatch" : "restart_invalidated";
|
|
12785
|
+
toTerminal(record, reason, timestamp);
|
|
12786
|
+
recovered.push({ ...record });
|
|
12787
|
+
}
|
|
12788
|
+
if (recovered.length > 0 || removedExpiredTombstones) {
|
|
12789
|
+
persistState(options, state);
|
|
12790
|
+
}
|
|
12791
|
+
return recovered;
|
|
12792
|
+
}
|
|
12793
|
+
|
|
11978
12794
|
// src/card/ask-user-question.ts
|
|
11979
12795
|
var DINGTALK_API4 = "https://api.dingtalk.com";
|
|
11980
12796
|
var PENDING_QUESTION_TTL_MS = 5 * 60 * 1e3;
|
|
@@ -12244,9 +13060,11 @@ function supersedePendingQuestionsInScope(ctx) {
|
|
|
12244
13060
|
});
|
|
12245
13061
|
}
|
|
12246
13062
|
}
|
|
12247
|
-
function storePendingQuestion(ctx) {
|
|
13063
|
+
function storePendingQuestion(ctx, options = {}) {
|
|
12248
13064
|
ctx.ownerUserId = resolvePendingQuestionOwner(ctx);
|
|
12249
|
-
|
|
13065
|
+
if (options.supersedeExisting !== false) {
|
|
13066
|
+
supersedePendingQuestionsInScope(ctx);
|
|
13067
|
+
}
|
|
12250
13068
|
pendingQuestionsByTrackId.set(ctx.outTrackId, ctx);
|
|
12251
13069
|
pendingQuestionsByQuestionId.set(ctx.questionId, ctx);
|
|
12252
13070
|
addScopeIndex(ctx);
|
|
@@ -12254,7 +13072,9 @@ function storePendingQuestion(ctx) {
|
|
|
12254
13072
|
if (!pendingQuestionsByTrackId.has(ctx.outTrackId) || ctx.submitted) {
|
|
12255
13073
|
return;
|
|
12256
13074
|
}
|
|
12257
|
-
ctx
|
|
13075
|
+
if (!claimPendingQuestionForDispatch(ctx)) {
|
|
13076
|
+
return;
|
|
13077
|
+
}
|
|
12258
13078
|
consumePendingQuestion(ctx);
|
|
12259
13079
|
addHandledQuestionTombstone(ctx, "expired");
|
|
12260
13080
|
void updateQuestionCardBestEffort(ctx, {
|
|
@@ -12262,14 +13082,12 @@ function storePendingQuestion(ctx) {
|
|
|
12262
13082
|
question_desc: "\u95EE\u9898\u5DF2\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u53D1\u8D77\u3002",
|
|
12263
13083
|
form_btn_text: "\u5DF2\u5931\u6548"
|
|
12264
13084
|
});
|
|
12265
|
-
|
|
12266
|
-
|
|
12267
|
-
|
|
12268
|
-
|
|
12269
|
-
|
|
12270
|
-
|
|
12271
|
-
}
|
|
12272
|
-
);
|
|
13085
|
+
dispatchSyntheticAnswer({
|
|
13086
|
+
ctx,
|
|
13087
|
+
text: buildExpiredAnswerMessage(ctx),
|
|
13088
|
+
suffix: "expired",
|
|
13089
|
+
successReason: "expired",
|
|
13090
|
+
log: ctx.log
|
|
12273
13091
|
});
|
|
12274
13092
|
}, PENDING_QUESTION_TTL_MS);
|
|
12275
13093
|
}
|
|
@@ -12294,6 +13112,123 @@ async function updateQuestionCardBestEffort(ctx, variables) {
|
|
|
12294
13112
|
);
|
|
12295
13113
|
}
|
|
12296
13114
|
}
|
|
13115
|
+
function getAskUserStoreOptions(params) {
|
|
13116
|
+
if (!params.storePath) {
|
|
13117
|
+
return void 0;
|
|
13118
|
+
}
|
|
13119
|
+
return {
|
|
13120
|
+
storePath: params.storePath,
|
|
13121
|
+
accountId: params.accountId,
|
|
13122
|
+
log: params.log
|
|
13123
|
+
};
|
|
13124
|
+
}
|
|
13125
|
+
function terminalCardVariables(reason) {
|
|
13126
|
+
const descriptions = {
|
|
13127
|
+
delivery_failed: "\u95EE\u9898\u5361\u7247\u53D1\u9001\u5931\u8D25\u3002",
|
|
13128
|
+
superseded_by_question: "\u5DF2\u6709\u65B0\u7684\u95EE\u9898\u5361\u7247\uFF0C\u8BF7\u56DE\u7B54\u6700\u65B0\u5361\u7247\u3002",
|
|
13129
|
+
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",
|
|
13130
|
+
expired: "\u95EE\u9898\u5DF2\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u53D1\u8D77\u3002",
|
|
13131
|
+
cancelled: "\u5DF2\u53D6\u6D88\u3002",
|
|
13132
|
+
empty: "\u5DF2\u63D0\u4EA4\uFF0C\u672A\u586B\u5199\u4EFB\u4F55\u5185\u5BB9\u3002",
|
|
13133
|
+
submitted: "\u5DF2\u63D0\u4EA4\u3002",
|
|
13134
|
+
pause_failed: "\u5F53\u524D\u4EFB\u52A1\u672A\u80FD\u6682\u505C\uFF0C\u6B64\u5361\u5DF2\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u53D1\u8D77\u3002",
|
|
13135
|
+
restart_invalidated: "\u670D\u52A1\u5DF2\u91CD\u542F\uFF0C\u539F\u95EE\u9898\u4E0A\u4E0B\u6587\u5DF2\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u53D1\u8D77\u3002",
|
|
13136
|
+
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",
|
|
13137
|
+
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"
|
|
13138
|
+
};
|
|
13139
|
+
return {
|
|
13140
|
+
card_status: reason === "cancelled" ? "cancelled" : reason === "submitted" || reason === "empty" ? "submitted" : "expired",
|
|
13141
|
+
question_desc: descriptions[reason],
|
|
13142
|
+
form_btn_text: reason === "cancelled" ? "\u5DF2\u53D6\u6D88" : reason === "submitted" || reason === "empty" ? "\u5DF2\u63D0\u4EA4" : "\u5DF2\u5931\u6548"
|
|
13143
|
+
};
|
|
13144
|
+
}
|
|
13145
|
+
async function updateLifecycleRecordCardBestEffort(params) {
|
|
13146
|
+
const reason = params.record.terminalReason;
|
|
13147
|
+
if (!reason || reason === "delivery_failed") {
|
|
13148
|
+
return;
|
|
13149
|
+
}
|
|
13150
|
+
try {
|
|
13151
|
+
const token = await getAccessToken(params.config, params.log);
|
|
13152
|
+
await updateCardVariables(
|
|
13153
|
+
params.record.outTrackId,
|
|
13154
|
+
terminalCardVariables(reason),
|
|
13155
|
+
token,
|
|
13156
|
+
params.config
|
|
13157
|
+
);
|
|
13158
|
+
} catch (err) {
|
|
13159
|
+
params.log?.warn?.(
|
|
13160
|
+
`[DingTalk][AskUser] Failed to update lifecycle card ${params.record.questionId}: ${String(err)}`
|
|
13161
|
+
);
|
|
13162
|
+
}
|
|
13163
|
+
}
|
|
13164
|
+
function consumeLifecyclePendingContext(record) {
|
|
13165
|
+
const ctx = pendingQuestionsByQuestionId.get(record.questionId) ?? pendingQuestionsByTrackId.get(record.outTrackId);
|
|
13166
|
+
if (!ctx) {
|
|
13167
|
+
return void 0;
|
|
13168
|
+
}
|
|
13169
|
+
ctx.submitted = true;
|
|
13170
|
+
consumePendingQuestion(ctx);
|
|
13171
|
+
addHandledQuestionTombstone(ctx, record.terminalReason === "expired" ? "expired" : "superseded");
|
|
13172
|
+
return ctx;
|
|
13173
|
+
}
|
|
13174
|
+
function invalidateAskUserQuestionsForScope(params) {
|
|
13175
|
+
const invalidated = invalidateAskUserQuestionsInScope(
|
|
13176
|
+
{
|
|
13177
|
+
storePath: params.storePath,
|
|
13178
|
+
accountId: params.accountId,
|
|
13179
|
+
log: params.log
|
|
13180
|
+
},
|
|
13181
|
+
params.questionScopeKey,
|
|
13182
|
+
params.reason
|
|
13183
|
+
);
|
|
13184
|
+
for (const record of invalidated) {
|
|
13185
|
+
consumeLifecyclePendingContext(record);
|
|
13186
|
+
}
|
|
13187
|
+
return invalidated;
|
|
13188
|
+
}
|
|
13189
|
+
async function syncInvalidatedAskUserQuestionCards(params) {
|
|
13190
|
+
await Promise.allSettled(
|
|
13191
|
+
params.records.map(
|
|
13192
|
+
(record) => updateLifecycleRecordCardBestEffort({
|
|
13193
|
+
record,
|
|
13194
|
+
config: params.config,
|
|
13195
|
+
log: params.log
|
|
13196
|
+
})
|
|
13197
|
+
)
|
|
13198
|
+
);
|
|
13199
|
+
}
|
|
13200
|
+
async function recoverAskUserQuestionsForAccount(params) {
|
|
13201
|
+
if (!params.storePath) {
|
|
13202
|
+
return 0;
|
|
13203
|
+
}
|
|
13204
|
+
const recovered = recoverAskUserQuestionsAfterRestart({
|
|
13205
|
+
storePath: params.storePath,
|
|
13206
|
+
accountId: params.accountId,
|
|
13207
|
+
log: params.log
|
|
13208
|
+
});
|
|
13209
|
+
for (const record of recovered) {
|
|
13210
|
+
consumeLifecyclePendingContext(record);
|
|
13211
|
+
await updateLifecycleRecordCardBestEffort({
|
|
13212
|
+
record,
|
|
13213
|
+
config: params.config,
|
|
13214
|
+
log: params.log
|
|
13215
|
+
});
|
|
13216
|
+
}
|
|
13217
|
+
return recovered.length;
|
|
13218
|
+
}
|
|
13219
|
+
async function terminatePendingQuestion(params) {
|
|
13220
|
+
const storeOptions = getAskUserStoreOptions(params.ctx);
|
|
13221
|
+
if (storeOptions) {
|
|
13222
|
+
terminateAskUserQuestion(storeOptions, params.ctx.questionId, params.reason);
|
|
13223
|
+
}
|
|
13224
|
+
params.ctx.submitted = true;
|
|
13225
|
+
consumePendingQuestion(params.ctx);
|
|
13226
|
+
addHandledQuestionTombstone(
|
|
13227
|
+
params.ctx,
|
|
13228
|
+
params.reason === "cancelled" ? "cancelled" : params.reason === "empty" ? "empty" : params.reason === "submitted" ? "submitted" : params.reason === "expired" ? "expired" : "superseded"
|
|
13229
|
+
);
|
|
13230
|
+
await updateQuestionCardBestEffort(params.ctx, terminalCardVariables(params.reason));
|
|
13231
|
+
}
|
|
12297
13232
|
function parseEmbeddedJson2(value) {
|
|
12298
13233
|
if (typeof value !== "string") {
|
|
12299
13234
|
return value;
|
|
@@ -12413,7 +13348,42 @@ async function injectAnswerSyntheticMessage(ctx, text, suffix) {
|
|
|
12413
13348
|
data: syntheticData,
|
|
12414
13349
|
sessionWebhook: ctx.sessionWebhook,
|
|
12415
13350
|
log: ctx.log,
|
|
12416
|
-
dingtalkConfig: ctx.dingtalkConfig
|
|
13351
|
+
dingtalkConfig: ctx.dingtalkConfig,
|
|
13352
|
+
inboundOrigin: "ask-user",
|
|
13353
|
+
routeOverride: ctx.resolvedRoute,
|
|
13354
|
+
subAgentOptions: ctx.continuationSubAgentOptions
|
|
13355
|
+
});
|
|
13356
|
+
}
|
|
13357
|
+
function claimPendingQuestionForDispatch(ctx) {
|
|
13358
|
+
const storeOptions = getAskUserStoreOptions(ctx);
|
|
13359
|
+
if (storeOptions) {
|
|
13360
|
+
return Boolean(
|
|
13361
|
+
claimAskUserQuestion(storeOptions, {
|
|
13362
|
+
questionId: ctx.questionId,
|
|
13363
|
+
outTrackId: ctx.outTrackId
|
|
13364
|
+
})
|
|
13365
|
+
);
|
|
13366
|
+
}
|
|
13367
|
+
if (ctx.submitted) {
|
|
13368
|
+
return false;
|
|
13369
|
+
}
|
|
13370
|
+
ctx.submitted = true;
|
|
13371
|
+
return true;
|
|
13372
|
+
}
|
|
13373
|
+
function dispatchSyntheticAnswer(params) {
|
|
13374
|
+
const storeOptions = getAskUserStoreOptions(params.ctx);
|
|
13375
|
+
void injectAnswerSyntheticMessage(params.ctx, params.text, params.suffix).then(() => {
|
|
13376
|
+
if (storeOptions) {
|
|
13377
|
+
terminateAskUserQuestion(storeOptions, params.ctx.questionId, params.successReason);
|
|
13378
|
+
}
|
|
13379
|
+
}).catch((err) => {
|
|
13380
|
+
if (storeOptions) {
|
|
13381
|
+
terminateAskUserQuestion(storeOptions, params.ctx.questionId, "dispatch_failed");
|
|
13382
|
+
}
|
|
13383
|
+
void updateQuestionCardBestEffort(params.ctx, terminalCardVariables("dispatch_failed"));
|
|
13384
|
+
params.log?.error?.(
|
|
13385
|
+
`[DingTalk][AskUser] Failed to inject ${params.suffix} answer message: ${String(err)}`
|
|
13386
|
+
);
|
|
12417
13387
|
});
|
|
12418
13388
|
}
|
|
12419
13389
|
async function handleDingTalkAskUserCardCallback(params) {
|
|
@@ -12425,8 +13395,43 @@ async function handleDingTalkAskUserCardCallback(params) {
|
|
|
12425
13395
|
);
|
|
12426
13396
|
return { handled: true };
|
|
12427
13397
|
}
|
|
13398
|
+
const storeOptions = params.storePath ? {
|
|
13399
|
+
storePath: params.storePath,
|
|
13400
|
+
accountId: params.accountId,
|
|
13401
|
+
log: params.log
|
|
13402
|
+
} : void 0;
|
|
13403
|
+
const lifecycleRecord = storeOptions ? resolveAskUserQuestion(storeOptions, {
|
|
13404
|
+
questionId: parsed.actionId,
|
|
13405
|
+
outTrackId: parsed.outTrackId
|
|
13406
|
+
}) : void 0;
|
|
13407
|
+
if (lifecycleRecord?.state === "terminal") {
|
|
13408
|
+
params.log?.debug?.(
|
|
13409
|
+
`[DingTalk][AskUser] Ignoring terminal callback question=${lifecycleRecord.questionId} reason=${lifecycleRecord.terminalReason ?? "unknown"}`
|
|
13410
|
+
);
|
|
13411
|
+
await updateLifecycleRecordCardBestEffort({
|
|
13412
|
+
record: lifecycleRecord,
|
|
13413
|
+
config: params.config,
|
|
13414
|
+
log: params.log
|
|
13415
|
+
});
|
|
13416
|
+
return { handled: true };
|
|
13417
|
+
}
|
|
12428
13418
|
const ctx = (parsed.outTrackId ? pendingQuestionsByTrackId.get(parsed.outTrackId) : void 0) ?? (parsed.actionId ? pendingQuestionsByQuestionId.get(parsed.actionId) : void 0);
|
|
12429
13419
|
if (!ctx) {
|
|
13420
|
+
if (lifecycleRecord && storeOptions) {
|
|
13421
|
+
const recovered = terminateAskUserQuestion(
|
|
13422
|
+
storeOptions,
|
|
13423
|
+
lifecycleRecord.questionId,
|
|
13424
|
+
lifecycleRecord.state === "dispatching" ? "restart_during_dispatch" : "restart_invalidated"
|
|
13425
|
+
);
|
|
13426
|
+
if (recovered) {
|
|
13427
|
+
await updateLifecycleRecordCardBestEffort({
|
|
13428
|
+
record: recovered,
|
|
13429
|
+
config: params.config,
|
|
13430
|
+
log: params.log
|
|
13431
|
+
});
|
|
13432
|
+
}
|
|
13433
|
+
return { handled: true };
|
|
13434
|
+
}
|
|
12430
13435
|
return { handled: false };
|
|
12431
13436
|
}
|
|
12432
13437
|
if (!isOwnerClick(ctx, params.clickerUserId)) {
|
|
@@ -12441,13 +13446,15 @@ async function handleDingTalkAskUserCardCallback(params) {
|
|
|
12441
13446
|
);
|
|
12442
13447
|
return { handled: true };
|
|
12443
13448
|
}
|
|
12444
|
-
if (ctx.submitted) {
|
|
13449
|
+
if (ctx.submitted || lifecycleRecord?.state === "dispatching") {
|
|
12445
13450
|
params.log?.debug?.(`[DingTalk][AskUser] Duplicate submit ignored question=${ctx.questionId}`);
|
|
12446
13451
|
return { handled: true };
|
|
12447
13452
|
}
|
|
12448
13453
|
const isCancel = parseBooleanLike(parsed.params.user_cancel) === true;
|
|
12449
|
-
ctx.submitted = true;
|
|
12450
13454
|
if (isCancel) {
|
|
13455
|
+
if (!claimPendingQuestionForDispatch(ctx)) {
|
|
13456
|
+
return { handled: true };
|
|
13457
|
+
}
|
|
12451
13458
|
await updateQuestionCardBestEffort(ctx, {
|
|
12452
13459
|
card_status: "cancelled",
|
|
12453
13460
|
question_desc: "\u5DF2\u53D6\u6D88\u3002",
|
|
@@ -12455,25 +13462,25 @@ async function handleDingTalkAskUserCardCallback(params) {
|
|
|
12455
13462
|
});
|
|
12456
13463
|
consumePendingQuestion(ctx);
|
|
12457
13464
|
addHandledQuestionTombstone(ctx, "cancelled");
|
|
12458
|
-
|
|
12459
|
-
|
|
12460
|
-
|
|
12461
|
-
|
|
12462
|
-
|
|
12463
|
-
|
|
12464
|
-
}
|
|
12465
|
-
);
|
|
13465
|
+
dispatchSyntheticAnswer({
|
|
13466
|
+
ctx,
|
|
13467
|
+
text: buildCancelledAnswerMessage(ctx),
|
|
13468
|
+
suffix: "cancelled",
|
|
13469
|
+
successReason: "cancelled",
|
|
13470
|
+
log: params.log
|
|
12466
13471
|
});
|
|
12467
13472
|
return { handled: true };
|
|
12468
13473
|
}
|
|
12469
13474
|
const form = asRecord5(parsed.params.form);
|
|
12470
13475
|
if (!form) {
|
|
12471
|
-
ctx.submitted = false;
|
|
12472
13476
|
params.log?.warn?.(
|
|
12473
13477
|
`[DingTalk][AskUser] Missing form payload question=${ctx.questionId} params=${JSON.stringify(parsed.params)}`
|
|
12474
13478
|
);
|
|
12475
13479
|
return { handled: true };
|
|
12476
13480
|
}
|
|
13481
|
+
if (!claimPendingQuestionForDispatch(ctx)) {
|
|
13482
|
+
return { handled: true };
|
|
13483
|
+
}
|
|
12477
13484
|
const answers = [];
|
|
12478
13485
|
const selectedValues = [];
|
|
12479
13486
|
for (const question of ctx.questions) {
|
|
@@ -12497,12 +13504,12 @@ async function handleDingTalkAskUserCardCallback(params) {
|
|
|
12497
13504
|
});
|
|
12498
13505
|
consumePendingQuestion(ctx);
|
|
12499
13506
|
addHandledQuestionTombstone(ctx, "empty");
|
|
12500
|
-
|
|
12501
|
-
|
|
12502
|
-
|
|
12503
|
-
|
|
12504
|
-
|
|
12505
|
-
|
|
13507
|
+
dispatchSyntheticAnswer({
|
|
13508
|
+
ctx,
|
|
13509
|
+
text: buildEmptyAnswerMessage(ctx),
|
|
13510
|
+
suffix: "empty",
|
|
13511
|
+
successReason: "empty",
|
|
13512
|
+
log: params.log
|
|
12506
13513
|
});
|
|
12507
13514
|
return { handled: true };
|
|
12508
13515
|
}
|
|
@@ -12517,10 +13524,12 @@ async function handleDingTalkAskUserCardCallback(params) {
|
|
|
12517
13524
|
consumePendingQuestion(ctx);
|
|
12518
13525
|
addHandledQuestionTombstone(ctx, "submitted");
|
|
12519
13526
|
const message = buildAnswerMessage(ctx, answers);
|
|
12520
|
-
|
|
12521
|
-
|
|
12522
|
-
|
|
12523
|
-
|
|
13527
|
+
dispatchSyntheticAnswer({
|
|
13528
|
+
ctx,
|
|
13529
|
+
text: message,
|
|
13530
|
+
suffix: "submitted",
|
|
13531
|
+
successReason: "submitted",
|
|
13532
|
+
log: params.log
|
|
12524
13533
|
});
|
|
12525
13534
|
return { handled: true };
|
|
12526
13535
|
}
|
|
@@ -12647,13 +13656,12 @@ function registerDingTalkAskUserQuestionTool(api) {
|
|
|
12647
13656
|
api.logger?.warn?.(`${TOOL_NAME}: registerTool unavailable, skipping tool registration`);
|
|
12648
13657
|
return;
|
|
12649
13658
|
}
|
|
12650
|
-
|
|
13659
|
+
const createTool = (context) => ({
|
|
12651
13660
|
name: TOOL_NAME,
|
|
12652
13661
|
label: "Ask User Question",
|
|
12653
13662
|
description: "Ask the user a blocking question or collect structured input via an interactive DingTalk form card when the current task cannot continue without the user's answer. Returns immediately after sending the card. The user's answer will arrive as a new message in the conversation. Do NOT poll or re-call this tool \u2014 just wait for the response message. Use questions only for simple confirmation, single-select, multi-select, or simple free-text prompts. For simple selection questions, provide options; for simple free-text input, set options to an empty array. When collecting multiple missing values, when the user asks for a form, or when you would otherwise list required parameters for the user to fill, call this tool with top-level fields instead of replying with a markdown checklist. Do not call this tool for normal explanations, why/how questions, capability introductions, or cases where you can answer directly.",
|
|
12654
13663
|
parameters: AskUserQuestionSchema,
|
|
12655
13664
|
async execute(_toolCallId, params) {
|
|
12656
|
-
const context = getDingTalkQuestionContext();
|
|
12657
13665
|
if (!context) {
|
|
12658
13666
|
return jsonToolResult({
|
|
12659
13667
|
status: "failed",
|
|
@@ -12687,6 +13695,16 @@ function registerDingTalkAskUserQuestionTool(api) {
|
|
|
12687
13695
|
selected_values: "[]",
|
|
12688
13696
|
form: { fields }
|
|
12689
13697
|
};
|
|
13698
|
+
const storeOptions = getAskUserStoreOptions(context);
|
|
13699
|
+
const canPersistLifecycle = Boolean(storeOptions && context.questionScopeKey);
|
|
13700
|
+
if (storeOptions && context.questionScopeKey) {
|
|
13701
|
+
reserveAskUserQuestion(storeOptions, {
|
|
13702
|
+
questionId,
|
|
13703
|
+
questionScopeKey: context.questionScopeKey,
|
|
13704
|
+
outTrackId,
|
|
13705
|
+
title
|
|
13706
|
+
});
|
|
13707
|
+
}
|
|
12690
13708
|
try {
|
|
12691
13709
|
await createAndDeliverQuestionCard({
|
|
12692
13710
|
config: context.dingtalkConfig,
|
|
@@ -12698,6 +13716,9 @@ function registerDingTalkAskUserQuestionTool(api) {
|
|
|
12698
13716
|
log: context.log
|
|
12699
13717
|
});
|
|
12700
13718
|
} catch (err) {
|
|
13719
|
+
if (storeOptions && canPersistLifecycle) {
|
|
13720
|
+
terminateAskUserQuestion(storeOptions, questionId, "delivery_failed");
|
|
13721
|
+
}
|
|
12701
13722
|
const detail = formatDingTalkErrorPayloadLog("ask_user_create", err, "[DingTalk]");
|
|
12702
13723
|
return jsonToolResult({
|
|
12703
13724
|
status: "failed",
|
|
@@ -12708,20 +13729,75 @@ function registerDingTalkAskUserQuestionTool(api) {
|
|
|
12708
13729
|
...context,
|
|
12709
13730
|
onQuestionCardSent: void 0
|
|
12710
13731
|
};
|
|
12711
|
-
|
|
13732
|
+
const pendingQuestion = {
|
|
12712
13733
|
...pendingContext,
|
|
12713
13734
|
questionId,
|
|
12714
13735
|
outTrackId,
|
|
12715
13736
|
title,
|
|
12716
13737
|
questions: parsed,
|
|
12717
13738
|
submitted: false
|
|
13739
|
+
};
|
|
13740
|
+
storePendingQuestion(pendingQuestion, {
|
|
13741
|
+
supersedeExisting: !canPersistLifecycle
|
|
12718
13742
|
});
|
|
13743
|
+
if (storeOptions && canPersistLifecycle) {
|
|
13744
|
+
const activation = activateAskUserQuestion(storeOptions, questionId);
|
|
13745
|
+
if (activation.record?.state !== "pending") {
|
|
13746
|
+
const terminalReason = activation.record?.terminalReason ?? "superseded_by_message";
|
|
13747
|
+
if (activation.record) {
|
|
13748
|
+
consumeLifecyclePendingContext(activation.record);
|
|
13749
|
+
} else {
|
|
13750
|
+
pendingQuestion.submitted = true;
|
|
13751
|
+
consumePendingQuestion(pendingQuestion);
|
|
13752
|
+
addHandledQuestionTombstone(pendingQuestion, "superseded");
|
|
13753
|
+
}
|
|
13754
|
+
await updateQuestionCardBestEffort(
|
|
13755
|
+
pendingQuestion,
|
|
13756
|
+
terminalCardVariables(terminalReason)
|
|
13757
|
+
);
|
|
13758
|
+
return jsonToolResult({
|
|
13759
|
+
status: "failed",
|
|
13760
|
+
questionId,
|
|
13761
|
+
outTrackId,
|
|
13762
|
+
error: "\u95EE\u9898\u5361\u7247\u5728\u53D1\u9001\u671F\u95F4\u5DF2\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u53D1\u8D77\u3002"
|
|
13763
|
+
});
|
|
13764
|
+
}
|
|
13765
|
+
for (const superseded of activation.superseded) {
|
|
13766
|
+
const supersededContext = consumeLifecyclePendingContext(superseded);
|
|
13767
|
+
if (supersededContext) {
|
|
13768
|
+
void updateQuestionCardBestEffort(
|
|
13769
|
+
supersededContext,
|
|
13770
|
+
terminalCardVariables("superseded_by_question")
|
|
13771
|
+
);
|
|
13772
|
+
} else {
|
|
13773
|
+
void updateLifecycleRecordCardBestEffort({
|
|
13774
|
+
record: superseded,
|
|
13775
|
+
config: context.dingtalkConfig,
|
|
13776
|
+
log: context.log
|
|
13777
|
+
});
|
|
13778
|
+
}
|
|
13779
|
+
}
|
|
13780
|
+
}
|
|
13781
|
+
let takeoverSucceeded = void 0;
|
|
12719
13782
|
try {
|
|
12720
|
-
await context.onQuestionCardSent?.({ questionId, outTrackId });
|
|
13783
|
+
takeoverSucceeded = await context.onQuestionCardSent?.({ questionId, outTrackId });
|
|
12721
13784
|
} catch (err) {
|
|
12722
13785
|
context.log?.warn?.(
|
|
12723
13786
|
`[DingTalk][AskUser] onQuestionCardSent hook failed: ${err instanceof Error ? err.message : String(err)}`
|
|
12724
13787
|
);
|
|
13788
|
+
takeoverSucceeded = false;
|
|
13789
|
+
}
|
|
13790
|
+
if (takeoverSucceeded === false) {
|
|
13791
|
+
await terminatePendingQuestion({
|
|
13792
|
+
ctx: pendingQuestion,
|
|
13793
|
+
reason: "pause_failed"
|
|
13794
|
+
});
|
|
13795
|
+
return jsonToolResult({
|
|
13796
|
+
status: "failed",
|
|
13797
|
+
questionId,
|
|
13798
|
+
outTrackId,
|
|
13799
|
+
error: "\u5F53\u524D\u4EFB\u52A1\u672A\u80FD\u6682\u505C\uFF0C\u6B64\u5361\u5DF2\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u53D1\u8D77\u3002"
|
|
13800
|
+
});
|
|
12725
13801
|
}
|
|
12726
13802
|
context.log?.info?.(
|
|
12727
13803
|
`[DingTalk][AskUser] question card sent question=${questionId} outTrackId=${outTrackId}`
|
|
@@ -12734,6 +13810,18 @@ function registerDingTalkAskUserQuestionTool(api) {
|
|
|
12734
13810
|
});
|
|
12735
13811
|
}
|
|
12736
13812
|
});
|
|
13813
|
+
registerTool.call(
|
|
13814
|
+
api,
|
|
13815
|
+
(toolContext) => {
|
|
13816
|
+
const context = getDingTalkQuestionContext();
|
|
13817
|
+
const runtimeSessionKey = toolContext.sessionKey?.trim();
|
|
13818
|
+
const contextSessionKey = context?.resolvedRoute?.sessionKey.trim();
|
|
13819
|
+
return createTool(
|
|
13820
|
+
context && (!runtimeSessionKey || contextSessionKey === runtimeSessionKey) ? context : void 0
|
|
13821
|
+
);
|
|
13822
|
+
},
|
|
13823
|
+
{ name: TOOL_NAME }
|
|
13824
|
+
);
|
|
12737
13825
|
api.logger?.debug?.(`${TOOL_NAME}: registered tool`);
|
|
12738
13826
|
}
|
|
12739
13827
|
|
|
@@ -12926,6 +14014,7 @@ async function handleCardAction(params) {
|
|
|
12926
14014
|
payload: params.payload,
|
|
12927
14015
|
cfg: params.cfg,
|
|
12928
14016
|
accountId: params.accountId,
|
|
14017
|
+
storePath: params.storePath,
|
|
12929
14018
|
config: params.config,
|
|
12930
14019
|
clickerUserId: params.analysis.userId,
|
|
12931
14020
|
log: params.log
|
|
@@ -13066,8 +14155,8 @@ var ConnectionManager = class _ConnectionManager {
|
|
|
13066
14155
|
`[${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
14156
|
);
|
|
13068
14157
|
}
|
|
13069
|
-
recordSocketActivity(
|
|
13070
|
-
this.lastSocketActivityAt =
|
|
14158
|
+
recordSocketActivity(now2 = Date.now()) {
|
|
14159
|
+
this.lastSocketActivityAt = now2;
|
|
13071
14160
|
this.consecutiveHeartbeatMisses = 0;
|
|
13072
14161
|
}
|
|
13073
14162
|
setupHeartbeat(socket) {
|
|
@@ -13079,13 +14168,13 @@ var ConnectionManager = class _ConnectionManager {
|
|
|
13079
14168
|
if (socket.readyState !== 1) {
|
|
13080
14169
|
return;
|
|
13081
14170
|
}
|
|
13082
|
-
const
|
|
13083
|
-
const idleMs = this.lastSocketActivityAt !== void 0 ?
|
|
14171
|
+
const now2 = Date.now();
|
|
14172
|
+
const idleMs = this.lastSocketActivityAt !== void 0 ? now2 - this.lastSocketActivityAt : _ConnectionManager.HEARTBEAT_INTERVAL_MS;
|
|
13084
14173
|
if (idleMs >= _ConnectionManager.HEARTBEAT_INTERVAL_MS) {
|
|
13085
14174
|
this.consecutiveHeartbeatMisses += 1;
|
|
13086
14175
|
this.runtimeCounters.heartbeatMisses += 1;
|
|
13087
14176
|
if (this.consecutiveHeartbeatMisses >= _ConnectionManager.HEARTBEAT_MISS_THRESHOLD) {
|
|
13088
|
-
const lastPingAgoMs = this.lastHeartbeatPingAt !== void 0 ?
|
|
14177
|
+
const lastPingAgoMs = this.lastHeartbeatPingAt !== void 0 ? now2 - this.lastHeartbeatPingAt : void 0;
|
|
13089
14178
|
this.log?.warn?.(
|
|
13090
14179
|
`[${this.accountId}] Connection heartbeat missed ${this.consecutiveHeartbeatMisses}/${_ConnectionManager.HEARTBEAT_MISS_THRESHOLD} checks, triggering reconnection (lastPingAgoMs=${lastPingAgoMs ?? "n/a"})`
|
|
13091
14180
|
);
|
|
@@ -13099,7 +14188,7 @@ var ConnectionManager = class _ConnectionManager {
|
|
|
13099
14188
|
`[${this.accountId}] Connection heartbeat missed (${this.consecutiveHeartbeatMisses}/${_ConnectionManager.HEARTBEAT_MISS_THRESHOLD})`
|
|
13100
14189
|
);
|
|
13101
14190
|
}
|
|
13102
|
-
this.lastHeartbeatPingAt =
|
|
14191
|
+
this.lastHeartbeatPingAt = now2;
|
|
13103
14192
|
try {
|
|
13104
14193
|
socket.ping("", true);
|
|
13105
14194
|
} catch (err) {
|
|
@@ -13340,8 +14429,8 @@ var ConnectionManager = class _ConnectionManager {
|
|
|
13340
14429
|
this.consecutiveUnhealthyChecks = 0;
|
|
13341
14430
|
return;
|
|
13342
14431
|
}
|
|
13343
|
-
const
|
|
13344
|
-
const withinGraceWindow = this.connectedAt !== void 0 &&
|
|
14432
|
+
const now2 = Date.now();
|
|
14433
|
+
const withinGraceWindow = this.connectedAt !== void 0 && now2 - this.connectedAt < _ConnectionManager.HEALTH_CHECK_GRACE_MS;
|
|
13345
14434
|
if (withinGraceWindow) {
|
|
13346
14435
|
this.consecutiveUnhealthyChecks = 0;
|
|
13347
14436
|
return;
|
|
@@ -13699,12 +14788,12 @@ var MESSAGE_DEDUP_TTL = 6e4;
|
|
|
13699
14788
|
var MESSAGE_DEDUP_MAX_SIZE = 1e3;
|
|
13700
14789
|
var messageCounter = 0;
|
|
13701
14790
|
function isMessageProcessed(dedupKey) {
|
|
13702
|
-
const
|
|
14791
|
+
const now2 = Date.now();
|
|
13703
14792
|
const expiresAt = processedMessages.get(dedupKey);
|
|
13704
14793
|
if (expiresAt === void 0) {
|
|
13705
14794
|
return false;
|
|
13706
14795
|
}
|
|
13707
|
-
if (
|
|
14796
|
+
if (now2 >= expiresAt) {
|
|
13708
14797
|
processedMessages.delete(dedupKey);
|
|
13709
14798
|
return false;
|
|
13710
14799
|
}
|
|
@@ -13714,9 +14803,9 @@ function markMessageProcessed(dedupKey) {
|
|
|
13714
14803
|
const expiresAt = Date.now() + MESSAGE_DEDUP_TTL;
|
|
13715
14804
|
processedMessages.set(dedupKey, expiresAt);
|
|
13716
14805
|
if (processedMessages.size > MESSAGE_DEDUP_MAX_SIZE) {
|
|
13717
|
-
const
|
|
14806
|
+
const now2 = Date.now();
|
|
13718
14807
|
for (const [key, expiry] of processedMessages.entries()) {
|
|
13719
|
-
if (
|
|
14808
|
+
if (now2 >= expiry) {
|
|
13720
14809
|
processedMessages.delete(key);
|
|
13721
14810
|
}
|
|
13722
14811
|
}
|
|
@@ -13735,9 +14824,9 @@ function markMessageProcessed(dedupKey) {
|
|
|
13735
14824
|
messageCounter++;
|
|
13736
14825
|
if (messageCounter >= 10) {
|
|
13737
14826
|
messageCounter = 0;
|
|
13738
|
-
const
|
|
14827
|
+
const now2 = Date.now();
|
|
13739
14828
|
for (const [key, expiry] of processedMessages.entries()) {
|
|
13740
|
-
if (
|
|
14829
|
+
if (now2 >= expiry) {
|
|
13741
14830
|
processedMessages.delete(key);
|
|
13742
14831
|
}
|
|
13743
14832
|
}
|
|
@@ -13794,7 +14883,6 @@ function instrumentConnectionStages(client) {
|
|
|
13794
14883
|
}
|
|
13795
14884
|
};
|
|
13796
14885
|
}
|
|
13797
|
-
var INFLIGHT_TTL_MS = 5 * 60 * 1e3;
|
|
13798
14886
|
var processingDedupKeys = /* @__PURE__ */ new Map();
|
|
13799
14887
|
var inboundCountersByAccount = /* @__PURE__ */ new Map();
|
|
13800
14888
|
var INBOUND_COUNTER_LOG_EVERY = 10;
|
|
@@ -13864,6 +14952,23 @@ function createDingTalkGateway() {
|
|
|
13864
14952
|
`[${account.accountId}] Failed to recover unfinished cards: ${err.message}`
|
|
13865
14953
|
);
|
|
13866
14954
|
}
|
|
14955
|
+
try {
|
|
14956
|
+
const recoveredQuestions = await recoverAskUserQuestionsForAccount({
|
|
14957
|
+
storePath: accountStorePath,
|
|
14958
|
+
accountId: account.accountId,
|
|
14959
|
+
config,
|
|
14960
|
+
log: pluginLog
|
|
14961
|
+
});
|
|
14962
|
+
if (recoveredQuestions > 0) {
|
|
14963
|
+
pluginLog?.info?.(
|
|
14964
|
+
`[${account.accountId}] Invalidated ${recoveredQuestions} unfinished Ask User card(s) from previous runtime`
|
|
14965
|
+
);
|
|
14966
|
+
}
|
|
14967
|
+
} catch (err) {
|
|
14968
|
+
pluginLog?.warn?.(
|
|
14969
|
+
`[${account.accountId}] Failed to recover Ask User cards: ${err.message}`
|
|
14970
|
+
);
|
|
14971
|
+
}
|
|
13867
14972
|
const useConnectionManager = config.useConnectionManager ?? true;
|
|
13868
14973
|
const applyStatusPatch = (patch) => {
|
|
13869
14974
|
ctx.setStatus({
|
|
@@ -13921,7 +15026,9 @@ function createDingTalkGateway() {
|
|
|
13921
15026
|
data,
|
|
13922
15027
|
sessionWebhook: data.sessionWebhook,
|
|
13923
15028
|
log: pluginLog,
|
|
13924
|
-
dingtalkConfig: config
|
|
15029
|
+
dingtalkConfig: config,
|
|
15030
|
+
inboundOrigin: "stream",
|
|
15031
|
+
inboundQueueEligible: true
|
|
13925
15032
|
});
|
|
13926
15033
|
stats.processed += 1;
|
|
13927
15034
|
if (stats.received % INBOUND_COUNTER_LOG_EVERY === 0) {
|
|
@@ -13936,22 +15043,14 @@ function createDingTalkGateway() {
|
|
|
13936
15043
|
logInboundCounters(pluginLog, account.accountId, "dedup-skipped");
|
|
13937
15044
|
return;
|
|
13938
15045
|
}
|
|
13939
|
-
|
|
13940
|
-
|
|
13941
|
-
|
|
13942
|
-
|
|
13943
|
-
|
|
13944
|
-
|
|
13945
|
-
|
|
13946
|
-
|
|
13947
|
-
pluginLog?.debug?.(
|
|
13948
|
-
`[${account.accountId}] Skipping in-flight duplicate message: ${dedupKey}`
|
|
13949
|
-
);
|
|
13950
|
-
stats.inflightSkipped += 1;
|
|
13951
|
-
acknowledge();
|
|
13952
|
-
logInboundCounters(pluginLog, account.accountId, "inflight-skipped");
|
|
13953
|
-
return;
|
|
13954
|
-
}
|
|
15046
|
+
if (processingDedupKeys.has(dedupKey)) {
|
|
15047
|
+
pluginLog?.debug?.(
|
|
15048
|
+
`[${account.accountId}] Skipping in-flight duplicate message: ${dedupKey}`
|
|
15049
|
+
);
|
|
15050
|
+
stats.inflightSkipped += 1;
|
|
15051
|
+
acknowledge();
|
|
15052
|
+
logInboundCounters(pluginLog, account.accountId, "inflight-skipped");
|
|
15053
|
+
return;
|
|
13955
15054
|
}
|
|
13956
15055
|
acknowledge();
|
|
13957
15056
|
processingDedupKeys.set(dedupKey, Date.now());
|
|
@@ -13962,7 +15061,9 @@ function createDingTalkGateway() {
|
|
|
13962
15061
|
data,
|
|
13963
15062
|
sessionWebhook: data.sessionWebhook,
|
|
13964
15063
|
log: pluginLog,
|
|
13965
|
-
dingtalkConfig: config
|
|
15064
|
+
dingtalkConfig: config,
|
|
15065
|
+
inboundOrigin: "stream",
|
|
15066
|
+
inboundQueueEligible: true
|
|
13966
15067
|
});
|
|
13967
15068
|
stats.processed += 1;
|
|
13968
15069
|
markMessageProcessed(dedupKey);
|
|
@@ -14034,6 +15135,7 @@ function createDingTalkGateway() {
|
|
|
14034
15135
|
analysis,
|
|
14035
15136
|
cfg,
|
|
14036
15137
|
accountId: account.accountId,
|
|
15138
|
+
storePath: accountStorePath,
|
|
14037
15139
|
config,
|
|
14038
15140
|
log: pluginLog
|
|
14039
15141
|
});
|
|
@@ -14185,19 +15287,6 @@ function createDingTalkGateway() {
|
|
|
14185
15287
|
lastError: null
|
|
14186
15288
|
});
|
|
14187
15289
|
} else if (state === "FAILED" /* FAILED */ || state === "DISCONNECTED" /* DISCONNECTED */) {
|
|
14188
|
-
const robotKey = resolveRobotCode(config) || account.accountId;
|
|
14189
|
-
let cleared = 0;
|
|
14190
|
-
for (const key of processingDedupKeys.keys()) {
|
|
14191
|
-
if (key.startsWith(`${robotKey}:`)) {
|
|
14192
|
-
processingDedupKeys.delete(key);
|
|
14193
|
-
cleared++;
|
|
14194
|
-
}
|
|
14195
|
-
}
|
|
14196
|
-
if (cleared > 0) {
|
|
14197
|
-
pluginLog?.info?.(
|
|
14198
|
-
`[${account.accountId}] Cleared ${cleared} stale in-flight lock(s) on disconnect`
|
|
14199
|
-
);
|
|
14200
|
-
}
|
|
14201
15290
|
applyStatusPatch({
|
|
14202
15291
|
running: false,
|
|
14203
15292
|
connected: false,
|
|
@@ -14338,13 +15427,13 @@ async function beginDeviceRegistration() {
|
|
|
14338
15427
|
}) : null;
|
|
14339
15428
|
abortPromise?.catch(() => {
|
|
14340
15429
|
});
|
|
14341
|
-
const
|
|
15430
|
+
const sleep2 = () => new Promise((resolve3) => setTimeout(resolve3, interval * 1e3));
|
|
14342
15431
|
try {
|
|
14343
15432
|
while (Date.now() < deadline) {
|
|
14344
15433
|
if (signal?.aborted) {
|
|
14345
15434
|
throw new RegistrationError("registration cancelled");
|
|
14346
15435
|
}
|
|
14347
|
-
await (abortPromise ? Promise.race([
|
|
15436
|
+
await (abortPromise ? Promise.race([sleep2(), abortPromise]) : sleep2());
|
|
14348
15437
|
if (signal?.aborted) {
|
|
14349
15438
|
throw new RegistrationError("registration cancelled");
|
|
14350
15439
|
}
|
|
@@ -14704,9 +15793,9 @@ async function configureDingTalkAccount(params) {
|
|
|
14704
15793
|
let lastWaitingNote = 0;
|
|
14705
15794
|
const result = await session.waitForResult({
|
|
14706
15795
|
onWaiting: () => {
|
|
14707
|
-
const
|
|
14708
|
-
if (
|
|
14709
|
-
lastWaitingNote =
|
|
15796
|
+
const now2 = Date.now();
|
|
15797
|
+
if (now2 - lastWaitingNote >= 15e3) {
|
|
15798
|
+
lastWaitingNote = now2;
|
|
14710
15799
|
prompter.note("Waiting for authorization. Please finish the scan in DingTalk...", "Polling").catch(() => {
|
|
14711
15800
|
});
|
|
14712
15801
|
}
|