feat(rectification): start from an offer card in the consultation, come back to it (BUG-1247~1251)
TASK-rectification-in-chat-step1-20261006, direct execution.
- The homepage 生时校正 pill and the text trigger (isRectificationHandoffQuestion)
are gone. The consult agent decides (RECTIFICATION_OFFER_RULE, one
definition) and calls offer-birth-time-rectification; the server decides
whether a card may show (self, product on, no case in progress,
feature_pricing price). The offer is stored with the answer and travels on
run.completed; the card hangs under that answer.
- Opening from the card binds the offer to the case in the consultation's own
messages (no table change); GET/POST /api/rectification/cases/[caseId]/source
find the source and go back: one divider (time changed or not), earlier
answers captioned 按校正前的时间, and a 按新时间重新看 button that sends only
when clicked. Auto-return after the post-adopt check or a terminal case, only
for work done in this view.
- Model history skips the divider and notes answers written with the old time.
Full suite 4987 / fail 24, identical to 6ec8efd5 (4973 / 24), no test name
lost; tsc 0; lint 0 errors; build keeps / Static, first-load gzip +0.20%.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
328f919d5d
commit
6ad0bea320
@@ -14,6 +14,8 @@ import { inputBlockedCopy, moderate, MODERATION_OUTPUT_REPLACED_COPY } from "@/l
|
||||
import { recordModerationEvent } from "@/lib/moderation/log";
|
||||
import { consultationDomainSchema } from "@/lib/consultation-domain-registry";
|
||||
import { parseAgentReply } from "@/lib/agent-reply";
|
||||
import { revisitQuestionForOffer, sessionHasRectificationOffer } from "@/lib/rectification-in-chat";
|
||||
import { decideRectificationOffer } from "@/lib/rectification-in-chat-server";
|
||||
import { createConsultationReplyMetadata } from "@/lib/consultation-reply-metadata";
|
||||
import {
|
||||
ConsultationPlanValidationError,
|
||||
@@ -55,6 +57,7 @@ import {
|
||||
natalUserTurnShape,
|
||||
natalConsultationThinkingPlan,
|
||||
type PublicThinkingSection,
|
||||
RECTIFICATION_OFFER_RULE,
|
||||
} from "@/lib/consultation-thinking-plan";
|
||||
import {
|
||||
AGENT_MAX_STEPS,
|
||||
@@ -66,6 +69,7 @@ import {
|
||||
consultationNatalPrepareStep,
|
||||
consultationWindowPrepareStep,
|
||||
createConsultationAgentContext,
|
||||
type RectificationOfferHook,
|
||||
createWindowConsultationAgentContext,
|
||||
precomputeWindowConsultation,
|
||||
windowPrecomputedPacketMessage,
|
||||
@@ -240,7 +244,7 @@ function generalNoMinuteInstruction(hasPublicDaily: boolean) {
|
||||
}
|
||||
|
||||
function declaredWindowInstruction() {
|
||||
return "当前是声明出生窗口咨询,没有单一出生分钟。如需个人结构结论,必须调用 run-jyotish-window-consultation。探针时刻不是出生时间,不得把 00:00、时段中点、中午或任一探针写成出生分钟。只能根据稳定层作方向性回答;上升或宫位若在窗口内变化,列出可能星座,不得写成「你的上升是 X」。精确应期一律不可用。这只是粗看。";
|
||||
return "当前是声明出生窗口咨询,没有单一出生分钟。如需个人结构结论,必须调用 run-jyotish-window-consultation。探针时刻不是出生时间,不得把 00:00、时段中点、中午或任一探针写成出生分钟。只能根据稳定层作方向性回答;上升或宫位若在窗口内变化,列出可能星座,不得写成「你的上升是 X」。精确应期一律不可用。这只是粗看。" + RECTIFICATION_OFFER_RULE;
|
||||
}
|
||||
|
||||
function usesPublicDailyGeneralAgent(
|
||||
@@ -867,6 +871,8 @@ export async function POST(request: Request) {
|
||||
techniqueTruth,
|
||||
workflowReceipt,
|
||||
...(agentExecutionReceipt ? { agentExecutionReceipt } : {}),
|
||||
// BUG-1248: the offer card hangs under this answer, also after a reload.
|
||||
...(latencyState?.rectificationOffer ? { rectificationOffer: latencyState.rectificationOffer } : {}),
|
||||
};
|
||||
const actualUsage = await usagePayload(usage);
|
||||
const completion = await retryDetachedSettlement(async () => {
|
||||
@@ -950,6 +956,17 @@ export async function POST(request: Request) {
|
||||
// evidence of having done so.
|
||||
const state = createConsultationRuntimeState({ plannedSteps: AGENT_MAX_STEPS });
|
||||
latencyState = state;
|
||||
// C2/C3 (BUG-1248): the offer tool asks the server, never decides itself.
|
||||
const rectificationOffer: RectificationOfferHook = {
|
||||
decide: () => decideRectificationOffer({
|
||||
accounting,
|
||||
userId,
|
||||
subjectRole: prepared.subject?.role === "other" ? "other" : "self",
|
||||
modelId: selectedModel.id,
|
||||
}),
|
||||
alreadyOffered: sessionHasRectificationOffer(chatSession?.messages),
|
||||
revisitQuestion: revisitQuestionForOffer(chatSession?.messages, visibleQuestion, requestId),
|
||||
};
|
||||
const hooks = createConsultationRuntimeHooks(state);
|
||||
const usages: Promise<Usage>[] = [];
|
||||
const agentStartedAt = Date.now();
|
||||
@@ -1272,6 +1289,7 @@ export async function POST(request: Request) {
|
||||
declaredWindow: prepared.declaredWindow,
|
||||
abortSignal: agentAbortSignal,
|
||||
state,
|
||||
rectificationOffer,
|
||||
});
|
||||
const agent = getWindowJyotishAgent(selectedModel, agentContext);
|
||||
const retry = async () => {
|
||||
@@ -1409,6 +1427,7 @@ export async function POST(request: Request) {
|
||||
serverChart: prepared.serverChart,
|
||||
abortSignal: agentAbortSignal,
|
||||
state,
|
||||
rectificationOffer,
|
||||
});
|
||||
const agent = getJyotishAgent(selectedModel, agentContext);
|
||||
const result = await streamWithOverflowRetry(agent, natalStreamOptions);
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
import { jsonForSupabaseSetupFailure } from "@/lib/api/service-unavailable";
|
||||
import { createAdminSupabaseClient } from "@/lib/supabase/admin";
|
||||
import { createServerSupabaseClient } from "@/lib/supabase/server";
|
||||
import { findRectificationSource, returnToRectificationSource } from "@/lib/rectification-in-chat-server";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
type RouteContext = { params: Promise<{ caseId: string }> };
|
||||
|
||||
/**
|
||||
* GET /api/rectification/cases/[caseId]/source — the consultation this case
|
||||
* was started from, or { source: null } (homepage / history cases).
|
||||
* POST /api/rectification/cases/[caseId]/source — go back: appends the return
|
||||
* divider to that consultation once and answers where to go.
|
||||
* (TASK-rectification-in-chat-step1-20261006 C4/C5, BUG-1249/1250)
|
||||
*/
|
||||
async function clients(label: string) {
|
||||
try {
|
||||
return { supabase: await createServerSupabaseClient(), accounting: createAdminSupabaseClient() };
|
||||
} catch (error) {
|
||||
return { failure: jsonForSupabaseSetupFailure(error, label) };
|
||||
}
|
||||
}
|
||||
|
||||
async function caller(supabase: Awaited<ReturnType<typeof createServerSupabaseClient>>) {
|
||||
const { data: { user }, error } = await supabase.auth.getUser();
|
||||
return error || !user ? null : user;
|
||||
}
|
||||
|
||||
export async function GET(_request: Request, context: RouteContext) {
|
||||
const opened = await clients("GET /api/rectification/cases/[caseId]/source");
|
||||
if ("failure" in opened) return opened.failure;
|
||||
const user = await caller(opened.supabase);
|
||||
if (!user) return NextResponse.json({ error: "请先登录" }, { status: 401 });
|
||||
const { caseId } = await context.params;
|
||||
if (!z.string().uuid().safeParse(caseId).success) {
|
||||
return NextResponse.json({ error: "请求内容不正确", code: "invalid_case_id" }, { status: 400 });
|
||||
}
|
||||
const source = await findRectificationSource(opened.supabase, user.id, caseId).catch(() => null);
|
||||
return NextResponse.json({
|
||||
source: source ? { sessionId: source.sessionId, question: source.question, returned: Boolean(source.returned) } : null,
|
||||
});
|
||||
}
|
||||
|
||||
export async function POST(_request: Request, context: RouteContext) {
|
||||
const opened = await clients("POST /api/rectification/cases/[caseId]/source");
|
||||
if ("failure" in opened) return opened.failure;
|
||||
const user = await caller(opened.supabase);
|
||||
if (!user) return NextResponse.json({ error: "请先登录" }, { status: 401 });
|
||||
const { caseId } = await context.params;
|
||||
if (!z.string().uuid().safeParse(caseId).success) {
|
||||
return NextResponse.json({ error: "请求内容不正确", code: "invalid_case_id" }, { status: 400 });
|
||||
}
|
||||
try {
|
||||
const result = await returnToRectificationSource({
|
||||
client: opened.supabase,
|
||||
accounting: opened.accounting,
|
||||
userId: user.id,
|
||||
caseId,
|
||||
});
|
||||
if (!result) {
|
||||
return NextResponse.json({ error: "找不到这次校正对应的对话", code: "source_not_found" }, { status: 404 });
|
||||
}
|
||||
return NextResponse.json({ sessionId: result.sessionId, record: result.record });
|
||||
} catch {
|
||||
return NextResponse.json({ error: "暂时没能回到刚才的对话,请稍后再试", code: "return_failed" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
import { parseOpenRectificationCaseRequest } from "@/lib/rectification-agentic/v9/open-request";
|
||||
import { prepareCaseSegmentChecks } from "@/lib/rectification-agentic/v9/segment-checks";
|
||||
import { resolveRectificationProductDomain } from "@/lib/rectification-agentic/v9/product-domain";
|
||||
import { bindRectificationSource } from "@/lib/rectification-in-chat-server";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
@@ -78,6 +79,18 @@ export async function POST(request: Request) {
|
||||
if (response.shouldStartOpening) {
|
||||
await prepareCaseSegmentChecks({ accounting: accounting as never, userId: user.id, caseId: response.caseId });
|
||||
}
|
||||
// C4 (BUG-1249): a case started from a consultation's offer card is bound
|
||||
// to that card, so the case can go back there. Best effort: the case is
|
||||
// already open, and a failed bind only loses the way back.
|
||||
if (parsed.intent === "homepage" && parsed.sourceSessionId) {
|
||||
await bindRectificationSource({
|
||||
client: supabase,
|
||||
userId: user.id,
|
||||
sourceSessionId: parsed.sourceSessionId,
|
||||
caseId: response.caseId,
|
||||
rectificationSessionId: response.sessionId,
|
||||
}).catch(() => false);
|
||||
}
|
||||
return NextResponse.json(response);
|
||||
} catch (error) {
|
||||
return serviceErrorResponse(error);
|
||||
|
||||
Reference in New Issue
Block a user