fix(consult): resolve chat subject from server-owned profile binding

This commit is contained in:
jesse-ux
2026-09-22 12:11:52 +08:00
parent 10baeb2fa8
commit 6ac61be07d
10 changed files with 1101 additions and 8 deletions
+34 -4
View File
@@ -74,6 +74,11 @@ import {
prepareConsultationRoute,
type PreparedConsultationRoute,
} from "@/lib/consultation-route-service";
import {
ConsultationSubjectError,
consultationSubjectBindingFromSession,
consultationSubjectFailureResponse,
} from "@/lib/consultation-subject-resolver";
import { loadReportCandidateRange } from "@/lib/report-candidate-range";
import {
loadGeneralDailyPanchangaContext,
@@ -297,7 +302,7 @@ export async function POST(request: Request) {
const { data: chatSession, error: chatSessionError } = await supabase
.from("chat_sessions")
.select("id,model_id,model_config_version,session_type,messages,title,theme,chart_profile_role,context_summary")
.select("id,model_id,model_config_version,session_type,messages,title,theme,chart_profile_id,chart_profile_name,chart_profile_role,context_summary")
.eq("id", parsed.data.sessionId)
.eq("user_id", user.id)
.maybeSingle();
@@ -393,6 +398,23 @@ export async function POST(request: Request) {
prepared = await prepareConsultationRoute<ModelSelection, ConsultationPlan>({
userId,
mode: parsed.data.consultationMode,
subject: {
binding: consultationSubjectBindingFromSession(chatSession),
clientBirth: parsed.data,
async loadOwnedChartProfile(chartProfileId) {
const { data, error } = await supabase
.from("chart_profiles")
.select("id,user_id,role,profile")
.eq("id", chartProfileId)
.eq("user_id", userId)
.maybeSingle();
if (error) throw new ConsultationSubjectError("subject_unavailable");
if (!data || typeof data.id !== "string" || typeof data.user_id !== "string" || typeof data.role !== "string") {
return null;
}
return { id: data.id, userId: data.user_id, role: data.role, profile: data.profile };
},
},
async loadProfile(profileUserId) {
const { data, error } = await supabase
.from("profiles")
@@ -458,6 +480,10 @@ export async function POST(request: Request) {
{ status: 409 },
);
}
if (error instanceof ConsultationSubjectError) {
const failure = consultationSubjectFailureResponse(error);
return NextResponse.json(failure.body, { status: failure.status });
}
if (error instanceof ConsultationProfileTruthError) {
const modeChanged = error.code === "mode_changed";
return NextResponse.json(
@@ -483,6 +509,10 @@ export async function POST(request: Request) {
);
}
const subjectName = prepared.subject?.name
|| prepared.serverChart?.name
|| prepared.declaredWindow?.name
|| "";
const resolvedQuestion = resolveConsultationQuestion({
visibleQuestion: parsed.data.question,
entrypoint: parsed.data.entrypoint,
@@ -593,7 +623,7 @@ export async function POST(request: Request) {
model: selectedModel,
question: visibleQuestion,
history: chatSession.messages,
name: parsed.data.name,
name: subjectName,
signal: request.signal,
onObservation(observation) {
if (!observation.late) {
@@ -803,7 +833,7 @@ export async function POST(request: Request) {
model: selectedModel,
question: parsed.data.question,
theme: consultationTheme,
chartRole: chatSession.chart_profile_role === "other" ? "other" : "self",
chartRole: prepared.subject?.role === "other" ? "other" : "self",
signal: request.signal,
}).then(async (title) => {
if (!title) return null;
@@ -1359,7 +1389,7 @@ export async function POST(request: Request) {
try {
const history = storedHistory;
const name = prepared.serverChart?.name ?? prepared.declaredWindow?.name ?? parsed.data.name;
const name = subjectName;
const consultationMode: ConsultationBirthTimeMode = prepared.consultationMode;
const generalDailyContext = shouldLoadGeneralDailyPanchanga({
consultationMode,
@@ -10,6 +10,15 @@ import {
import { logIgnoredSessionMessages } from "@/lib/chat-session-observability";
import { consumeUserRequestRateLimit } from "@/lib/request-rate-limit";
import { metadataUpdateValues } from "@/lib/session-metadata-update";
import {
authoritativeSessionBinding,
ConsultationSubjectError,
consultationSubjectBindingFromSession,
consultationSubjectFailureResponse,
ownedChartProfileFromResult,
selfNameFromResult,
sessionBindingRequested,
} from "@/lib/consultation-subject-resolver";
type RouteContext = { params: Promise<{ id: string }> };
@@ -91,6 +100,34 @@ export async function PATCH(request: Request, context: RouteContext) {
if (!values) {
return NextResponse.json({ error: "聊天记录格式不正确" }, { status: 400 });
}
if (sessionBindingRequested(values)) {
const { data: existing, error: existingError } = await supabase
.from("chat_sessions")
.select("messages,chart_profile_id,chart_profile_name,chart_profile_role")
.eq("id", id)
.eq("user_id", user.id)
.maybeSingle();
if (existingError) return NextResponse.json({ error: "聊天记录暂时无法同步" }, { status: 500 });
if (!existing) return NextResponse.json({ error: "聊天记录不存在或已被删除" }, { status: 404 });
const binding = await authoritativeSessionBinding({
userId: user.id,
requested: values,
existingMessages: existing.messages,
existingBinding: consultationSubjectBindingFromSession(existing),
loadSelfProfile: async (profileUserId) => selfNameFromResult(await supabase
.from("profiles")
.select("name")
.eq("id", profileUserId)
.maybeSingle()),
loadOwnedChartProfile: async (chartProfileId) => ownedChartProfileFromResult(await supabase
.from("chart_profiles")
.select("id,user_id,role,profile")
.eq("id", chartProfileId)
.eq("user_id", user.id)
.maybeSingle()),
});
if (binding) values = { ...values, ...binding };
}
const { data, error } = await supabase
.from("chat_sessions")
.update(values)
@@ -102,6 +139,10 @@ export async function PATCH(request: Request, context: RouteContext) {
if (!data) return NextResponse.json({ error: "聊天记录不存在或已被删除" }, { status: 404 });
return NextResponse.json({ ok: true });
} catch (error) {
if (error instanceof ConsultationSubjectError) {
const failure = consultationSubjectFailureResponse(error);
return NextResponse.json(failure.body, { status: failure.status });
}
if (error instanceof ChatSessionBodyTooLargeError) {
return NextResponse.json({ error: error.message }, { status: 413 });
}
+30 -1
View File
@@ -5,6 +5,13 @@ import {
ChatSessionBodyTooLargeError,
readChatSessionJson,
} from "@/lib/chat-session-write-contract";
import {
authoritativeSessionBinding,
ConsultationSubjectError,
consultationSubjectFailureResponse,
ownedChartProfileFromResult,
selfNameFromResult,
} from "@/lib/consultation-subject-resolver";
import { consumeUserRequestRateLimit } from "@/lib/request-rate-limit";
import { resolveInheritedContextSummary } from "@/lib/session-context-summary";
import { isSupabaseConfigurationError } from "@/lib/supabase/config";
@@ -93,6 +100,24 @@ export async function POST(request: Request) {
const parsed = chatSessionCreateSchema.safeParse(await readChatSessionJson(request));
if (!parsed.success) return NextResponse.json({ error: "聊天记录格式不正确" }, { status: 400 });
const { id, updated_at: _ignoredClientClock, continued_from_session_id: continuedFromSessionId, ...values } = parsed.data;
const binding = await authoritativeSessionBinding({
userId: user.id,
requested: values,
existingMessages: values.messages,
existingBinding: null,
loadSelfProfile: async (profileUserId) => selfNameFromResult(await supabase
.from("profiles")
.select("name")
.eq("id", profileUserId)
.maybeSingle()),
loadOwnedChartProfile: async (chartProfileId) => ownedChartProfileFromResult(await supabase
.from("chart_profiles")
.select("id,user_id,role,profile")
.eq("id", chartProfileId)
.eq("user_id", user.id)
.maybeSingle()),
});
const insertValues = binding ? { ...values, ...binding } : values;
const inheritedSummary = await resolveInheritedContextSummary({
continuedFromSessionId,
loadOwnedSummary: async (sourceId) => {
@@ -109,7 +134,7 @@ export async function POST(request: Request) {
chatSessionCreateInsertRow({
id,
userId: user.id,
values,
values: insertValues,
inheritedSummary,
updatedAt: new Date().toISOString(),
}),
@@ -117,6 +142,10 @@ export async function POST(request: Request) {
if (error) return NextResponse.json({ error: "聊天记录暂时无法同步" }, { status: 500 });
return NextResponse.json({ ok: true }, { status: 201 });
} catch (error) {
if (error instanceof ConsultationSubjectError) {
const failure = consultationSubjectFailureResponse(error);
return NextResponse.json(failure.body, { status: failure.status });
}
if (error instanceof ChatSessionBodyTooLargeError) {
return NextResponse.json({ error: error.message }, { status: 413 });
}