import "server-only"; import { LONGFORM_APPENDIX_TABLE, nextLongformAppendixState, parseLongformAppendixRow, type LongformAppendixRow, } from "./personal-report-longform-appendix"; import { buildLongformBirthPayload, utcToday } from "./personal-report-longform-birth"; import { buildLongformCoverDocument } from "./personal-report-longform-cover"; import type { GeneratePersonalReportResult } from "./personal-report-generation"; import type { PersonalReportRecord } from "./personal-report-service-core"; import type { ReportDocumentV2 } from "./personal-report-contract"; const APPENDIX_SELECT = "report_id,user_id,request_id,status,markdown,content_sha256,attempt_count,last_error_code"; const DEFAULT_API_BASE = "http://127.0.0.1:5200"; const ENGINE_TIMEOUT_MS = 180_000; export class LongformGenerateError extends Error { readonly name = "LongformGenerateError"; constructor( readonly code: "calculation_unavailable" | "report_schema_invalid" | "birth_time_not_usable", readonly retryable: boolean, message?: string, ) { super(message ?? code); } } type JsonRecord = Record; export type AppendixClient = { from(table: string): { select(columns: string): { eq(column: string, value: unknown): AppendixFilter; }; upsert( row: JsonRecord, options?: { onConflict?: string }, ): PromiseLike<{ error: { message?: string } | null }>; }; }; type AppendixFilter = { eq(column: string, value: unknown): AppendixFilter; maybeSingle(): PromiseLike<{ data: unknown; error: { message?: string } | null }>; }; export type LongformGenerateDeps = Readonly<{ report: PersonalReportRecord; profile: unknown; candidateRange: Readonly<{ startTime: string; endTime: string }> | null; admin: AppendixClient; displayName: string; birthTimeStatus: ReportDocumentV2["subject"]["birthTimeStatus"]; apiBase?: string; fetchImpl?: typeof fetch; now?: () => Date; signal?: AbortSignal; }>; function record(value: unknown): JsonRecord | null { return value !== null && typeof value === "object" && !Array.isArray(value) ? value as JsonRecord : null; } function text(value: unknown): string | null { if (typeof value !== "string") return null; const trimmed = value.trim(); return trimmed.length > 0 ? trimmed : null; } export async function readOwnedLongformAppendix( client: AppendixClient, reportId: string, userId: string, ): Promise { const result = await client .from(LONGFORM_APPENDIX_TABLE) .select(APPENDIX_SELECT) .eq("report_id", reportId) .eq("user_id", userId) .maybeSingle(); if (result.error) return null; return parseLongformAppendixRow(result.data); } export async function persistLongformAppendix(input: Readonly<{ admin: AppendixClient; reportId: string; userId: string; requestId: string; current: LongformAppendixRow | null; successMarkdown?: string | null; errorCode?: string | null; }>): Promise { const next = nextLongformAppendixState({ current: input.current, successMarkdown: input.successMarkdown, errorCode: input.errorCode, }); try { const result = await input.admin.from(LONGFORM_APPENDIX_TABLE).upsert({ report_id: input.reportId, user_id: input.userId, request_id: input.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(), }, { onConflict: "report_id" }); if (result.error) { throw new LongformGenerateError("calculation_unavailable", true, "appendix_persist_failed"); } } catch (error) { if (error instanceof LongformGenerateError) throw error; throw new LongformGenerateError("calculation_unavailable", true, "appendix_persist_failed"); } } async function fetchLongformMarkdown(input: Readonly<{ payload: Record; apiBase: string; fetchImpl: typeof fetch; signal?: AbortSignal; }>): Promise { const timeout = AbortSignal.timeout(ENGINE_TIMEOUT_MS); const signal = input.signal ? AbortSignal.any([input.signal, timeout]) : timeout; const started = Date.now(); let httpStatus: number | null = null; try { const upstream = await input.fetchImpl(`${input.apiBase.replace(/\/$/, "")}/api/professional_report_reference`, { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json" }, body: JSON.stringify(input.payload), cache: "no-store", signal, }); httpStatus = upstream.status; if (!upstream.ok) { throw new LongformGenerateError( "calculation_unavailable", true, upstream.status === 429 ? "upstream_busy" : "upstream_unavailable", ); } const result = await upstream.json().catch(() => null) as { format?: unknown; markdown?: unknown } | null; if (result?.format !== "markdown" || typeof result.markdown !== "string" || !result.markdown.trim()) { throw new LongformGenerateError("calculation_unavailable", true, "empty_markdown"); } return result.markdown; } catch (error) { throw error; } finally { const durationMs = Date.now() - started; if (httpStatus !== null) { console.info(`[personal-report] engine http_status=${httpStatus} duration_ms=${durationMs}`); } else { console.info(`[personal-report] engine http_status=error duration_ms=${durationMs}`); } } } export async function generatePersonalReportLongform( deps: LongformGenerateDeps, ): Promise { const profile = record(deps.profile); if (!profile) { throw new LongformGenerateError("report_schema_invalid", false); } const today = utcToday(deps.now?.() ?? new Date()); const range = deps.candidateRange ? { start_time: deps.candidateRange.startTime, end_time: deps.candidateRange.endTime } : null; const payload = buildLongformBirthPayload(deps.profile, { today, targetYear: Number(today.slice(0, 4)), candidateRange: range, birthTimeAccuracy: range && range.start_time !== range.end_time ? "provisional" : "confirmed", }); if (!payload) { throw new LongformGenerateError("birth_time_not_usable", false); } const current = await readOwnedLongformAppendix(deps.admin, deps.report.id, deps.report.userId); let markdown = current?.status === "ready" && current.markdown ? current.markdown : null; if (!markdown) { try { markdown = await fetchLongformMarkdown({ payload, apiBase: deps.apiBase ?? process.env.JYOTISH_API_BASE ?? DEFAULT_API_BASE, fetchImpl: deps.fetchImpl ?? fetch, signal: deps.signal, }); } catch (error) { if (deps.signal?.aborted) throw error; await persistLongformAppendix({ admin: deps.admin, reportId: deps.report.id, userId: deps.report.userId, requestId: deps.report.requestId, current, errorCode: error instanceof LongformGenerateError ? error.message.slice(0, 80) : "generation_failed", }).catch(() => undefined); if (error instanceof LongformGenerateError) throw error; throw new LongformGenerateError("calculation_unavailable", true); } await persistLongformAppendix({ admin: deps.admin, reportId: deps.report.id, userId: deps.report.userId, requestId: deps.report.requestId, current, successMarkdown: markdown, }); } try { const document = buildLongformCoverDocument({ report: deps.report, subject: { displayName: deps.displayName, birthTimeStatus: deps.birthTimeStatus, birthPlaceLabel: text(profile.birth_place_label) ?? "未知出生地", }, markdown, generatedAt: (deps.now?.() ?? new Date()).toISOString(), }); return { status: "ready", document, evidenceHash: document.provenance.evidenceHash, }; } catch { throw new LongformGenerateError("report_schema_invalid", false, "final_parse_rejected"); } }