fix(rectification): keep one delivery and adopt with the birth date

Accept and GET now decide with the same birth snapshot as the answer, so an under-age probe cannot flip can_adopt. The exit skips a second write when this reply already delivered. The card stays without an adopt button when adoption is not allowed, and a refusal shows the Chinese sentence.
This commit is contained in:
jesse-ux
2026-10-06 16:03:57 +08:00
committed by Jesse_Chen
parent da226db1d4
commit 6e16dc5e16
34 changed files with 1100 additions and 182 deletions
@@ -111,6 +111,9 @@ export async function POST(request: Request, context: RouteContext) {
const { loadV9CaseDossier, evidenceLedgerFingerprint } = await import(
"@/lib/rectification-agentic/v9/tool-service"
);
const { loadCaseDecisionInputs } = await import(
"@/lib/rectification-agentic/v9/decision-inputs"
);
const { decideFromDossier, overlayPublicDecision } = await import(
"@/lib/rectification-agentic/v9/interview-state"
);
@@ -120,16 +123,20 @@ export async function POST(request: Request, context: RouteContext) {
const { assertV9ResultWritable } = await import("@/lib/rectification-agentic/v9/result-identity");
await assertV9ResultWritable(accounting, user.id, caseId, parsed.data.resultId);
const dossier = await loadV9CaseDossier(accounting, user.id, caseId);
const decision = decideFromDossier(dossier, {
const decision = decideFromDossier(dossier, await loadCaseDecisionInputs(accounting, user.id, caseId, {
currentEvidenceFingerprint: evidenceLedgerFingerprint(dossier.evidence),
});
}));
const fields = publicDecisionFields(decision);
const overlaid = dossier.latestResult
? overlayPublicDecision(dossier.latestResult, decision)
: fields;
if (overlaid.can_adopt !== true) {
return NextResponse.json(
{ error: "adoption_not_allowed", session_outcome: fields.session_outcome },
{
error: "这次的结果还不能采用,已刷新到最新状态",
code: "adoption_not_allowed",
session_outcome: fields.session_outcome,
},
{ status: 409 },
);
}
@@ -207,6 +214,12 @@ export async function POST(request: Request, context: RouteContext) {
...(adoptTurnId ? { adopt_turn_id: adoptTurnId } : {}),
});
} catch (error) {
if (error instanceof RectificationToolServiceError && error.code === "birth_snapshot_unavailable") {
return NextResponse.json(
{ error: "暂时无法采用", code: "birth_snapshot_unavailable" },
{ status: 503 },
);
}
if (error instanceof RectificationToolServiceError) {
return errorResponse(error);
}
@@ -13,6 +13,7 @@ import {
} from "@/lib/rectification-agentic/v9/tool-service";
import { ensureNonTerminalTurnExit } from "@/lib/rectification-agentic/v9/answer-choice";
import { dossierResponseWithIdentity } from "@/lib/rectification-agentic/v9/case-dossier-response";
import { loadCaseDecisionInputs } from "@/lib/rectification-agentic/v9/decision-inputs";
export const runtime = "nodejs";
@@ -75,9 +76,13 @@ export async function POST(request: Request, context: RouteContext) {
}),
);
const listed = await listV10ConversationFocuses(accounting, user.id, caseId);
const decisionInputs = await loadCaseDecisionInputs(accounting, user.id, caseId);
return NextResponse.json({
ok: true,
...await dossierResponseWithIdentity(dossier, receipts, skillIdentity, { listed }),
...await dossierResponseWithIdentity(dossier, receipts, skillIdentity, {
listed,
birthDate: decisionInputs.birthDate,
}),
});
} catch (error) {
if (error instanceof RectificationToolServiceError) {
@@ -14,6 +14,7 @@ import {
safeToolErrorCode,
} from "@/lib/rectification-agentic/v9/tool-service";
import { dossierResponseWithIdentity } from "@/lib/rectification-agentic/v9/case-dossier-response";
import { loadCaseDecisionInputs } from "@/lib/rectification-agentic/v9/decision-inputs";
import { scheduleRectificationTelemetry } from "@/lib/rectification-agentic/v9/telemetry";
export const runtime = "nodejs";
@@ -97,9 +98,11 @@ export async function GET(request: Request, context: RouteContext) {
}),
);
const listed = await listV10ConversationFocuses(accounting, user.id, caseId);
const decisionInputs = await loadCaseDecisionInputs(accounting, user.id, caseId);
return NextResponse.json(await dossierResponseWithIdentity(dossier, receipts, skillIdentity, {
fullDetail,
listed,
birthDate: decisionInputs.birthDate,
// Anonymous aggregate telemetry: fire-and-forget, never awaited here.
onProjected: ({ decision, rangeDelivery, readOnly }) => {
void scheduleRectificationTelemetry({