feat(report): add dense personal report appendix
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
import { LONGFORM_APPENDIX_TABLE, parseLongformAppendixRow } from "@/lib/personal-report-longform-appendix";
|
||||
import { checkSameOrigin, resolveAllowedReportOrigins } from "@/lib/personal-report-entitlement";
|
||||
import { createSupabasePersonalReportService } from "@/lib/personal-report-service";
|
||||
import { releaseRawReportAppendix } from "@/lib/personal-report-raw-appendix";
|
||||
import { readLongformSnapshot, type LongformSnapshotReader } from "@/lib/personal-report-longform-snapshot";
|
||||
import { isSupabaseConfigurationError } from "@/lib/supabase/config";
|
||||
import { createServerSupabaseClient } from "@/lib/supabase/server";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
||||
const privateHeaders = { "Cache-Control": "private, no-store", "X-Content-Type-Options": "nosniff" };
|
||||
const json = (body: unknown, status = 200) => NextResponse.json(body, { status, headers: privateHeaders });
|
||||
|
||||
type RouteContext = { params: Promise<{ reportId: string }> };
|
||||
|
||||
/** Cache-only attachment: no birth input, recalculation, or alternate subject selection. */
|
||||
export async function POST(request: Request, context: RouteContext) {
|
||||
try {
|
||||
const supabase = await createServerSupabaseClient();
|
||||
const { data: { user }, error } = await supabase.auth.getUser();
|
||||
if (error || !user) return json({ error: "请先登录" }, 401);
|
||||
const origin = checkSameOrigin(request.url, request.headers.get("origin"), resolveAllowedReportOrigins(process.env), request.headers);
|
||||
if (!origin.ok) return json({ error: "此处无法下载,请回到报告页重试。" }, 403);
|
||||
const { reportId } = await context.params;
|
||||
if (!uuidPattern.test(reportId)) return json({ error: "报告不存在" }, 404);
|
||||
const report = await createSupabasePersonalReportService(supabase).getOwnedById(user.id, reportId);
|
||||
if (!report) return json({ error: "报告不存在" }, 404);
|
||||
if (report.status !== "ready") return json({ error: "报告尚未完成" }, 409);
|
||||
const result = 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", reportId).eq("user_id", user.id).eq("request_id", report.requestId)
|
||||
.maybeSingle();
|
||||
const cached = result.error ? null : parseLongformAppendixRow(result.data);
|
||||
if (!cached || cached.reportId !== reportId || cached.userId !== user.id || cached.requestId !== report.requestId
|
||||
|| cached.status !== "ready" || !cached.markdown) {
|
||||
return json({ error: "这份报告暂无原始附录。" }, 410);
|
||||
}
|
||||
const stored = await readLongformSnapshot(supabase as unknown as LongformSnapshotReader, { reportId, userId: user.id, requestId: report.requestId });
|
||||
if (stored && (stored.status !== "ready" || stored.snapshot.markdown !== cached.markdown)) {
|
||||
return json({ error: "原始附录暂时无法下载,请稍后重试。" }, 503);
|
||||
}
|
||||
return json(releaseRawReportAppendix(cached.markdown, stored?.status === "ready" ? stored.snapshot.readerDashaApplicability : undefined));
|
||||
} catch (error) {
|
||||
return json({ error: "原始附录暂时无法下载,请稍后重试。" }, isSupabaseConfigurationError(error) ? 503 : 500);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { LONGFORM_SNAPSHOT_SECTION_ID } from "@/lib/personal-report-longform-snapshot";
|
||||
import { resolveMissingBirthTimezoneOffset } from "@/lib/birth-profile-timezone";
|
||||
import { runConsultationWorkflow } from "@/mastra";
|
||||
import { createPersonalReportAgent } from "@/mastra/personal-report";
|
||||
@@ -108,14 +109,14 @@ export async function GET() {
|
||||
if (failed.length > 0) {
|
||||
const { data: sections, error: sectionError } = await supabase
|
||||
.from("personal_report_sections")
|
||||
.select("request_id, status, last_error_code")
|
||||
.select("request_id, section_id, status, last_error_code")
|
||||
.eq("user_id", user.id)
|
||||
.in("request_id", failed.map((report) => report.requestId));
|
||||
if (sectionError) throw sectionError;
|
||||
const grouped = new Map<string, Array<{ status: string; lastErrorCode: string | null }>>();
|
||||
for (const row of Array.isArray(sections) ? sections : []) {
|
||||
const requestId = typeof row.request_id === "string" ? row.request_id : "";
|
||||
if (!requestId) continue;
|
||||
if (!requestId || row.section_id === LONGFORM_SNAPSHOT_SECTION_ID) continue;
|
||||
const list = grouped.get(requestId) ?? [];
|
||||
list.push({
|
||||
status: typeof row.status === "string" ? row.status : "pending",
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
|
||||
.personal-report-fact-tables { margin-block: 2rem; min-width: 0; }
|
||||
.personal-report-fact-tables details { margin-block: 1rem; padding: 1rem; border: 1px solid var(--color-border); border-radius: 0.5rem; }
|
||||
.personal-report-fact-tables summary { cursor: pointer; min-height: 2.75rem; }
|
||||
.personal-report-fact-tables table { width: 100%; border-collapse: collapse; }
|
||||
.personal-report-fact-tables th, .personal-report-fact-tables td { padding: 0.5rem; text-align: start; border-bottom: 1px solid var(--color-border); overflow-wrap: anywhere; }
|
||||
@media print { .personal-report-fact-tables details { break-inside: auto; } }
|
||||
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--color-canvas);
|
||||
--color-foreground: var(--color-ink);
|
||||
|
||||
Reference in New Issue
Block a user