feat: enrich daily starlanguage with transit probe

This commit is contained in:
732642856
2026-07-18 04:23:38 +08:00
parent 15420d81e1
commit 3ef595ebc9
2 changed files with 85 additions and 2 deletions
@@ -1,4 +1,5 @@
import { NextResponse } from "next/server";
import { chinaLocations } from "@/data/china-locations";
type Profile = {
name?: string;
@@ -6,8 +7,11 @@ type Profile = {
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: "不要把暂时的沉默误读成最终答案。" },
@@ -21,14 +25,90 @@ function pickCard(profile: Profile, today: string) {
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: pickCard(profile, today),
source: "calculation_lite",
card: transitCard ?? pickCard(profile, today),
source: transitCard ? "jyotish_api_transit_lite" : "calculation_lite",
claim_status: "exploratory_unvalidated",
boundary: "not_deterministic_prediction",
});
@@ -22,6 +22,9 @@ def test_daily_starlanguage_entrypoint_is_productized() -> None:
def test_daily_starlanguage_api_declares_honest_source_boundary() -> None:
source = DAILY_ROUTE.read_text(encoding="utf-8")
assert "status: \"ok\"" in source
assert "/api/chart" in source
assert "/api/transit" in source
assert "jyotish_api_transit_lite" in source
assert "calculation_lite" in source
assert "exploratory_unvalidated" in source
assert "not_deterministic_prediction" in source