@soimy/dingtalk 3.6.6 → 3.6.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +678 -108
- package/dist/index.js.map +4 -4
- package/dist/src/card/ask-user-question-context.d.ts +5 -2
- package/dist/src/card/ask-user-question-context.d.ts.map +1 -1
- package/dist/src/card/ask-user-question-store.d.ts +42 -0
- package/dist/src/card/ask-user-question-store.d.ts.map +1 -0
- package/dist/src/card/ask-user-question.d.ts +20 -0
- package/dist/src/card/ask-user-question.d.ts.map +1 -1
- package/dist/src/card/card-action-handler.d.ts +1 -0
- package/dist/src/card/card-action-handler.d.ts.map +1 -1
- package/dist/src/gateway/channel-gateway.d.ts.map +1 -1
- package/dist/src/inbound-handler.d.ts.map +1 -1
- package/dist/src/targeting/agent-routing.d.ts +8 -1
- package/dist/src/targeting/agent-routing.d.ts.map +1 -1
- package/dist/src/types.d.ts +13 -0
- package/dist/src/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/access-control.ts +1 -1
- package/src/card/ask-user-question-context.ts +9 -1
- package/src/card/ask-user-question-store.ts +294 -0
- package/src/card/ask-user-question.ts +380 -34
- package/src/card/card-action-handler.ts +2 -0
- package/src/gateway/channel-gateway.ts +19 -0
- package/src/inbound-handler.ts +110 -37
- package/src/targeting/agent-routing.ts +67 -19
- package/src/types.ts +14 -0
|
@@ -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
|
}
|
|
@@ -1010,6 +1288,16 @@ export function registerDingTalkAskUserQuestionTool(api: OpenClawPluginApi): voi
|
|
|
1010
1288
|
selected_values: "[]",
|
|
1011
1289
|
form: { fields },
|
|
1012
1290
|
};
|
|
1291
|
+
const storeOptions = getAskUserStoreOptions(context);
|
|
1292
|
+
const canPersistLifecycle = Boolean(storeOptions && context.questionScopeKey);
|
|
1293
|
+
if (storeOptions && context.questionScopeKey) {
|
|
1294
|
+
reserveAskUserQuestion(storeOptions, {
|
|
1295
|
+
questionId,
|
|
1296
|
+
questionScopeKey: context.questionScopeKey,
|
|
1297
|
+
outTrackId,
|
|
1298
|
+
title,
|
|
1299
|
+
});
|
|
1300
|
+
}
|
|
1013
1301
|
|
|
1014
1302
|
try {
|
|
1015
1303
|
await createAndDeliverQuestionCard({
|
|
@@ -1025,6 +1313,9 @@ export function registerDingTalkAskUserQuestionTool(api: OpenClawPluginApi): voi
|
|
|
1025
1313
|
log: context.log,
|
|
1026
1314
|
});
|
|
1027
1315
|
} catch (err) {
|
|
1316
|
+
if (storeOptions && canPersistLifecycle) {
|
|
1317
|
+
terminateAskUserQuestion(storeOptions, questionId, "delivery_failed");
|
|
1318
|
+
}
|
|
1028
1319
|
const detail = formatDingTalkErrorPayloadLog("ask_user_create", err, "[DingTalk]");
|
|
1029
1320
|
return jsonToolResult({
|
|
1030
1321
|
status: "failed",
|
|
@@ -1035,21 +1326,76 @@ export function registerDingTalkAskUserQuestionTool(api: OpenClawPluginApi): voi
|
|
|
1035
1326
|
...context,
|
|
1036
1327
|
onQuestionCardSent: undefined,
|
|
1037
1328
|
};
|
|
1038
|
-
|
|
1329
|
+
const pendingQuestion: PendingQuestion = {
|
|
1039
1330
|
...pendingContext,
|
|
1040
1331
|
questionId,
|
|
1041
1332
|
outTrackId,
|
|
1042
1333
|
title,
|
|
1043
1334
|
questions: parsed,
|
|
1044
1335
|
submitted: false,
|
|
1336
|
+
};
|
|
1337
|
+
storePendingQuestion(pendingQuestion, {
|
|
1338
|
+
supersedeExisting: !canPersistLifecycle,
|
|
1045
1339
|
});
|
|
1340
|
+
if (storeOptions && canPersistLifecycle) {
|
|
1341
|
+
const activation = activateAskUserQuestion(storeOptions, questionId);
|
|
1342
|
+
if (activation.record?.state !== "pending") {
|
|
1343
|
+
const terminalReason = activation.record?.terminalReason ?? "superseded_by_message";
|
|
1344
|
+
if (activation.record) {
|
|
1345
|
+
consumeLifecyclePendingContext(activation.record);
|
|
1346
|
+
} else {
|
|
1347
|
+
pendingQuestion.submitted = true;
|
|
1348
|
+
consumePendingQuestion(pendingQuestion);
|
|
1349
|
+
addHandledQuestionTombstone(pendingQuestion, "superseded");
|
|
1350
|
+
}
|
|
1351
|
+
await updateQuestionCardBestEffort(
|
|
1352
|
+
pendingQuestion,
|
|
1353
|
+
terminalCardVariables(terminalReason),
|
|
1354
|
+
);
|
|
1355
|
+
return jsonToolResult({
|
|
1356
|
+
status: "failed",
|
|
1357
|
+
questionId,
|
|
1358
|
+
outTrackId,
|
|
1359
|
+
error: "问题卡片在发送期间已失效,请重新发起。",
|
|
1360
|
+
});
|
|
1361
|
+
}
|
|
1362
|
+
for (const superseded of activation.superseded) {
|
|
1363
|
+
const supersededContext = consumeLifecyclePendingContext(superseded);
|
|
1364
|
+
if (supersededContext) {
|
|
1365
|
+
void updateQuestionCardBestEffort(
|
|
1366
|
+
supersededContext,
|
|
1367
|
+
terminalCardVariables("superseded_by_question"),
|
|
1368
|
+
);
|
|
1369
|
+
} else {
|
|
1370
|
+
void updateLifecycleRecordCardBestEffort({
|
|
1371
|
+
record: superseded,
|
|
1372
|
+
config: context.dingtalkConfig,
|
|
1373
|
+
log: context.log,
|
|
1374
|
+
});
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1046
1378
|
|
|
1379
|
+
let takeoverSucceeded: boolean | void = undefined;
|
|
1047
1380
|
try {
|
|
1048
|
-
await context.onQuestionCardSent?.({ questionId, outTrackId });
|
|
1381
|
+
takeoverSucceeded = await context.onQuestionCardSent?.({ questionId, outTrackId });
|
|
1049
1382
|
} catch (err) {
|
|
1050
1383
|
context.log?.warn?.(
|
|
1051
1384
|
`[DingTalk][AskUser] onQuestionCardSent hook failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
1052
1385
|
);
|
|
1386
|
+
takeoverSucceeded = false;
|
|
1387
|
+
}
|
|
1388
|
+
if (takeoverSucceeded === false) {
|
|
1389
|
+
await terminatePendingQuestion({
|
|
1390
|
+
ctx: pendingQuestion,
|
|
1391
|
+
reason: "pause_failed",
|
|
1392
|
+
});
|
|
1393
|
+
return jsonToolResult({
|
|
1394
|
+
status: "failed",
|
|
1395
|
+
questionId,
|
|
1396
|
+
outTrackId,
|
|
1397
|
+
error: "当前任务未能暂停,此卡已失效,请重新发起。",
|
|
1398
|
+
});
|
|
1053
1399
|
}
|
|
1054
1400
|
|
|
1055
1401
|
context.log?.info?.(
|
|
@@ -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,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { DWClient, TOPIC_CARD, TOPIC_ROBOT } from "dingtalk-stream";
|
|
2
2
|
import { analyzeCardCallback } from "../card-callback-service";
|
|
3
3
|
import { finalizeActiveCardsForAccount, recoverPendingCardsForAccount } from "../card-service";
|
|
4
|
+
import { recoverAskUserQuestionsForAccount } from "../card/ask-user-question";
|
|
4
5
|
import { handleCardAction } from "../card/card-action-handler";
|
|
5
6
|
import { resolveRobotCode, resolveRuntimeConfig } from "../config";
|
|
6
7
|
import { ConnectionManager } from "../connection-manager";
|
|
@@ -199,6 +200,23 @@ export function createDingTalkGateway(): NonNullable<DingTalkChannelPlugin["gate
|
|
|
199
200
|
`[${account.accountId}] Failed to recover unfinished cards: ${err.message}`,
|
|
200
201
|
);
|
|
201
202
|
}
|
|
203
|
+
try {
|
|
204
|
+
const recoveredQuestions = await recoverAskUserQuestionsForAccount({
|
|
205
|
+
storePath: accountStorePath,
|
|
206
|
+
accountId: account.accountId,
|
|
207
|
+
config,
|
|
208
|
+
log: pluginLog,
|
|
209
|
+
});
|
|
210
|
+
if (recoveredQuestions > 0) {
|
|
211
|
+
pluginLog?.info?.(
|
|
212
|
+
`[${account.accountId}] Invalidated ${recoveredQuestions} unfinished Ask User card(s) from previous runtime`,
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
} catch (err: any) {
|
|
216
|
+
pluginLog?.warn?.(
|
|
217
|
+
`[${account.accountId}] Failed to recover Ask User cards: ${err.message}`,
|
|
218
|
+
);
|
|
219
|
+
}
|
|
202
220
|
|
|
203
221
|
const useConnectionManager = config.useConnectionManager ?? true;
|
|
204
222
|
const applyStatusPatch = (patch: Record<string, unknown>) => {
|
|
@@ -382,6 +400,7 @@ export function createDingTalkGateway(): NonNullable<DingTalkChannelPlugin["gate
|
|
|
382
400
|
analysis,
|
|
383
401
|
cfg,
|
|
384
402
|
accountId: account.accountId,
|
|
403
|
+
storePath: accountStorePath,
|
|
385
404
|
config,
|
|
386
405
|
log: pluginLog,
|
|
387
406
|
});
|