import "server-only"; import { runConsultationWorkflow } from "@/mastra"; import { createPersonalReportAgent } from "@/mastra/personal-report"; import { loadLanguageModelCatalog } from "@/lib/model-catalog"; import { buildReportEvidenceBundleV2, generatePersonalReport, type SkillSnapshot, } from "@/lib/personal-report-generation"; import { createSupabasePersonalReportJobService } from "@/lib/personal-report-job-service"; import { createPersonalReportWorker, PersonalReportWorkerError, runPersonalReportWorkerLoop, type PersonalReportWorkerGenerationContext, } from "@/lib/personal-report-worker-core"; import { createSupabasePersonalReportService } from "@/lib/personal-report-service"; import { createAdminSupabaseClient } from "@/lib/supabase/admin"; import type { ConsultationInput } from "@/mastra/consultation-workflow"; const PROFILE_COLUMNS = [ "name", "birth_date", "active_birth_time", "birth_time_status", "latitude", "longitude", "timezone_offset", "birth_place_label", ].join(","); type JsonRecord = Record; type WorkerGlobal = typeof globalThis & { jyotishaPersonalReportWorker?: PersonalReportWorkerHandle; }; export type PersonalReportWorkerHandle = Readonly<{ workerId: string; done: Promise; stop: () => void; }>; 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; } function finiteNumber(value: unknown): number | null { return typeof value === "number" && Number.isFinite(value) ? value : null; } function parseBirthDate(value: unknown): { year: number; month: number; day: number } | null { const date = text(value); const match = date ? /^(\d{4})-(\d{2})-(\d{2})$/.exec(date) : null; if (!match) return null; const year = Number.parseInt(match[1], 10); const month = Number.parseInt(match[2], 10); const day = Number.parseInt(match[3], 10); if (year < 1900 || year > 2100 || month < 1 || month > 12 || day < 1 || day > 31) return null; return { year, month, day }; } function parseBirthClock(value: unknown): { hour: number; minute: number } | null { const clock = text(value); const match = clock ? /^(\d{1,2}):(\d{2})(?::\d{2})?$/.exec(clock) : null; if (!match) return null; const hour = Number.parseInt(match[1], 10); const minute = Number.parseInt(match[2], 10); if (hour < 0 || hour > 23 || minute < 0 || minute > 59) return null; return { hour, minute }; } function skillSnapshotForReport(context: PersonalReportWorkerGenerationContext): SkillSnapshot { const { report } = context; if (!report.skillName || !report.skillVersion || !report.skillSnapshotSha256) { throw new PersonalReportWorkerError("report_schema_invalid", false, "report Skill provenance is incomplete"); } return { name: report.skillName, version: report.skillVersion, sha256: report.skillSnapshotSha256, sourceCommit: report.skillSourceCommit, }; } async function generateProductionReport(context: PersonalReportWorkerGenerationContext) { const profile = record(context.profile); if (!profile) throw new PersonalReportWorkerError("profile_incomplete", false); const birthTimeStatus = text(profile.birth_time_status); const birthDate = parseBirthDate(profile.birth_date); const birthClock = parseBirthClock(profile.active_birth_time); const latitude = finiteNumber(profile.latitude); const longitude = finiteNumber(profile.longitude); const timezoneOffset = finiteNumber(profile.timezone_offset); const birthPlaceLabel = text(profile.birth_place_label) ?? "未知出生地"; const displayName = text(profile.name) ?? "我的报告"; if ((birthTimeStatus !== "accepted" && birthTimeStatus !== "confirmed") || !birthDate || !birthClock || latitude === null || longitude === null || timezoneOffset === null) { throw new PersonalReportWorkerError("birth_time_not_usable", false); } const catalog = await loadLanguageModelCatalog(); const model = catalog.models.find((entry) => entry.id === catalog.defaultModelId) ?? null; if (!model) throw new PersonalReportWorkerError("model_unavailable", true); const workflows: { theme: string; workflow: unknown }[] = []; for (const rawTheme of context.report.requestedThemes) { const input: ConsultationInput = { year: birthDate.year, month: birthDate.month, day: birthDate.day, hour: birthClock.hour, minute: birthClock.minute, lat: latitude, lon: longitude, tz: timezoneOffset, city: birthPlaceLabel, question: `请为个人报告计算 ${rawTheme} 主题证据`, theme: rawTheme as ConsultationInput["theme"], entryMode: "direct_chart", }; try { workflows.push({ theme: rawTheme, workflow: await runConsultationWorkflow(input, { signal: context.signal }), }); } catch (error) { if (context.signal.aborted) throw error; throw new PersonalReportWorkerError("calculation_unavailable", true); } } const hasUsableBaseChart = workflows.some(({ workflow }) => { const workflowRecord = record(workflow); return workflowRecord?.success === true && record(workflowRecord.chart) !== null; }); if (!hasUsableBaseChart) { throw new PersonalReportWorkerError("calculation_unavailable", true); } let bundle; try { bundle = buildReportEvidenceBundleV2({ workflows, subject: { displayName, birthTimeStatus, birthPlaceLabel, }, requestedThemes: context.report.requestedThemes, reportType: context.report.reportType, presentationMode: context.report.presentationMode, skillSnapshot: skillSnapshotForReport(context), }); } catch { throw new PersonalReportWorkerError("calculation_unavailable", false); } return generatePersonalReport({ reportId: context.report.id, bundle, depth: context.report.depth, agent: createPersonalReportAgent(model), signal: context.signal, }); } function createProductionWorker(workerId: string) { const admin = createAdminSupabaseClient(); const backend = admin as unknown as { from(table: string): { select(columns: string): { eq(column: string, value: unknown): { maybeSingle(): PromiseLike<{ data: unknown; error: { message: string } | null }>; }; }; }; }; return createPersonalReportWorker({ workerId, jobs: createSupabasePersonalReportJobService(admin), reports: createSupabasePersonalReportService(admin), loadProfile: async (userId) => { const { data, error } = await backend .from("profiles") .select(PROFILE_COLUMNS) .eq("id", userId) .maybeSingle(); if (error) throw new Error("personal report profile load failed"); return data ?? null; }, generate: generateProductionReport, }); } function sanitizedErrorName(error: unknown): string { return error instanceof Error ? error.name : "UnknownError"; } /** * Starts one unref'ed loop per Node.js server instance. Database leases remain * the cross-instance exclusivity boundary; the global only prevents duplicate * loops caused by repeated instrumentation/module evaluation in one process. */ export function startPersonalReportWorker(): PersonalReportWorkerHandle { const state = globalThis as WorkerGlobal; if (state.jyotishaPersonalReportWorker) return state.jyotishaPersonalReportWorker; const controller = new AbortController(); const workerId = `personal-report:${globalThis.crypto.randomUUID()}`; let worker: ReturnType | null = null; const lazyWorker = { tick: async () => { worker ??= createProductionWorker(workerId); return worker.tick(); }, }; const done = runPersonalReportWorkerLoop(lazyWorker, { signal: controller.signal, onError: (error) => { console.error(`[personal-report-worker] tick failed reason=${sanitizedErrorName(error)}`); }, }); const handle: PersonalReportWorkerHandle = { workerId, done, stop: () => controller.abort(), }; state.jyotishaPersonalReportWorker = handle; return handle; }