feat(report): render longform Markdown as the report and close gaps2 holes

New reports skip the writer, persist pl9 Markdown as the body, and settle zero-token usage on the catalog model. Planned longform sections now emit blocked rows instead of vanishing.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-09-06 21:46:16 +08:00
co-authored by Cursor
parent e428e9d056
commit cfcd369d4f
36 changed files with 2290 additions and 402 deletions
@@ -2,106 +2,26 @@ import { NextResponse } from "next/server";
import {
LONGFORM_APPENDIX_TABLE,
nextLongformAppendixState,
parseLongformAppendixRow,
} from "@/lib/personal-report-longform-appendix";
import { PERSONAL_REPORT_LEGACY_PLACEHOLDER } from "@/lib/personal-report-longform-copy";
import {
checkSameOrigin,
resolveAllowedReportOrigins,
} from "@/lib/personal-report-entitlement";
import { createSupabasePersonalReportService } from "@/lib/personal-report-service";
import { loadReportCandidateRange } from "@/lib/report-candidate-range";
import {
ACCOUNT_BIRTH_SELECT,
globalBirthProfileFromAccountRow,
} from "@/lib/server-owned-birth-profile";
import { createAdminSupabaseClient } from "@/lib/supabase/admin";
import { isSupabaseConfigurationError } from "@/lib/supabase/config";
import { createServerSupabaseClient } from "@/lib/supabase/server";
export const runtime = "nodejs";
export const maxDuration = 300;
export const maxDuration = 60;
const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
const jyotishApiBase = process.env.JYOTISH_API_BASE ?? "http://127.0.0.1:5200";
const APPENDIX_SELECT =
"report_id,user_id,request_id,status,markdown,content_sha256,attempt_count,last_error_code";
type RouteContext = { params: Promise<{ reportId: string }> };
type ProfessionalReferenceResponse = Readonly<{
format?: unknown;
markdown?: unknown;
}>;
function utcToday(): string {
return new Date().toISOString().slice(0, 10);
}
function birthPayload(
row: unknown,
extras: Readonly<{
today: string;
targetYear: number;
candidateRange: { start_time: string; end_time: string } | null;
birthTimeAccuracy: "confirmed" | "provisional";
}>,
): Record<string, unknown> | null {
const profile = globalBirthProfileFromAccountRow(row);
const dateMatch = /^(\d{4})-(\d{2})-(\d{2})$/.exec(profile.date ?? "");
const timeMatch = /^(\d{2}):(\d{2})$/.exec(profile.time ?? "");
if (
!dateMatch
|| !timeMatch
|| profile.latitude === null
|| profile.longitude === null
|| profile.timezoneOffset === null
) {
return null;
}
const year = Number.parseInt(dateMatch[1], 10);
return {
year,
month: Number.parseInt(dateMatch[2], 10),
day: Number.parseInt(dateMatch[3], 10),
hour: Number.parseInt(timeMatch[1], 10),
minute: Number.parseInt(timeMatch[2], 10),
lat: profile.latitude,
lon: profile.longitude,
tz: profile.timezoneOffset,
ayanamsa: profile.ayanamsa,
format: "markdown",
packs: ["full"],
today: extras.today,
target_year: extras.targetYear,
age: extras.targetYear - year,
birth_time_accuracy: extras.birthTimeAccuracy,
...(extras.candidateRange ? { candidate_range: extras.candidateRange } : {}),
};
}
function unavailableResponse() {
return NextResponse.json({ error: "全量数据附录暂不可用" }, { status: 503 });
}
function upstreamError(status: number, retryAfter: string | null) {
return NextResponse.json(
{ error: status === 429 ? "全量数据附录生成繁忙,请稍后重试" : "全量数据附录暂不可用" },
{
status: status === 429 ? 429 : 503,
headers: retryAfter ? { "Retry-After": retryAfter } : undefined,
},
);
}
function tryAdminClient() {
try {
return createAdminSupabaseClient();
} catch {
return null;
}
}
export async function POST(request: Request, context: RouteContext) {
try {
const supabase = await createServerSupabaseClient();
@@ -142,79 +62,15 @@ export async function POST(request: Request, context: RouteContext) {
if (cached?.status === "ready" && cached.markdown) {
return NextResponse.json({ format: "markdown", markdown: cached.markdown });
}
if (cached?.status === "unavailable") {
return unavailableResponse();
}
const { data: profileRow, error: profileError } = await supabase
.from("profiles")
.select(ACCOUNT_BIRTH_SELECT)
.eq("id", user.id)
.maybeSingle();
const admin = tryAdminClient();
const range = admin
? await loadReportCandidateRange(admin, { userId: user.id })
: null;
const today = utcToday();
const payload = profileError ? null : birthPayload(profileRow, {
today,
targetYear: Number(today.slice(0, 4)),
candidateRange: range ? { start_time: range.startTime, end_time: range.endTime } : null,
birthTimeAccuracy: range && range.startTime !== range.endTime ? "provisional" : "confirmed",
});
if (!payload) {
return NextResponse.json({ error: "出生资料不完整" }, { status: 422 });
}
const persistAppendix = async (input: {
successMarkdown?: string | null;
errorCode?: string | null;
}) => {
if (!admin || !report.requestId) return;
const next = nextLongformAppendixState({
current: cached,
successMarkdown: input.successMarkdown,
errorCode: input.errorCode,
});
await admin.from(LONGFORM_APPENDIX_TABLE).upsert({
report_id: reportId,
user_id: user.id,
request_id: report.requestId,
status: next.status,
markdown: next.markdown,
content_sha256: next.contentSha256,
attempt_count: next.attemptCount,
last_error_code: next.lastErrorCode,
generated_at: next.status === "ready" ? new Date().toISOString() : null,
updated_at: new Date().toISOString(),
});
};
const upstream = await fetch(`${jyotishApiBase}/api/professional_report_reference`, {
method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json" },
body: JSON.stringify(payload),
cache: "no-store",
});
if (!upstream.ok) {
await persistAppendix({
errorCode: upstream.status === 429 ? "upstream_busy" : "upstream_unavailable",
}).catch(() => undefined);
return upstreamError(upstream.status, upstream.headers.get("retry-after"));
}
const result = await upstream.json().catch(() => null) as ProfessionalReferenceResponse | null;
if (result?.format !== "markdown" || typeof result.markdown !== "string" || !result.markdown.trim()) {
await persistAppendix({ errorCode: "empty_markdown" }).catch(() => undefined);
return upstreamError(502, null);
}
await persistAppendix({ successMarkdown: result.markdown }).catch(() => undefined);
return NextResponse.json({ format: "markdown", markdown: result.markdown });
return NextResponse.json(
{ error: PERSONAL_REPORT_LEGACY_PLACEHOLDER, code: "legacy_report" },
{ status: 410 },
);
} catch (error) {
if (isSupabaseConfigurationError(error)) {
return NextResponse.json({ error: "数据库尚未配置" }, { status: 503 });
}
console.error("professional_report_reference_failed", error instanceof Error ? error.name : "UnknownError");
return NextResponse.json({ error: "全量数据附录暂不可用" }, { status: 500 });
return NextResponse.json({ error: PERSONAL_REPORT_LEGACY_PLACEHOLDER }, { status: 500 });
}
}
@@ -6,6 +6,10 @@ import {
resolveReportRead,
} from "@/lib/personal-report-route-core";
import { safeParseServerReportDocument } from "@/lib/personal-report-contract.server";
import {
LONGFORM_APPENDIX_TABLE,
parseLongformAppendixRow,
} from "@/lib/personal-report-longform-appendix";
import {
createSupabasePersonalReportService,
type PersonalReportService,
@@ -39,7 +43,13 @@ async function resolvePersistenceForUser() {
const supabase = await createServerSupabaseClient();
const { data: { user }, error: authError } = await supabase.auth.getUser();
if (authError || !user) {
return { userId: null as string | null, persistence: null as PersonalReportService | null, jobs: null, listSections: undefined };
return {
userId: null as string | null,
persistence: null as PersonalReportService | null,
jobs: null,
listSections: undefined,
loadLongformMarkdown: undefined,
};
}
const persistence = createSupabasePersonalReportService(supabase);
const sections = createPersonalReportSectionService(supabase as never);
@@ -51,12 +61,23 @@ async function resolvePersistenceForUser() {
const rows = await sections.list(ownerId, requestId);
return rows.map((row) => ({ status: row.status, lastErrorCode: row.lastErrorCode }));
},
loadLongformMarkdown: async (input: Readonly<{ userId: string; reportId: string }>) => {
const appendixRead = await supabase
.from(LONGFORM_APPENDIX_TABLE)
.select("report_id,user_id,request_id,status,markdown,content_sha256,attempt_count,last_error_code")
.eq("report_id", input.reportId)
.eq("user_id", input.userId)
.maybeSingle();
if (appendixRead.error) return null;
const row = parseLongformAppendixRow(appendixRead.data);
return row?.status === "ready" && row.markdown ? row.markdown : null;
},
};
}
export async function GET(request: Request, context: RouteContext) {
try {
const { userId, persistence, jobs, listSections } = await resolvePersistenceForUser();
const { userId, persistence, jobs, listSections, loadLongformMarkdown } = await resolvePersistenceForUser();
const { reportId } = await context.params;
if (!uuidPattern.test(reportId)) {
return NextResponse.json(
@@ -82,6 +103,7 @@ export async function GET(request: Request, context: RouteContext) {
// a substitute.
jobs: jobs ?? undefined,
listSections,
loadLongformMarkdown,
validateReadyDocument: (document) => {
const parsed = safeParseServerReportDocument(document);
return parsed.ok
+4
View File
@@ -45,6 +45,7 @@ const REPORT_LIST_COLUMNS = [
"failure_code",
"created_at",
"completed_at",
"card_summary:report_document->executiveSummary->>summary",
].join(",");
function sanitizedErrorCode(error: unknown): string {
@@ -76,6 +77,9 @@ function listReportView(
completedAt: row.completed_at == null || row.completed_at === ""
? null
: reportListTimestamp(row.completed_at) || null,
...(typeof row.card_summary === "string" && row.card_summary.trim()
? { cardSummary: row.card_summary.trim().slice(0, 240) }
: {}),
...(failure?.summary ? { failureSummary: failure.summary } : {}),
};
}