Merge origin/main into agent-guided birth time rectification

This commit is contained in:
Jesse_Chen
2026-07-18 18:54:21 +08:00
170 changed files with 77128 additions and 355 deletions
+91 -1
View File
@@ -1,5 +1,5 @@
import { NextResponse } from "next/server";
import { isAdminEmail } from "@/lib/supabase/admin";
import { createAdminSupabaseClient, isAdminEmail } from "@/lib/supabase/admin";
import {
isSupabaseConfigurationError,
} from "@/lib/supabase/config";
@@ -7,6 +7,35 @@ import { createServerSupabaseClient } from "@/lib/supabase/server";
export const runtime = "nodejs";
type ProfilePatchPayload = {
name?: unknown;
birth_date?: unknown;
birth_time?: unknown;
country_code?: unknown;
province_code?: unknown;
city_code?: unknown;
district_code?: unknown;
latitude?: unknown;
longitude?: unknown;
timezone_offset?: unknown;
};
function nullableString(value: unknown) {
return typeof value === "string" && value.trim() ? value.trim() : null;
}
function nullableNumber(value: unknown) {
return typeof value === "number" && Number.isFinite(value) ? value : null;
}
function isMissingProfileColumn(error: { code?: string; message?: string } | null) {
const message = error?.message?.toLowerCase() ?? "";
return error?.code === "PGRST204"
|| error?.code === "42703"
|| message.includes("schema cache")
|| message.includes("column");
}
export async function GET() {
try {
const supabase = await createServerSupabaseClient();
@@ -38,3 +67,64 @@ export async function GET() {
return NextResponse.json({ error: "账户服务暂时不可用" }, { status: 500 });
}
}
export async function PATCH(request: Request) {
try {
const supabase = await createServerSupabaseClient();
const { data: { user }, error: authError } = await supabase.auth.getUser();
if (authError || !user) {
return NextResponse.json({ error: "请先登录" }, { status: 401 });
}
const payload = await request.json().catch(() => null) as ProfilePatchPayload | null;
if (!payload || typeof payload !== "object") {
return NextResponse.json({ error: "账户资料格式不正确" }, { status: 400 });
}
const admin = createAdminSupabaseClient();
const baseProfile = {
id: user.id,
name: nullableString(payload.name),
birth_date: nullableString(payload.birth_date),
birth_time: nullableString(payload.birth_time),
country_code: nullableString(payload.country_code),
province_code: nullableString(payload.province_code),
city_code: nullableString(payload.city_code),
district_code: nullableString(payload.district_code),
updated_at: new Date().toISOString(),
};
const withCoordinates = {
...baseProfile,
latitude: nullableNumber(payload.latitude),
longitude: nullableNumber(payload.longitude),
timezone_offset: nullableNumber(payload.timezone_offset),
};
const withoutCoordinates = baseProfile;
let { data, error } = await admin
.from("profiles")
.upsert(withCoordinates, { onConflict: "id" })
.select("id")
.single();
if (error && isMissingProfileColumn(error)) {
const fallback = await admin
.from("profiles")
.upsert(withoutCoordinates, { onConflict: "id" })
.select("id")
.single();
data = fallback.data;
error = fallback.error;
}
if (error || !data) {
return NextResponse.json({ error: "暂时无法保存账户资料" }, { status: 500 });
}
return NextResponse.json({ ok: true });
} catch (error) {
if (isSupabaseConfigurationError(error)) {
return NextResponse.json({ error: "Supabase 尚未配置", code: "SUPABASE_NOT_CONFIGURED" }, { status: 503 });
}
return NextResponse.json({ error: "账户服务暂时不可用" }, { status: 500 });
}
}
@@ -0,0 +1,71 @@
import { NextResponse } from "next/server";
import { chinaLocations } from "@/data/china-locations";
type Profile = {
date?: string;
time?: string;
provinceCode?: string;
cityCode?: string;
districtCode?: string;
};
const jyotishApiBase = process.env.JYOTISH_API_BASE ?? "http://127.0.0.1:5200";
function payloadFromProfile(profile: Profile) {
if (!profile.date || !profile.time) return null;
const province = chinaLocations.country.provinces.find((item) => item.code === profile.provinceCode);
const city = province?.cities.find((item) => item.code === profile.cityCode);
const district = city?.districts.find((item) => item.code === profile.districtCode);
const location = district ?? city;
if (!location) return null;
return {
birth_time: `${profile.date} ${profile.time}`,
uncertainty_minutes: 30,
step_minutes: 2,
lat: location.center[1],
lon: location.center[0],
tz: chinaLocations.country.timezone,
};
}
async function fetchQuestionnaire(payload: Record<string, unknown>) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 2500);
try {
const response = await fetch(`${jyotishApiBase}/api/active_rectification_questions`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
cache: "no-store",
signal: controller.signal,
});
if (!response.ok) throw new Error(`jyotish_api_${response.status}`);
return await response.json() as Record<string, unknown>;
} finally {
clearTimeout(timeout);
}
}
export async function POST(request: Request) {
const body = await request.json().catch(() => null) as { profile?: Profile } | null;
const payload = payloadFromProfile(body?.profile ?? {});
if (!payload) {
return NextResponse.json({
status: "blocked",
boundary: "not_auto_rectified",
reason: "profile_incomplete",
});
}
const questionnaire = await fetchQuestionnaire(payload).catch(() => null);
const candidateScan = questionnaire?.candidate_scan && typeof questionnaire.candidate_scan === "object"
? questionnaire.candidate_scan
: null;
const questions = Array.isArray(questionnaire?.questions) ? questionnaire.questions : [];
return NextResponse.json({
status: questionnaire ? "ok" : "blocked",
candidate_scan: candidateScan,
question_count: questions.length,
boundary: "not_auto_rectified",
source: questionnaire ? "active_rectification_questions" : "fallback_unavailable",
});
}
@@ -0,0 +1,35 @@
import { NextResponse } from "next/server";
import { createServerSupabaseClient } from "@/lib/supabase/server";
import { isSupabaseConfigurationError } from "@/lib/supabase/config";
type RouteContext = {
params: Promise<{ id: string }>;
};
function errorMessage(error: unknown, fallback: string) {
return error instanceof Error ? error.message : fallback;
}
export async function DELETE(_request: Request, context: RouteContext) {
try {
const { id } = await context.params;
const supabase = await createServerSupabaseClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user) return NextResponse.json({ error: "请先登录" }, { status: 401 });
const { error } = await supabase
.from("chart_profiles")
.delete()
.eq("id", id)
.eq("user_id", user.id)
.eq("role", "other");
if (error) throw error;
return NextResponse.json({ ok: true });
} catch (error) {
if (isSupabaseConfigurationError(error)) {
return NextResponse.json({ error: "Supabase 尚未配置", code: "SUPABASE_NOT_CONFIGURED" }, { status: 503 });
}
return NextResponse.json({ error: errorMessage(error, "星盘删除失败") }, { status: 500 });
}
}
@@ -0,0 +1,95 @@
import { NextResponse } from "next/server";
import { createServerSupabaseClient } from "@/lib/supabase/server";
import { isSupabaseConfigurationError } from "@/lib/supabase/config";
type ChartProfilePayload = {
id?: string;
role?: "self" | "other";
profile?: unknown;
};
function errorMessage(error: unknown, fallback: string) {
return error instanceof Error ? error.message : fallback;
}
export async function GET() {
try {
const supabase = await createServerSupabaseClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user) return NextResponse.json({ error: "请先登录" }, { status: 401 });
const { data, error } = await supabase
.from("chart_profiles")
.select("id, role, profile, updated_at")
.eq("user_id", user.id)
.order("updated_at", { ascending: false });
if (error) throw error;
return NextResponse.json({ profiles: data ?? [] });
} catch (error) {
if (isSupabaseConfigurationError(error)) {
return NextResponse.json({ error: "Supabase 尚未配置", code: "SUPABASE_NOT_CONFIGURED" }, { status: 503 });
}
return NextResponse.json({ error: errorMessage(error, "星盘库暂时不可用") }, { status: 500 });
}
}
export async function POST(request: Request) {
try {
const supabase = await createServerSupabaseClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user) return NextResponse.json({ error: "请先登录" }, { status: 401 });
const body = await request.json().catch(() => null) as ChartProfilePayload | null;
if (!body?.profile || typeof body.profile !== "object") {
return NextResponse.json({ error: "星盘资料格式不正确" }, { status: 400 });
}
const role = body.role === "self" ? "self" : "other";
const updatedAt = new Date().toISOString();
let data;
let error;
if (role === "self") {
const existing = await supabase
.from("chart_profiles")
.select("id")
.eq("user_id", user.id)
.eq("role", "self")
.maybeSingle();
if (existing.error) throw existing.error;
const query = existing.data?.id
? supabase
.from("chart_profiles")
.update({ profile: body.profile, updated_at: updatedAt })
.eq("id", existing.data.id)
.select("id, role, profile, updated_at")
.single()
: supabase
.from("chart_profiles")
.insert({ user_id: user.id, role, profile: body.profile, updated_at: updatedAt })
.select("id, role, profile, updated_at")
.single();
({ data, error } = await query);
} else {
const record = {
...(body.id ? { id: body.id } : {}),
user_id: user.id,
role,
profile: body.profile,
updated_at: updatedAt,
};
({ data, error } = await supabase
.from("chart_profiles")
.upsert(record, { onConflict: "id" })
.select("id, role, profile, updated_at")
.single());
}
if (error) throw error;
return NextResponse.json({ profile: data });
} catch (error) {
if (isSupabaseConfigurationError(error)) {
return NextResponse.json({ error: "Supabase 尚未配置", code: "SUPABASE_NOT_CONFIGURED" }, { status: 503 });
}
return NextResponse.json({ error: errorMessage(error, "星盘保存失败") }, { status: 500 });
}
}
@@ -0,0 +1,115 @@
import { NextResponse } from "next/server";
import { chinaLocations } from "@/data/china-locations";
type Profile = {
name?: string;
date?: string;
time?: string;
provinceCode?: string;
cityCode?: string;
districtCode?: string;
};
const jyotishApiBase = process.env.JYOTISH_API_BASE ?? "http://127.0.0.1:5200";
const cards = [
{ trend: "先收束,再推进。适合把一个悬而未决的问题拆小。", action: "选一件最重要的事,给它留出 45 分钟不被打断的时间。", caution: "避免在情绪最满时做承诺。" },
{ trend: "适合整理关系与边界。越清楚,越不容易被外界节奏带走。", action: "把今天要回复的人和要推迟的事分开列出来。", caution: "不要把暂时的沉默误读成最终答案。" },
{ trend: "执行力比灵感更重要。小步完成会比大计划更有力量。", action: "先完成一个可交付版本,再考虑优化。", caution: "别让完美感拖慢开始。" },
{ trend: "适合观察资源流向:时间、注意力、金钱都算。", action: "检查一个正在消耗你的习惯,并给它设上限。", caution: "不要为了短期安心做长期成本高的选择。" },
];
function pickCard(profile: Profile, today: string) {
const seed = `${today}-${profile.date ?? ""}-${profile.time ?? ""}-${profile.provinceCode ?? ""}-${profile.cityCode ?? ""}`;
const index = Array.from(seed).reduce((sum, char) => sum + char.charCodeAt(0), 0) % cards.length;
return cards[index];
}
function profilePayload(profile: Profile, today: string) {
if (!profile.date || !profile.time) return null;
const [year, month, day] = profile.date.split("-").map(Number);
const [hour, minute] = profile.time.split(":").map(Number);
const province = chinaLocations.country.provinces.find((item) => item.code === profile.provinceCode);
const city = province?.cities.find((item) => item.code === profile.cityCode);
const district = city?.districts.find((item) => item.code === profile.districtCode);
const location = district ?? city;
if (!year || !month || !day || Number.isNaN(hour) || Number.isNaN(minute) || !location) return null;
return {
year,
month,
day,
hour,
minute,
lat: location.center[1],
lon: location.center[0],
tz: chinaLocations.country.timezone,
transit_date: today,
today,
ayanamsa: "lahiri",
node_mode: "mean",
};
}
function chartPoints(chart: Record<string, unknown>) {
const modules = chart.modules && typeof chart.modules === "object" ? chart.modules as Record<string, unknown> : {};
const chartModule = modules.chart && typeof modules.chart === "object" ? modules.chart as Record<string, unknown> : chart;
const planets = chartModule.planets && typeof chartModule.planets === "object" ? chartModule.planets : undefined;
const ascendant = chartModule.ascendant && typeof chartModule.ascendant === "object" ? chartModule.ascendant : undefined;
return planets && ascendant ? { planets, ascendant } : null;
}
async function fetchJson(path: string, body: Record<string, unknown>) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 2500);
try {
const response = await fetch(`${jyotishApiBase}${path}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
cache: "no-store",
signal: controller.signal,
});
if (!response.ok) throw new Error(`jyotish_api_${response.status}`);
return await response.json() as Record<string, unknown>;
} finally {
clearTimeout(timeout);
}
}
async function transitBackedCard(profile: Profile, today: string) {
const payload = profilePayload(profile, today);
if (!payload) return null;
const chart = await fetchJson("/api/chart", payload);
const points = chartPoints(chart);
if (!points) return null;
const tomorrow = new Date(`${today}T00:00:00.000Z`);
tomorrow.setUTCDate(tomorrow.getUTCDate() + 1);
const transit = await fetchJson("/api/transit", {
natal_planets: points.planets,
ascendant: points.ascendant,
start: today,
end: tomorrow.toISOString().slice(0, 10),
planets_to_check: ["Saturn", "Jupiter", "Rahu", "Ketu"],
});
const summary = transit.summary && typeof transit.summary === "object" ? transit.summary as Record<string, unknown> : {};
const total = Number(summary.total_triggers ?? 0);
return {
trend: total > 0 ? `今日有 ${total} 个可观察过境触发点,适合把它当作时间窗口观察。` : "今日未发现强精确过境触发,适合按本命节奏稳步推进。",
action: "把今日计划压缩到一件主事,并记录实际发生的触发点。",
caution: "过境触发不能单独定事件,需与 Dasha、分盘和本命承诺交叉确认。",
};
}
export async function POST(request: Request) {
const body = await request.json().catch(() => null) as { profile?: Profile; today?: string } | null;
const profile = body?.profile ?? {};
const today = body?.today || new Date().toISOString().slice(0, 10);
const transitCard = await transitBackedCard(profile, today).catch(() => null);
return NextResponse.json({
status: "ok",
card: transitCard ?? pickCard(profile, today),
source: transitCard ? "jyotish_api_transit_lite" : "calculation_lite",
claim_status: "exploratory_unvalidated",
boundary: "not_deterministic_prediction",
});
}
+72
View File
@@ -0,0 +1,72 @@
import { NextResponse } from "next/server";
type Check = {
status: "ok" | "degraded" | "blocked";
message?: string;
latencyMs?: number;
};
const jyotishApiBase = process.env.JYOTISH_API_BASE ?? "http://127.0.0.1:5200";
function envCheck(names: string[]): Check {
const missing = names.filter((name) => !process.env[name]);
return missing.length
? { status: "blocked", message: `missing:${missing.join(",")}` }
: { status: "ok" };
}
function anyEnvCheck(names: string[]): Check {
return names.some((name) => process.env[name])
? { status: "ok" }
: { status: "blocked", message: `missing_one_of:${names.join("|")}` };
}
async function jyotishApiCheck(): Promise<Check> {
const started = Date.now();
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 3000);
try {
const response = await fetch(`${jyotishApiBase}/api/health`, {
cache: "no-store",
signal: controller.signal,
});
return {
status: response.ok ? "ok" : "degraded",
message: response.ok ? undefined : `http:${response.status}`,
latencyMs: Date.now() - started,
};
} catch (error) {
return {
status: "blocked",
message: error instanceof Error ? error.name : "jyotish_api_unavailable",
latencyMs: Date.now() - started,
};
} finally {
clearTimeout(timeout);
}
}
function aggregate(checks: Record<string, Check>) {
if (Object.values(checks).some((check) => check.status === "blocked")) return "blocked";
if (Object.values(checks).some((check) => check.status === "degraded")) return "degraded";
return "ok";
}
export async function GET() {
const checks = {
web: { status: "ok" } satisfies Check,
supabasePublicConfig: envCheck(["NEXT_PUBLIC_SUPABASE_URL", "NEXT_PUBLIC_SUPABASE_ANON_KEY"]),
supabaseServiceRole: envCheck(["SUPABASE_SERVICE_ROLE_KEY"]),
modelProvider: anyEnvCheck(["LLM_MODELS_JSON", "OPENAI_API_KEY", "LLM_API_KEY", "DEEPSEEK_API_KEY"]),
jyotishApi: await jyotishApiCheck(),
};
const status = aggregate(checks);
return NextResponse.json(
{
status,
timestamp: new Date().toISOString(),
checks,
},
{ status: status === "ok" ? 200 : 503 },
);
}
@@ -0,0 +1,72 @@
import { NextResponse } from "next/server";
import { isSupabaseConfigurationError } from "@/lib/supabase/config";
import { createServerSupabaseClient } from "@/lib/supabase/server";
type SynastryReportPayload = {
id?: string;
partnerName?: string;
report?: unknown;
};
function errorMessage(error: unknown, fallback: string) {
return error instanceof Error ? error.message : fallback;
}
export async function GET() {
try {
const supabase = await createServerSupabaseClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user) return NextResponse.json({ error: "请先登录" }, { status: 401 });
const { data, error } = await supabase
.from("synastry_reports")
.select("id, partner_name, report, created_at")
.eq("user_id", user.id)
.order("created_at", { ascending: false })
.limit(10);
if (error) throw error;
return NextResponse.json({ reports: data ?? [] });
} catch (error) {
if (isSupabaseConfigurationError(error)) {
return NextResponse.json({ error: "Supabase 尚未配置", code: "SUPABASE_NOT_CONFIGURED" }, { status: 503 });
}
return NextResponse.json({ error: errorMessage(error, "合盘历史暂时不可用") }, { status: 500 });
}
}
export async function POST(request: Request) {
try {
const body = await request.json().catch(() => null) as SynastryReportPayload | null;
if (!body?.report || typeof body.report !== "object" || Array.isArray(body.report)) {
return NextResponse.json({ error: "合盘报告格式不正确" }, { status: 400 });
}
const supabase = await createServerSupabaseClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user) return NextResponse.json({ error: "请先登录" }, { status: 401 });
const partnerName = body.partnerName?.trim() || "对方";
const record = {
...(body.id ? { id: body.id } : {}),
user_id: user.id,
partner_name: partnerName,
report: body.report,
created_at: new Date().toISOString(),
};
const { data, error } = await supabase
.from("synastry_reports")
.insert(record)
.select("id, partner_name, report, created_at")
.single();
if (error) throw error;
return NextResponse.json({ report: data });
} catch (error) {
if (isSupabaseConfigurationError(error)) {
return NextResponse.json({ error: "Supabase 尚未配置", code: "SUPABASE_NOT_CONFIGURED" }, { status: 503 });
}
return NextResponse.json({ error: errorMessage(error, "合盘历史保存失败") }, { status: 500 });
}
}
+159
View File
@@ -0,0 +1,159 @@
import { NextResponse } from "next/server";
import { chinaLocations } from "@/data/china-locations";
type Profile = {
name?: string;
date?: string;
time?: string;
countryCode?: "CN";
provinceCode?: string;
cityCode?: string;
districtCode?: string;
};
const apiBase = process.env.JYOTISH_API_BASE ?? "http://127.0.0.1:5200";
const china = chinaLocations.country;
function birthPayload(profile: Profile) {
const [year, month, day] = String(profile.date || "").split("-").map(Number);
const [hour, minute] = String(profile.time || "").split(":").map(Number);
const province = china.provinces.find((item) => item.code === profile.provinceCode);
const city = province?.cities.find((item) => item.code === profile.cityCode);
const district = city?.districts.find((item) => item.code === profile.districtCode);
const location = district ?? city;
if (![year, month, day, hour, minute].every(Number.isFinite) || !location) {
throw new Error("birth_profile_incomplete");
}
return {
year, month, day, hour, minute,
second: 0,
lat: location.center[1],
lon: location.center[0],
tz: china.timezone,
};
}
function moonLongitude(chart: Record<string, unknown>) {
const planets = chart.planets && typeof chart.planets === "object" ? chart.planets as Record<string, unknown> : {};
const moon = planets.Moon && typeof planets.Moon === "object" ? planets.Moon as Record<string, unknown> : {};
const value = moon.lon ?? moon.longitude ?? moon.degree;
const numeric = Number(value);
if (!Number.isFinite(numeric)) throw new Error("moon_longitude_missing");
return numeric;
}
function moonSummary(chart: Record<string, unknown>) {
const planets = chart.planets && typeof chart.planets === "object" ? chart.planets as Record<string, unknown> : {};
const moon = planets.Moon && typeof planets.Moon === "object" ? planets.Moon as Record<string, unknown> : {};
return {
sign: moon.sign,
nakshatra: moon.nakshatra,
pada: moon.nakshatra_pada,
lord: moon.nakshatra_lord,
longitude: moonLongitude(chart),
};
}
function d9Summary(varga: Record<string, unknown>) {
const result = varga.result && typeof varga.result === "object" ? varga.result as Record<string, unknown> : {};
const d9 = result.D9_Navamsa && typeof result.D9_Navamsa === "object" ? result.D9_Navamsa as Record<string, unknown> : {};
const ascendant = d9.ascendant && typeof d9.ascendant === "object" ? d9.ascendant as Record<string, unknown> : {};
const planets = d9.planets && typeof d9.planets === "object" ? d9.planets as Record<string, unknown> : {};
return {
ascendant,
moon: planets.Moon,
venus: planets.Venus,
mars: planets.Mars,
source: varga.source,
};
}
function planetSign(point: unknown) {
return point && typeof point === "object" && "sign" in point
? String((point as Record<string, unknown>).sign || "unknown")
: "unknown";
}
function relationshipReport(synastry: Record<string, unknown>, selfD9: Record<string, unknown>, partnerD9: Record<string, unknown>) {
const total = Number(synastry.total_score ?? 0);
const max = Number(synastry.max_score ?? 36);
const ratio = max > 0 ? total / max : 0;
const band = ratio >= 0.72 ? "supportive" : ratio >= 0.5 ? "mixed" : "challenging";
const self = d9Summary(selfD9);
const partner = d9Summary(partnerD9);
return {
status: "evidence_summary",
scoreBand: band,
headline: band === "supportive"
? "基础匹配度偏支持,但仍需结合现实互动与长期运势。"
: band === "mixed"
? "基础匹配度中等,适合重点观察沟通节奏、价值观与关系承诺。"
: "基础匹配度偏谨慎,需要先处理冲突模式与现实条件。",
strengths: [
`Ashtakoot ${total}/${max}`,
`本人 D9 Moon${planetSign(self.moon)}`,
`对方 D9 Moon${planetSign(partner.moon)}`,
],
risks: [
"这不是完整婚恋结论;尚未纳入双方 Dasha、UL/DK 与长期时机。",
`D9 Venus/Mars 需要继续解释:本人 ${planetSign(self.venus)}/${planetSign(self.mars)},对方 ${planetSign(partner.venus)}/${planetSign(partner.mars)}`,
],
nextEvidence: ["双方 Dasha", "UL/DK", "D9 7宫/7主", "现实关系时间线"],
};
}
async function postPython(path: string, body: unknown) {
const response = await fetch(`${apiBase}${path}`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(body),
signal: AbortSignal.timeout(45_000),
});
const data = await response.json().catch(() => null);
if (!response.ok || !data || typeof data !== "object") {
throw new Error(`jyotish_api_${response.status}`);
}
return data as Record<string, unknown>;
}
export async function POST(request: Request) {
try {
const body = await request.json().catch(() => null) as { selfProfile?: Profile; partnerProfile?: Profile } | null;
if (!body?.selfProfile || !body.partnerProfile) {
return NextResponse.json({ error: "请提供双方星盘资料" }, { status: 400 });
}
const selfChart = await postPython("/api/chart", birthPayload(body.selfProfile));
const partnerChart = await postPython("/api/chart", birthPayload(body.partnerProfile));
const selfD9 = await postPython("/api/varga_full", {
...birthPayload(body.selfProfile),
planets: selfChart.planets,
ascendant: selfChart.ascendant,
divisions: ["D9"],
});
const partnerD9 = await postPython("/api/varga_full", {
...birthPayload(body.partnerProfile),
planets: partnerChart.planets,
ascendant: partnerChart.ascendant,
divisions: ["D9"],
});
const synastry = await postPython("/api/synastry", {
male_moon: moonLongitude(selfChart),
female_moon: moonLongitude(partnerChart),
});
return NextResponse.json({
status: "ok",
method: "ashtakoot_plus_moon_nakshatra_d9",
evidenceLayers: ["ashtakoot", "moon_nakshatra", "d9_navamsa"],
selfChart: { moon: moonSummary(selfChart), d9: d9Summary(selfD9) },
partnerChart: { moon: moonSummary(partnerChart), d9: d9Summary(partnerD9) },
synastry,
relationshipReport: relationshipReport(synastry, selfD9, partnerD9),
});
} catch (error) {
return NextResponse.json({
status: "blocked",
error: error instanceof Error ? error.message : "synastry_unavailable",
message: "合盘计算暂时不可用;可先保留合盘问题草稿。",
}, { status: 503 });
}
}