fix(rectification): resolve typed focus answers deterministically
Independent Staging Quality Gate / validate (push) Successful in 19m52s
Independent Staging Quality Gate / publish (push) Successful in 53m47s

This commit is contained in:
Jesse_Chen
2026-08-27 20:31:05 +08:00
parent f3327235ea
commit 85db4591d3
24 changed files with 695 additions and 168 deletions
@@ -2,6 +2,8 @@ import { NextResponse } from "next/server";
import { z } from "zod";
import { getRectificationV9Agent, type RectificationAgentAction } from "@/mastra/agentic-rectification";
import {
loadV9CaseDossier,
persistV9DeterministicTurn,
RectificationToolServiceError,
transitionV9CaseStatus,
} from "@/lib/rectification-agentic/v9/tool-service";
@@ -19,10 +21,33 @@ import { resolveSessionLanguageModel } from "@/lib/model-catalog";
import { createAdminSupabaseClient } from "@/lib/supabase/admin";
import { createServerSupabaseClient } from "@/lib/supabase/server";
import { defaultMessageOrigin, isRectificationMessageOrigin } from "@/lib/rectification-agentic/v9/message-origin";
import { previousInferenceFromReceipt } from "@/lib/rectification-agentic/v9/inference-adapter";
import { parseAgentChoiceCopy } from "@/lib/rectification-agentic/v9/choice-card";
import {
classifyRectificationTurnIntent,
optionIdForAnswerClass,
} from "@/lib/rectification-agentic/v9/turn-intent-classifier";
export const runtime = "nodejs";
export const maxDuration = 240;
function completedMessageResponse(text: string, requestId: string, caseId: string) {
const body = [
JSON.stringify({ type: "answer.delta", text }),
JSON.stringify({ type: "run.completed" }),
"",
].join("\n");
return new Response(body, {
headers: {
"cache-control": "no-cache, no-transform",
"content-type": "application/x-ndjson; charset=utf-8",
"x-accel-buffering": "no",
"x-ayanam-request-id": requestId,
"x-rectification-case-id": caseId,
},
});
}
const agentRequestSchema = z.object({
caseId: z.string().uuid(),
sessionId: z.string().uuid(),
@@ -279,6 +304,98 @@ export async function POST(request: Request) {
);
}
if (action === "message") {
try {
const dossier = await loadV9CaseDossier(accounting, userId, caseId);
const focus = dossier.conversationSummary.activeFocus;
if (focus) {
const choice = parseAgentChoiceCopy(focus.expectedAnswerSchema);
if (!choice) {
return completedMessageResponse("当前问题已更新,请刷新后重新作答。", requestId, caseId);
}
let classified = null;
try {
classified = await classifyRectificationTurnIntent(selectedModel, {
focus,
userMessage: parsed.data.message ?? "",
caseStatus,
signal: request.signal,
});
} catch {
classified = null;
}
if (!classified || classified.intent === "unclear") {
const narration = "我没能确定这句话是否在回答当前问题。请点选下面的选项,或换一种说法。";
await persistV9DeterministicTurn(accounting, userId, caseId, {
requestId,
userMessage: parsed.data.message ?? null,
assistantMessage: narration,
});
return completedMessageResponse(narration, requestId, caseId);
}
if (classified.intent === "answer_current_focus") {
if (!classified.answer_class) {
return completedMessageResponse("当前问题已更新,请刷新后重新作答。", requestId, caseId);
}
const optionId = optionIdForAnswerClass(focus, classified.answer_class);
if (!optionId) {
return completedMessageResponse("当前问题已更新,请刷新后重新作答。", requestId, caseId);
}
const previous = previousInferenceFromReceipt(dossier.latestResult?.decisionReceipt ?? null);
const applied = await applyRectificationChoice(accounting, {
userId,
caseId,
sessionId,
actionId: requestId,
action: CHOICE_ACTION,
focusId: focus.id,
questionId: focus.questionId,
probeId: typeof focus.expectedAnswerSchema.probe_id === "string"
? focus.expectedAnswerSchema.probe_id
: null,
optionId,
expectedRevision: previous?.revision ?? 0,
userDisplay: parsed.data.message ?? null,
});
return completedMessageResponse(applied.narration, requestId, caseId);
}
if (classified.intent === "stop_rectification") {
const previous = previousInferenceFromReceipt(dossier.latestResult?.decisionReceipt ?? null);
const applied = await applyRectificationChoice(accounting, {
userId,
caseId,
sessionId,
actionId: requestId,
action: STOP_ACTION,
focusId: focus.id,
questionId: focus.questionId,
probeId: typeof focus.expectedAnswerSchema.probe_id === "string"
? focus.expectedAnswerSchema.probe_id
: null,
optionId: "stop",
expectedRevision: previous?.revision ?? 0,
userDisplay: parsed.data.message ?? null,
});
await transitionV9CaseStatus(accounting, userId, caseId, "paused");
return completedMessageResponse(applied.narration, requestId, caseId);
}
}
} catch (error) {
if (error instanceof RectificationToolServiceError) {
const mapped = mapRectificationRpcError(error);
return NextResponse.json(
{ error: mapped.message, message: mapped.message, code: mapped.code },
{ status: mapped.status },
);
}
console.error(`[rectification-v9] focus answer failed case=${caseId} reason=${error instanceof Error ? error.name : "Unknown"}`);
return NextResponse.json(
{ error: "校正服务暂时不可用", message: "请稍后重试。", code: "rectification_service_failed" },
{ status: 500 },
);
}
}
const requestTime = new Date();
const chinaTime = new Date(requestTime.getTime() + 8 * 60 * 60 * 1000)
.toISOString()