fix: integrate people archive, report reader and western chart corrections
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:
@@ -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);
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user