fix: integrate people archive, report reader and western chart corrections
Independent Staging Quality Gate / validate (push) Successful in 16m28s
Independent Staging Quality Gate / publish (push) Successful in 3m33s

Validate on Linux Node 22 and PostgreSQL 17: 3894 frontend tests and 64 database tests pass, with no removed test names or new failures. Preserve static Home, bounded gzip, assertion-change records and manual acceptance gaps.

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
jesse-ux
2026-09-25 20:41:16 +08:00
co-authored by Claude Code
parent 48fe4daf51
commit 4d801e53c3
78 changed files with 34844 additions and 441 deletions
+6 -4
View File
@@ -42,7 +42,7 @@ import { SidebarTrigger } from "@/components/ui/sidebar";
import { useHomeShellRegistration } from "@/hooks/use-home-shell-registration";
import { useSessionList } from "@/hooks/use-session-list";
import { composerDraftSnapshot, setComposerDraft } from "@/lib/composer-draft";
import { bindCurrentSubjectAccount } from "@/lib/current-subject";
import { bindCurrentSubjectAccount, useCurrentSubjectProfile } from "@/lib/current-subject";
import { DailyStarlanguageBinder } from "@/components/daily-starlanguage-binder";
import { NewChatDeepLink, SynastryDeepLink } from "@/components/people-home-links";
import { StarterEntries, StarterGreeting } from "@/components/starter-home";
@@ -185,6 +185,7 @@ export default function Home() {
account,
setAccount,
ready: sessionListReady,
waitUntilReady: waitForSessionList,
settled: sessionListSettled,
boot: sessionListBoot,
reload: reloadSessionList,
@@ -407,7 +408,7 @@ export default function Home() {
});
const { synastryPanel, clearSynastryHistory, applyCloudSynastryHistory } = useSynastry({
accountId, profile, chooseSuggestedQuestion, closeAccountDialog,
accountId, profile, chooseSuggestedQuestion, closeAccountDialog, startNewChat,
});
const {
@@ -540,7 +541,6 @@ export default function Home() {
queueMicrotask(() => {
setChartLibrary([]);
clearSynastryHistory();
bindCurrentSubjectAccount("", null);
});
return;
}
@@ -604,7 +604,8 @@ export default function Home() {
}, [account, accountId, applyCloudSynastryHistory, beginChatProfileReload, clearSynastryHistory, markChatProfilesFailed, markChatProfilesReady, profile]);
const profileComplete = isProfileComplete(profile);
const birthTimeRoute = resolveBirthTimeConsultationRoute(profile, birthTimeConsultationConsent, activeSessionId);
const dailyProfile = useCurrentSubjectProfile(profile);
const birthTimeRoute = resolveBirthTimeConsultationRoute(dailyProfile ?? { ...profile, time: "", reportedTime: "", birthTimeStatus: "reported", birthTimeSource: "unknown" }, birthTimeConsultationConsent, activeSessionId);
const natalMinuteAvailable = birthTimeRoute.kind === "consult"
&& (birthTimeRoute.mode === "verified_chart" || birthTimeRoute.mode === "unverified_birth_time");
@@ -763,6 +764,7 @@ export default function Home() {
const controller = new AbortController();
void runHomeBootstrap({
sessionListReady,
waitForSessionList,
sessionListBoot,
uiPreview,
uiPreviewMode,
@@ -1,6 +1,6 @@
import { NextResponse } from "next/server";
import { libraryProfileFromSubjectRow, parseChartSubjectWrite } from "@/lib/chart-subject-write";
import { CHART_SUBJECT_API_SELECT } from "@/lib/subject-birth";
import { CHART_SUBJECT_API_SELECT, isChartSubjectId } from "@/lib/subject-birth";
import { createServerSupabaseClient } from "@/lib/supabase/server";
import { isSupabaseConfigurationError } from "@/lib/supabase/config";
@@ -9,7 +9,8 @@ type RouteContext = {
};
function errorMessage(error: unknown, fallback: string) {
return error instanceof Error ? error.message : fallback;
return error && typeof error === "object" && "message" in error && typeof error.message === "string"
? error.message : fallback;
}
function databaseFailure(error: unknown): NextResponse | null {
@@ -41,6 +42,7 @@ export async function GET(_request: Request, context: RouteContext) {
const supabase = await createServerSupabaseClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user) return NextResponse.json({ error: "请先登录" }, { status: 401 });
if (!isChartSubjectId(id)) return NextResponse.json({ error: "星盘不存在" }, { status: 404 });
const { data, error } = await supabase
.from("chart_profiles")
.select(CHART_SUBJECT_API_SELECT)
@@ -53,12 +55,13 @@ export async function GET(_request: Request, context: RouteContext) {
const usage = await supabase.rpc("chart_subject_usage", { p_id: id });
if (usage.error) throw usage.error;
const row = Array.isArray(usage.data) ? usage.data[0] as { session_count?: number; report_count?: number } : null;
if (!row || !Number.isInteger(row.session_count) || Number(row.session_count) < 0
|| !Number.isInteger(row.report_count) || Number(row.report_count) < 0) {
return NextResponse.json({ error: "暂时查不到这个人的对话数量,稍后再试" }, { status: 503 });
}
return NextResponse.json({
profile: toClientProfile(data as Record<string, unknown>),
usage: {
sessions: row?.session_count ?? 0,
reports: row?.report_count ?? 0,
},
usage: { sessions: row.session_count, reports: row.report_count },
});
} catch (error) {
if (isSupabaseConfigurationError(error)) {
@@ -79,6 +82,7 @@ export async function DELETE(_request: Request, context: RouteContext) {
const supabase = await createServerSupabaseClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user) return NextResponse.json({ error: "请先登录" }, { status: 401 });
if (!isChartSubjectId(id)) return NextResponse.json({ error: "星盘不存在" }, { status: 404 });
const { error } = await supabase.rpc("delete_chart_subject", { p_id: id });
if (error) {
const mapped = databaseFailure(error);
@@ -102,6 +106,7 @@ export async function PUT(request: Request, context: RouteContext) {
const supabase = await createServerSupabaseClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user) return NextResponse.json({ error: "请先登录" }, { status: 401 });
if (!isChartSubjectId(id)) return NextResponse.json({ error: "星盘不存在" }, { status: 404 });
const body = await request.json().catch(() => null) as { profile?: unknown } | null;
const parsed = parseChartSubjectWrite(body?.profile);
+6 -4
View File
@@ -11,7 +11,8 @@ import { isSupabaseConfigurationError } from "@/lib/supabase/config";
const SUBJECT_LIMIT_MESSAGE = "最多保存 5 个人的星盘";
function errorMessage(error: unknown, fallback: string) {
return error instanceof Error ? error.message : fallback;
return error && typeof error === "object" && "message" in error && typeof error.message === "string"
? error.message : fallback;
}
function databaseFailure(error: unknown): NextResponse | null {
@@ -83,11 +84,12 @@ export async function POST(request: Request) {
const existing = await supabase
.from("chart_profiles")
.select("id", { count: "exact", head: true })
.select("id")
.eq("user_id", user.id)
.eq("role", "other");
.eq("role", "other")
.limit(MAX_SAVED_SUBJECTS - 1);
if (existing.error) throw existing.error;
if (subjectLimitReached(existing.count ?? 0)) {
if (subjectLimitReached(Array.isArray(existing.data) ? existing.data.length : 0)) {
return NextResponse.json({ error: SUBJECT_LIMIT_MESSAGE, code: "subject_limit_reached" }, { status: 409 });
}
+12 -8
View File
@@ -1,3 +1,4 @@
import { normalizeReportSubjectRequest } from "@/lib/report-subject-request";
import { NextResponse } from "next/server";
import { LONGFORM_SNAPSHOT_SECTION_ID } from "@/lib/personal-report-longform-snapshot";
import { resolveMissingBirthTimezoneOffset } from "@/lib/birth-profile-timezone";
@@ -37,6 +38,7 @@ import { createServerSupabaseClient } from "@/lib/supabase/server";
import {
ConsultationSubjectError,
loadSubjectBirth,
isChartSubjectId,
subjectIdFromQuery,
} from "@/lib/subject-birth";
import { consultationSubjectFailureResponse } from "@/lib/consultation-subject-resolver";
@@ -104,6 +106,9 @@ export async function GET(request: Request) {
return NextResponse.json({ error: "请先登录" }, { status: 401 });
}
const subjectId = subjectIdFromQuery(new URL(request.url).searchParams.get("subject"));
if (subjectId !== "self" && !isChartSubjectId(subjectId)) {
return NextResponse.json({ error: "星盘不存在", code: "subject_not_found" }, { status: 404 });
}
let listQuery = supabase
.from("personal_reports")
.select(REPORT_LIST_COLUMNS)
@@ -180,14 +185,12 @@ export async function POST(request: Request) {
const persistence: PersonalReportService = createSupabasePersonalReportService(admin);
const adminDataClient = createPersonalReportDataClient(admin);
const rawBody = await request.json().catch(() => null);
const requestedSubject = subjectIdFromQuery(
rawBody && typeof rawBody === "object" && "chartProfileId" in rawBody
? (rawBody as { chartProfileId?: unknown }).chartProfileId
: rawBody && typeof rawBody === "object" && "subjectId" in rawBody
? (rawBody as { subjectId?: unknown }).subjectId
: null,
);
const normalized = normalizeReportSubjectRequest(await request.json().catch(() => null));
if (normalized.mismatch) {
return NextResponse.json({ error: "报告人物不一致", code: "subject_mismatch" }, { status: 400 });
}
const requestedSubject = normalized.subjectId;
const rawBody = normalized.body;
let profile: unknown = null;
if (userId) {
try {
@@ -220,6 +223,7 @@ export async function POST(request: Request) {
rawBody,
profile,
loadCandidateRange: async () => {
if (requestedSubject !== "self") return null;
const profileRow = profile && typeof profile === "object" ? profile as Record<string, unknown> : {};
const caseId = typeof profileRow.rectification_case_id === "string"
? profileRow.rectification_case_id