@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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
|
|
2
|
+
import type { OpenClawPluginApi, OpenClawPluginToolContext } from "openclaw/plugin-sdk/core";
|
|
3
3
|
import { getAccessToken } from "../auth";
|
|
4
4
|
import { updateCardVariables } from "../card-callback-service";
|
|
5
5
|
import { resolveRobotCode } from "../config";
|
|
@@ -11,6 +11,18 @@ import {
|
|
|
11
11
|
getDingTalkQuestionContext,
|
|
12
12
|
type DingTalkQuestionContext,
|
|
13
13
|
} from "./ask-user-question-context";
|
|
14
|
+
import {
|
|
15
|
+
activateAskUserQuestion,
|
|
16
|
+
claimAskUserQuestion,
|
|
17
|
+
invalidateAskUserQuestionsInScope as invalidateAskUserQuestionsInStore,
|
|
18
|
+
recoverAskUserQuestionsAfterRestart,
|
|
19
|
+
reserveAskUserQuestion,
|
|
20
|
+
resolveAskUserQuestion,
|
|
21
|
+
terminateAskUserQuestion,
|
|
22
|
+
type AskUserLifecycleRecord,
|
|
23
|
+
type AskUserStoreOptions,
|
|
24
|
+
type AskUserTerminalReason,
|
|
25
|
+
} from "./ask-user-question-store";
|
|
14
26
|
import { DINGTALK_ASK_USER_CARD_TEMPLATE } from "./card-template";
|
|
15
27
|
|
|
16
28
|
const DINGTALK_API = "https://api.dingtalk.com";
|
|
@@ -439,9 +451,14 @@ function supersedePendingQuestionsInScope(ctx: PendingQuestion): void {
|
|
|
439
451
|
}
|
|
440
452
|
}
|
|
441
453
|
|
|
442
|
-
function storePendingQuestion(
|
|
454
|
+
function storePendingQuestion(
|
|
455
|
+
ctx: PendingQuestion,
|
|
456
|
+
options: { supersedeExisting?: boolean } = {},
|
|
457
|
+
): void {
|
|
443
458
|
ctx.ownerUserId = resolvePendingQuestionOwner(ctx);
|
|
444
|
-
|
|
459
|
+
if (options.supersedeExisting !== false) {
|
|
460
|
+
supersedePendingQuestionsInScope(ctx);
|
|
461
|
+
}
|
|
445
462
|
pendingQuestionsByTrackId.set(ctx.outTrackId, ctx);
|
|
446
463
|
pendingQuestionsByQuestionId.set(ctx.questionId, ctx);
|
|
447
464
|
addScopeIndex(ctx);
|
|
@@ -449,7 +466,9 @@ function storePendingQuestion(ctx: PendingQuestion): void {
|
|
|
449
466
|
if (!pendingQuestionsByTrackId.has(ctx.outTrackId) || ctx.submitted) {
|
|
450
467
|
return;
|
|
451
468
|
}
|
|
452
|
-
ctx
|
|
469
|
+
if (!claimPendingQuestionForDispatch(ctx)) {
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
453
472
|
consumePendingQuestion(ctx);
|
|
454
473
|
addHandledQuestionTombstone(ctx, "expired");
|
|
455
474
|
void updateQuestionCardBestEffort(ctx, {
|
|
@@ -457,14 +476,12 @@ function storePendingQuestion(ctx: PendingQuestion): void {
|
|
|
457
476
|
question_desc: "问题已失效,请重新发起。",
|
|
458
477
|
form_btn_text: "已失效",
|
|
459
478
|
});
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
},
|
|
467
|
-
);
|
|
479
|
+
dispatchSyntheticAnswer({
|
|
480
|
+
ctx,
|
|
481
|
+
text: buildExpiredAnswerMessage(ctx),
|
|
482
|
+
suffix: "expired",
|
|
483
|
+
successReason: "expired",
|
|
484
|
+
log: ctx.log,
|
|
468
485
|
});
|
|
469
486
|
}, PENDING_QUESTION_TTL_MS);
|
|
470
487
|
}
|
|
@@ -499,6 +516,177 @@ async function updateQuestionCardBestEffort(
|
|
|
499
516
|
}
|
|
500
517
|
}
|
|
501
518
|
|
|
519
|
+
function getAskUserStoreOptions(
|
|
520
|
+
params: Pick<DingTalkQuestionContext, "storePath" | "accountId" | "log">,
|
|
521
|
+
): AskUserStoreOptions | undefined {
|
|
522
|
+
if (!params.storePath) {
|
|
523
|
+
return undefined;
|
|
524
|
+
}
|
|
525
|
+
return {
|
|
526
|
+
storePath: params.storePath,
|
|
527
|
+
accountId: params.accountId,
|
|
528
|
+
log: params.log,
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function terminalCardVariables(reason: AskUserTerminalReason): Record<string, unknown> {
|
|
533
|
+
const descriptions: Record<AskUserTerminalReason, string> = {
|
|
534
|
+
delivery_failed: "问题卡片发送失败。",
|
|
535
|
+
superseded_by_question: "已有新的问题卡片,请回答最新卡片。",
|
|
536
|
+
superseded_by_message: "你在问题卡片发出后发送了新消息,此卡已失效。请重新发起需要填写的问题。",
|
|
537
|
+
expired: "问题已失效,请重新发起。",
|
|
538
|
+
cancelled: "已取消。",
|
|
539
|
+
empty: "已提交,未填写任何内容。",
|
|
540
|
+
submitted: "已提交。",
|
|
541
|
+
pause_failed: "当前任务未能暂停,此卡已失效,请重新发起。",
|
|
542
|
+
restart_invalidated: "服务已重启,原问题上下文已失效,请重新发起。",
|
|
543
|
+
restart_during_dispatch: "服务在处理回答期间重启,本次处理结果可能未完成,请发送新消息继续。",
|
|
544
|
+
dispatch_failed: "回答已收到,但未能继续会话,请发送一条普通消息继续。",
|
|
545
|
+
};
|
|
546
|
+
return {
|
|
547
|
+
card_status:
|
|
548
|
+
reason === "cancelled"
|
|
549
|
+
? "cancelled"
|
|
550
|
+
: reason === "submitted" || reason === "empty"
|
|
551
|
+
? "submitted"
|
|
552
|
+
: "expired",
|
|
553
|
+
question_desc: descriptions[reason],
|
|
554
|
+
form_btn_text:
|
|
555
|
+
reason === "cancelled"
|
|
556
|
+
? "已取消"
|
|
557
|
+
: reason === "submitted" || reason === "empty"
|
|
558
|
+
? "已提交"
|
|
559
|
+
: "已失效",
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
async function updateLifecycleRecordCardBestEffort(params: {
|
|
564
|
+
record: AskUserLifecycleRecord;
|
|
565
|
+
config: DingTalkConfig;
|
|
566
|
+
log?: Logger;
|
|
567
|
+
}): Promise<void> {
|
|
568
|
+
const reason = params.record.terminalReason;
|
|
569
|
+
if (!reason || reason === "delivery_failed") {
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
try {
|
|
573
|
+
const token = await getAccessToken(params.config, params.log);
|
|
574
|
+
await updateCardVariables(
|
|
575
|
+
params.record.outTrackId,
|
|
576
|
+
terminalCardVariables(reason),
|
|
577
|
+
token,
|
|
578
|
+
params.config,
|
|
579
|
+
);
|
|
580
|
+
} catch (err) {
|
|
581
|
+
params.log?.warn?.(
|
|
582
|
+
`[DingTalk][AskUser] Failed to update lifecycle card ${params.record.questionId}: ${String(err)}`,
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function consumeLifecyclePendingContext(
|
|
588
|
+
record: AskUserLifecycleRecord,
|
|
589
|
+
): PendingQuestion | undefined {
|
|
590
|
+
const ctx =
|
|
591
|
+
pendingQuestionsByQuestionId.get(record.questionId) ??
|
|
592
|
+
pendingQuestionsByTrackId.get(record.outTrackId);
|
|
593
|
+
if (!ctx) {
|
|
594
|
+
return undefined;
|
|
595
|
+
}
|
|
596
|
+
ctx.submitted = true;
|
|
597
|
+
consumePendingQuestion(ctx);
|
|
598
|
+
addHandledQuestionTombstone(ctx, record.terminalReason === "expired" ? "expired" : "superseded");
|
|
599
|
+
return ctx;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
export function invalidateAskUserQuestionsForScope(params: {
|
|
603
|
+
storePath: string;
|
|
604
|
+
accountId: string;
|
|
605
|
+
questionScopeKey: string;
|
|
606
|
+
reason: "superseded_by_message";
|
|
607
|
+
log?: Logger;
|
|
608
|
+
}): AskUserLifecycleRecord[] {
|
|
609
|
+
const invalidated = invalidateAskUserQuestionsInStore(
|
|
610
|
+
{
|
|
611
|
+
storePath: params.storePath,
|
|
612
|
+
accountId: params.accountId,
|
|
613
|
+
log: params.log,
|
|
614
|
+
},
|
|
615
|
+
params.questionScopeKey,
|
|
616
|
+
params.reason,
|
|
617
|
+
);
|
|
618
|
+
for (const record of invalidated) {
|
|
619
|
+
consumeLifecyclePendingContext(record);
|
|
620
|
+
}
|
|
621
|
+
return invalidated;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
export async function syncInvalidatedAskUserQuestionCards(params: {
|
|
625
|
+
records: AskUserLifecycleRecord[];
|
|
626
|
+
config: DingTalkConfig;
|
|
627
|
+
log?: Logger;
|
|
628
|
+
}): Promise<void> {
|
|
629
|
+
await Promise.allSettled(
|
|
630
|
+
params.records.map((record) =>
|
|
631
|
+
updateLifecycleRecordCardBestEffort({
|
|
632
|
+
record,
|
|
633
|
+
config: params.config,
|
|
634
|
+
log: params.log,
|
|
635
|
+
}),
|
|
636
|
+
),
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
export async function recoverAskUserQuestionsForAccount(params: {
|
|
641
|
+
storePath?: string;
|
|
642
|
+
accountId: string;
|
|
643
|
+
config: DingTalkConfig;
|
|
644
|
+
log?: Logger;
|
|
645
|
+
}): Promise<number> {
|
|
646
|
+
if (!params.storePath) {
|
|
647
|
+
return 0;
|
|
648
|
+
}
|
|
649
|
+
const recovered = recoverAskUserQuestionsAfterRestart({
|
|
650
|
+
storePath: params.storePath,
|
|
651
|
+
accountId: params.accountId,
|
|
652
|
+
log: params.log,
|
|
653
|
+
});
|
|
654
|
+
for (const record of recovered) {
|
|
655
|
+
consumeLifecyclePendingContext(record);
|
|
656
|
+
await updateLifecycleRecordCardBestEffort({
|
|
657
|
+
record,
|
|
658
|
+
config: params.config,
|
|
659
|
+
log: params.log,
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
return recovered.length;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
async function terminatePendingQuestion(params: {
|
|
666
|
+
ctx: PendingQuestion;
|
|
667
|
+
reason: AskUserTerminalReason;
|
|
668
|
+
}): Promise<void> {
|
|
669
|
+
const storeOptions = getAskUserStoreOptions(params.ctx);
|
|
670
|
+
if (storeOptions) {
|
|
671
|
+
terminateAskUserQuestion(storeOptions, params.ctx.questionId, params.reason);
|
|
672
|
+
}
|
|
673
|
+
params.ctx.submitted = true;
|
|
674
|
+
consumePendingQuestion(params.ctx);
|
|
675
|
+
addHandledQuestionTombstone(
|
|
676
|
+
params.ctx,
|
|
677
|
+
params.reason === "cancelled"
|
|
678
|
+
? "cancelled"
|
|
679
|
+
: params.reason === "empty"
|
|
680
|
+
? "empty"
|
|
681
|
+
: params.reason === "submitted"
|
|
682
|
+
? "submitted"
|
|
683
|
+
: params.reason === "expired"
|
|
684
|
+
? "expired"
|
|
685
|
+
: "superseded",
|
|
686
|
+
);
|
|
687
|
+
await updateQuestionCardBestEffort(params.ctx, terminalCardVariables(params.reason));
|
|
688
|
+
}
|
|
689
|
+
|
|
502
690
|
function parseEmbeddedJson(value: unknown): unknown {
|
|
503
691
|
if (typeof value !== "string") {
|
|
504
692
|
return value;
|
|
@@ -644,13 +832,59 @@ async function injectAnswerSyntheticMessage(
|
|
|
644
832
|
sessionWebhook: ctx.sessionWebhook,
|
|
645
833
|
log: ctx.log,
|
|
646
834
|
dingtalkConfig: ctx.dingtalkConfig,
|
|
835
|
+
inboundOrigin: "ask-user",
|
|
836
|
+
routeOverride: ctx.resolvedRoute,
|
|
837
|
+
subAgentOptions: ctx.continuationSubAgentOptions,
|
|
647
838
|
});
|
|
648
839
|
}
|
|
649
840
|
|
|
841
|
+
function claimPendingQuestionForDispatch(ctx: PendingQuestion): boolean {
|
|
842
|
+
const storeOptions = getAskUserStoreOptions(ctx);
|
|
843
|
+
if (storeOptions) {
|
|
844
|
+
return Boolean(
|
|
845
|
+
claimAskUserQuestion(storeOptions, {
|
|
846
|
+
questionId: ctx.questionId,
|
|
847
|
+
outTrackId: ctx.outTrackId,
|
|
848
|
+
}),
|
|
849
|
+
);
|
|
850
|
+
}
|
|
851
|
+
if (ctx.submitted) {
|
|
852
|
+
return false;
|
|
853
|
+
}
|
|
854
|
+
ctx.submitted = true;
|
|
855
|
+
return true;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
function dispatchSyntheticAnswer(params: {
|
|
859
|
+
ctx: PendingQuestion;
|
|
860
|
+
text: string;
|
|
861
|
+
suffix: string;
|
|
862
|
+
successReason: "submitted" | "cancelled" | "empty" | "expired";
|
|
863
|
+
log?: Logger;
|
|
864
|
+
}): void {
|
|
865
|
+
const storeOptions = getAskUserStoreOptions(params.ctx);
|
|
866
|
+
void injectAnswerSyntheticMessage(params.ctx, params.text, params.suffix)
|
|
867
|
+
.then(() => {
|
|
868
|
+
if (storeOptions) {
|
|
869
|
+
terminateAskUserQuestion(storeOptions, params.ctx.questionId, params.successReason);
|
|
870
|
+
}
|
|
871
|
+
})
|
|
872
|
+
.catch((err) => {
|
|
873
|
+
if (storeOptions) {
|
|
874
|
+
terminateAskUserQuestion(storeOptions, params.ctx.questionId, "dispatch_failed");
|
|
875
|
+
}
|
|
876
|
+
void updateQuestionCardBestEffort(params.ctx, terminalCardVariables("dispatch_failed"));
|
|
877
|
+
params.log?.error?.(
|
|
878
|
+
`[DingTalk][AskUser] Failed to inject ${params.suffix} answer message: ${String(err)}`,
|
|
879
|
+
);
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
|
|
650
883
|
export async function handleDingTalkAskUserCardCallback(params: {
|
|
651
884
|
payload: unknown;
|
|
652
885
|
cfg: DingTalkQuestionContext["cfg"];
|
|
653
886
|
accountId: string;
|
|
887
|
+
storePath?: string;
|
|
654
888
|
config: DingTalkConfig;
|
|
655
889
|
clickerUserId?: string;
|
|
656
890
|
log?: Logger;
|
|
@@ -663,10 +897,49 @@ export async function handleDingTalkAskUserCardCallback(params: {
|
|
|
663
897
|
);
|
|
664
898
|
return { handled: true };
|
|
665
899
|
}
|
|
900
|
+
const storeOptions = params.storePath
|
|
901
|
+
? {
|
|
902
|
+
storePath: params.storePath,
|
|
903
|
+
accountId: params.accountId,
|
|
904
|
+
log: params.log,
|
|
905
|
+
}
|
|
906
|
+
: undefined;
|
|
907
|
+
const lifecycleRecord = storeOptions
|
|
908
|
+
? resolveAskUserQuestion(storeOptions, {
|
|
909
|
+
questionId: parsed.actionId,
|
|
910
|
+
outTrackId: parsed.outTrackId,
|
|
911
|
+
})
|
|
912
|
+
: undefined;
|
|
913
|
+
if (lifecycleRecord?.state === "terminal") {
|
|
914
|
+
params.log?.debug?.(
|
|
915
|
+
`[DingTalk][AskUser] Ignoring terminal callback question=${lifecycleRecord.questionId} reason=${lifecycleRecord.terminalReason ?? "unknown"}`,
|
|
916
|
+
);
|
|
917
|
+
await updateLifecycleRecordCardBestEffort({
|
|
918
|
+
record: lifecycleRecord,
|
|
919
|
+
config: params.config,
|
|
920
|
+
log: params.log,
|
|
921
|
+
});
|
|
922
|
+
return { handled: true };
|
|
923
|
+
}
|
|
666
924
|
const ctx =
|
|
667
925
|
(parsed.outTrackId ? pendingQuestionsByTrackId.get(parsed.outTrackId) : undefined) ??
|
|
668
926
|
(parsed.actionId ? pendingQuestionsByQuestionId.get(parsed.actionId) : undefined);
|
|
669
927
|
if (!ctx) {
|
|
928
|
+
if (lifecycleRecord && storeOptions) {
|
|
929
|
+
const recovered = terminateAskUserQuestion(
|
|
930
|
+
storeOptions,
|
|
931
|
+
lifecycleRecord.questionId,
|
|
932
|
+
lifecycleRecord.state === "dispatching" ? "restart_during_dispatch" : "restart_invalidated",
|
|
933
|
+
);
|
|
934
|
+
if (recovered) {
|
|
935
|
+
await updateLifecycleRecordCardBestEffort({
|
|
936
|
+
record: recovered,
|
|
937
|
+
config: params.config,
|
|
938
|
+
log: params.log,
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
return { handled: true };
|
|
942
|
+
}
|
|
670
943
|
return { handled: false };
|
|
671
944
|
}
|
|
672
945
|
|
|
@@ -684,15 +957,17 @@ export async function handleDingTalkAskUserCardCallback(params: {
|
|
|
684
957
|
return { handled: true };
|
|
685
958
|
}
|
|
686
959
|
|
|
687
|
-
if (ctx.submitted) {
|
|
960
|
+
if (ctx.submitted || lifecycleRecord?.state === "dispatching") {
|
|
688
961
|
params.log?.debug?.(`[DingTalk][AskUser] Duplicate submit ignored question=${ctx.questionId}`);
|
|
689
962
|
return { handled: true };
|
|
690
963
|
}
|
|
691
964
|
|
|
692
965
|
const isCancel = parseBooleanLike(parsed.params.user_cancel) === true;
|
|
693
|
-
ctx.submitted = true;
|
|
694
966
|
|
|
695
967
|
if (isCancel) {
|
|
968
|
+
if (!claimPendingQuestionForDispatch(ctx)) {
|
|
969
|
+
return { handled: true };
|
|
970
|
+
}
|
|
696
971
|
await updateQuestionCardBestEffort(ctx, {
|
|
697
972
|
card_status: "cancelled",
|
|
698
973
|
question_desc: "已取消。",
|
|
@@ -700,27 +975,28 @@ export async function handleDingTalkAskUserCardCallback(params: {
|
|
|
700
975
|
});
|
|
701
976
|
consumePendingQuestion(ctx);
|
|
702
977
|
addHandledQuestionTombstone(ctx, "cancelled");
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
},
|
|
710
|
-
);
|
|
978
|
+
dispatchSyntheticAnswer({
|
|
979
|
+
ctx,
|
|
980
|
+
text: buildCancelledAnswerMessage(ctx),
|
|
981
|
+
suffix: "cancelled",
|
|
982
|
+
successReason: "cancelled",
|
|
983
|
+
log: params.log,
|
|
711
984
|
});
|
|
712
985
|
return { handled: true };
|
|
713
986
|
}
|
|
714
987
|
|
|
715
988
|
const form = asRecord(parsed.params.form);
|
|
716
989
|
if (!form) {
|
|
717
|
-
ctx.submitted = false;
|
|
718
990
|
params.log?.warn?.(
|
|
719
991
|
`[DingTalk][AskUser] Missing form payload question=${ctx.questionId} params=${JSON.stringify(parsed.params)}`,
|
|
720
992
|
);
|
|
721
993
|
return { handled: true };
|
|
722
994
|
}
|
|
723
995
|
|
|
996
|
+
if (!claimPendingQuestionForDispatch(ctx)) {
|
|
997
|
+
return { handled: true };
|
|
998
|
+
}
|
|
999
|
+
|
|
724
1000
|
const answers: AnswerEntry[] = [];
|
|
725
1001
|
const selectedValues: string[] = [];
|
|
726
1002
|
for (const question of ctx.questions) {
|
|
@@ -745,12 +1021,12 @@ export async function handleDingTalkAskUserCardCallback(params: {
|
|
|
745
1021
|
});
|
|
746
1022
|
consumePendingQuestion(ctx);
|
|
747
1023
|
addHandledQuestionTombstone(ctx, "empty");
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
1024
|
+
dispatchSyntheticAnswer({
|
|
1025
|
+
ctx,
|
|
1026
|
+
text: buildEmptyAnswerMessage(ctx),
|
|
1027
|
+
suffix: "empty",
|
|
1028
|
+
successReason: "empty",
|
|
1029
|
+
log: params.log,
|
|
754
1030
|
});
|
|
755
1031
|
return { handled: true };
|
|
756
1032
|
}
|
|
@@ -767,10 +1043,12 @@ export async function handleDingTalkAskUserCardCallback(params: {
|
|
|
767
1043
|
addHandledQuestionTombstone(ctx, "submitted");
|
|
768
1044
|
|
|
769
1045
|
const message = buildAnswerMessage(ctx, answers);
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
1046
|
+
dispatchSyntheticAnswer({
|
|
1047
|
+
ctx,
|
|
1048
|
+
text: message,
|
|
1049
|
+
suffix: "submitted",
|
|
1050
|
+
successReason: "submitted",
|
|
1051
|
+
log: params.log,
|
|
774
1052
|
});
|
|
775
1053
|
return { handled: true };
|
|
776
1054
|
}
|
|
@@ -955,7 +1233,7 @@ export function registerDingTalkAskUserQuestionTool(api: OpenClawPluginApi): voi
|
|
|
955
1233
|
return;
|
|
956
1234
|
}
|
|
957
1235
|
|
|
958
|
-
|
|
1236
|
+
const createTool = (context: DingTalkQuestionContext | undefined) => ({
|
|
959
1237
|
name: TOOL_NAME,
|
|
960
1238
|
label: "Ask User Question",
|
|
961
1239
|
description:
|
|
@@ -969,7 +1247,6 @@ export function registerDingTalkAskUserQuestionTool(api: OpenClawPluginApi): voi
|
|
|
969
1247
|
"Do not call this tool for normal explanations, why/how questions, capability introductions, or cases where you can answer directly.",
|
|
970
1248
|
parameters: AskUserQuestionSchema as any,
|
|
971
1249
|
async execute(_toolCallId: string, params: unknown) {
|
|
972
|
-
const context = getDingTalkQuestionContext();
|
|
973
1250
|
if (!context) {
|
|
974
1251
|
return jsonToolResult({
|
|
975
1252
|
status: "failed",
|
|
@@ -1010,6 +1287,16 @@ export function registerDingTalkAskUserQuestionTool(api: OpenClawPluginApi): voi
|
|
|
1010
1287
|
selected_values: "[]",
|
|
1011
1288
|
form: { fields },
|
|
1012
1289
|
};
|
|
1290
|
+
const storeOptions = getAskUserStoreOptions(context);
|
|
1291
|
+
const canPersistLifecycle = Boolean(storeOptions && context.questionScopeKey);
|
|
1292
|
+
if (storeOptions && context.questionScopeKey) {
|
|
1293
|
+
reserveAskUserQuestion(storeOptions, {
|
|
1294
|
+
questionId,
|
|
1295
|
+
questionScopeKey: context.questionScopeKey,
|
|
1296
|
+
outTrackId,
|
|
1297
|
+
title,
|
|
1298
|
+
});
|
|
1299
|
+
}
|
|
1013
1300
|
|
|
1014
1301
|
try {
|
|
1015
1302
|
await createAndDeliverQuestionCard({
|
|
@@ -1025,6 +1312,9 @@ export function registerDingTalkAskUserQuestionTool(api: OpenClawPluginApi): voi
|
|
|
1025
1312
|
log: context.log,
|
|
1026
1313
|
});
|
|
1027
1314
|
} catch (err) {
|
|
1315
|
+
if (storeOptions && canPersistLifecycle) {
|
|
1316
|
+
terminateAskUserQuestion(storeOptions, questionId, "delivery_failed");
|
|
1317
|
+
}
|
|
1028
1318
|
const detail = formatDingTalkErrorPayloadLog("ask_user_create", err, "[DingTalk]");
|
|
1029
1319
|
return jsonToolResult({
|
|
1030
1320
|
status: "failed",
|
|
@@ -1035,21 +1325,76 @@ export function registerDingTalkAskUserQuestionTool(api: OpenClawPluginApi): voi
|
|
|
1035
1325
|
...context,
|
|
1036
1326
|
onQuestionCardSent: undefined,
|
|
1037
1327
|
};
|
|
1038
|
-
|
|
1328
|
+
const pendingQuestion: PendingQuestion = {
|
|
1039
1329
|
...pendingContext,
|
|
1040
1330
|
questionId,
|
|
1041
1331
|
outTrackId,
|
|
1042
1332
|
title,
|
|
1043
1333
|
questions: parsed,
|
|
1044
1334
|
submitted: false,
|
|
1335
|
+
};
|
|
1336
|
+
storePendingQuestion(pendingQuestion, {
|
|
1337
|
+
supersedeExisting: !canPersistLifecycle,
|
|
1045
1338
|
});
|
|
1339
|
+
if (storeOptions && canPersistLifecycle) {
|
|
1340
|
+
const activation = activateAskUserQuestion(storeOptions, questionId);
|
|
1341
|
+
if (activation.record?.state !== "pending") {
|
|
1342
|
+
const terminalReason = activation.record?.terminalReason ?? "superseded_by_message";
|
|
1343
|
+
if (activation.record) {
|
|
1344
|
+
consumeLifecyclePendingContext(activation.record);
|
|
1345
|
+
} else {
|
|
1346
|
+
pendingQuestion.submitted = true;
|
|
1347
|
+
consumePendingQuestion(pendingQuestion);
|
|
1348
|
+
addHandledQuestionTombstone(pendingQuestion, "superseded");
|
|
1349
|
+
}
|
|
1350
|
+
await updateQuestionCardBestEffort(
|
|
1351
|
+
pendingQuestion,
|
|
1352
|
+
terminalCardVariables(terminalReason),
|
|
1353
|
+
);
|
|
1354
|
+
return jsonToolResult({
|
|
1355
|
+
status: "failed",
|
|
1356
|
+
questionId,
|
|
1357
|
+
outTrackId,
|
|
1358
|
+
error: "问题卡片在发送期间已失效,请重新发起。",
|
|
1359
|
+
});
|
|
1360
|
+
}
|
|
1361
|
+
for (const superseded of activation.superseded) {
|
|
1362
|
+
const supersededContext = consumeLifecyclePendingContext(superseded);
|
|
1363
|
+
if (supersededContext) {
|
|
1364
|
+
void updateQuestionCardBestEffort(
|
|
1365
|
+
supersededContext,
|
|
1366
|
+
terminalCardVariables("superseded_by_question"),
|
|
1367
|
+
);
|
|
1368
|
+
} else {
|
|
1369
|
+
void updateLifecycleRecordCardBestEffort({
|
|
1370
|
+
record: superseded,
|
|
1371
|
+
config: context.dingtalkConfig,
|
|
1372
|
+
log: context.log,
|
|
1373
|
+
});
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1046
1377
|
|
|
1378
|
+
let takeoverSucceeded: boolean | void = undefined;
|
|
1047
1379
|
try {
|
|
1048
|
-
await context.onQuestionCardSent?.({ questionId, outTrackId });
|
|
1380
|
+
takeoverSucceeded = await context.onQuestionCardSent?.({ questionId, outTrackId });
|
|
1049
1381
|
} catch (err) {
|
|
1050
1382
|
context.log?.warn?.(
|
|
1051
1383
|
`[DingTalk][AskUser] onQuestionCardSent hook failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
1052
1384
|
);
|
|
1385
|
+
takeoverSucceeded = false;
|
|
1386
|
+
}
|
|
1387
|
+
if (takeoverSucceeded === false) {
|
|
1388
|
+
await terminatePendingQuestion({
|
|
1389
|
+
ctx: pendingQuestion,
|
|
1390
|
+
reason: "pause_failed",
|
|
1391
|
+
});
|
|
1392
|
+
return jsonToolResult({
|
|
1393
|
+
status: "failed",
|
|
1394
|
+
questionId,
|
|
1395
|
+
outTrackId,
|
|
1396
|
+
error: "当前任务未能暂停,此卡已失效,请重新发起。",
|
|
1397
|
+
});
|
|
1053
1398
|
}
|
|
1054
1399
|
|
|
1055
1400
|
context.log?.info?.(
|
|
@@ -1064,5 +1409,21 @@ export function registerDingTalkAskUserQuestionTool(api: OpenClawPluginApi): voi
|
|
|
1064
1409
|
});
|
|
1065
1410
|
},
|
|
1066
1411
|
});
|
|
1412
|
+
registerTool.call(
|
|
1413
|
+
api,
|
|
1414
|
+
(toolContext: OpenClawPluginToolContext) => {
|
|
1415
|
+
// Capture the inbound run's context while the factory is still inside its
|
|
1416
|
+
// AsyncLocalStorage scope; shared agent clients may execute the tool later.
|
|
1417
|
+
const context = getDingTalkQuestionContext();
|
|
1418
|
+
const runtimeSessionKey = toolContext.sessionKey?.trim();
|
|
1419
|
+
const contextSessionKey = context?.resolvedRoute?.sessionKey.trim();
|
|
1420
|
+
return createTool(
|
|
1421
|
+
context && (!runtimeSessionKey || contextSessionKey === runtimeSessionKey)
|
|
1422
|
+
? context
|
|
1423
|
+
: undefined,
|
|
1424
|
+
);
|
|
1425
|
+
},
|
|
1426
|
+
{ name: TOOL_NAME },
|
|
1427
|
+
);
|
|
1067
1428
|
api.logger?.debug?.(`${TOOL_NAME}: registered tool`);
|
|
1068
1429
|
}
|
|
@@ -14,6 +14,7 @@ export async function handleCardAction(params: {
|
|
|
14
14
|
analysis: CardCallbackAnalysis;
|
|
15
15
|
cfg: OpenClawConfig;
|
|
16
16
|
accountId: string;
|
|
17
|
+
storePath?: string;
|
|
17
18
|
config: DingTalkConfig;
|
|
18
19
|
log?: Logger;
|
|
19
20
|
}): Promise<CardActionResult> {
|
|
@@ -21,6 +22,7 @@ export async function handleCardAction(params: {
|
|
|
21
22
|
payload: params.payload,
|
|
22
23
|
cfg: params.cfg,
|
|
23
24
|
accountId: params.accountId,
|
|
25
|
+
storePath: params.storePath,
|
|
24
26
|
config: params.config,
|
|
25
27
|
clickerUserId: params.analysis.userId,
|
|
26
28
|
log: params.log,
|