fix(rectification): 分类器失败、引擎忙、整轮超时不再归因错
Independent Staging Quality Gate / validate (push) Canceled after 4m2s
Independent Staging Quality Gate / publish (push) Canceled after 0s

点选题把分类器两次异常说成用户没说清(BUG-722);引擎 429 被压成坏了且重算静默失败(BUG-723);两次 attempt 总预算大于路由 maxDuration(BUG-724)。本单只改归因:classifier_unavailable 请用户重发、busy 分档并可见「这次没有重新比较」、整轮 225s 预算不够则 host fallback。不改计费、分类模型、并发闸门。
This commit is contained in:
jesse-ux
2026-09-16 07:06:57 +08:00
parent 11893c7f97
commit 8b982baf64
23 changed files with 952 additions and 104 deletions
@@ -35,7 +35,6 @@ import { defaultMessageOrigin, isRectificationMessageOrigin } from "@/lib/rectif
import { previousInferenceFromReceipt } from "@/lib/rectification-agentic/v9/inference-adapter";
import { CHOICE_STOP_LABEL, parseAgentChoiceCopy } from "@/lib/rectification-agentic/v9/choice-card";
import {
classifyRectificationTurnIntent,
optionIdForAnswerClass,
collectFocusCloseStatus,
shouldContinueAgentForDatedEvent,
@@ -459,7 +458,16 @@ export async function POST(request: Request) {
const classified = intent.classified;
expectedWrite = intent.expectedWrite;
writeClassified = true;
if (!classified || classified.intent === "unclear") {
if (intent.outcome === "classifier_unavailable") {
const narration = RECTIFICATION_USER_COPY.classifierUnavailableReply;
const turn = await persistV9DeterministicTurn(accounting, userId, caseId, {
requestId,
userMessage: parsed.data.message ?? null,
assistantMessage: narration,
});
return completedMessageResponse(narration, requestId, caseId, turn.turnId);
}
if (intent.outcome === "unclear") {
const narration = RECTIFICATION_USER_COPY.unclearFocusReply;
const turn = await persistV9DeterministicTurn(accounting, userId, caseId, {
requestId,
@@ -468,7 +476,7 @@ export async function POST(request: Request) {
});
return completedMessageResponse(narration, requestId, caseId, turn.turnId);
}
if (classified.intent === "answer_current_focus") {
if (classified?.intent === "answer_current_focus") {
if (!classified.answer_class) {
return completedMessageResponse(RECTIFICATION_USER_COPY.questionUpdated, requestId, caseId);
}
@@ -499,7 +507,7 @@ export async function POST(request: Request) {
return completedMessageResponse(applied.narration, requestId, caseId, applied.turnId);
}
}
if (classified.intent === "stop_rectification" || classified.intent === "ask_about_result") {
if (classified?.intent === "stop_rectification" || classified?.intent === "ask_about_result") {
const previous = previousInferenceFromReceipt(dossier.latestResult?.decisionReceipt ?? null);
const applied = await applyRectificationChoice(accounting, {
userId,
@@ -631,16 +639,21 @@ export async function POST(request: Request) {
});
const open = openQuestionFromPersistedFocus(persisted);
if (open && persisted.focus && isRenderableChoiceOpenQuestion(open)) {
let classified = null;
try {
classified = await classifyRectificationTurnIntent(selectedModel, {
focus: persisted.focus,
userMessage: parsed.data.message ?? "",
caseStatus,
signal: request.signal,
const intent = await classifyTurnIntentWithRetry(resolvedModel, {
focus: persisted.focus,
userMessage: parsed.data.message ?? "",
caseStatus,
signal: request.signal,
});
const classified = intent.classified;
if (intent.outcome === "classifier_unavailable") {
const narration = RECTIFICATION_USER_COPY.classifierUnavailableReply;
const turn = await persistV9DeterministicTurn(accounting, userId, caseId, {
requestId,
userMessage: parsed.data.message ?? null,
assistantMessage: narration,
});
} catch {
classified = null;
return completedMessageResponse(narration, requestId, caseId, turn.turnId);
}
if (classified?.intent === "answer_current_focus" && classified.answer_class) {
const optionId = optionIdForAnswerClass(persisted.focus, classified.answer_class);