feat: enrich daily starlanguage with transit probe
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
|
import { chinaLocations } from "@/data/china-locations";
|
||||||
|
|
||||||
type Profile = {
|
type Profile = {
|
||||||
name?: string;
|
name?: string;
|
||||||
@@ -6,8 +7,11 @@ type Profile = {
|
|||||||
time?: string;
|
time?: string;
|
||||||
provinceCode?: string;
|
provinceCode?: string;
|
||||||
cityCode?: string;
|
cityCode?: string;
|
||||||
|
districtCode?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const jyotishApiBase = process.env.JYOTISH_API_BASE ?? "http://127.0.0.1:5200";
|
||||||
|
|
||||||
const cards = [
|
const cards = [
|
||||||
{ trend: "先收束,再推进。适合把一个悬而未决的问题拆小。", action: "选一件最重要的事,给它留出 45 分钟不被打断的时间。", caution: "避免在情绪最满时做承诺。" },
|
{ trend: "先收束,再推进。适合把一个悬而未决的问题拆小。", action: "选一件最重要的事,给它留出 45 分钟不被打断的时间。", caution: "避免在情绪最满时做承诺。" },
|
||||||
{ trend: "适合整理关系与边界。越清楚,越不容易被外界节奏带走。", action: "把今天要回复的人和要推迟的事分开列出来。", caution: "不要把暂时的沉默误读成最终答案。" },
|
{ trend: "适合整理关系与边界。越清楚,越不容易被外界节奏带走。", action: "把今天要回复的人和要推迟的事分开列出来。", caution: "不要把暂时的沉默误读成最终答案。" },
|
||||||
@@ -21,14 +25,90 @@ function pickCard(profile: Profile, today: string) {
|
|||||||
return cards[index];
|
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) {
|
export async function POST(request: Request) {
|
||||||
const body = await request.json().catch(() => null) as { profile?: Profile; today?: string } | null;
|
const body = await request.json().catch(() => null) as { profile?: Profile; today?: string } | null;
|
||||||
const profile = body?.profile ?? {};
|
const profile = body?.profile ?? {};
|
||||||
const today = body?.today || new Date().toISOString().slice(0, 10);
|
const today = body?.today || new Date().toISOString().slice(0, 10);
|
||||||
|
const transitCard = await transitBackedCard(profile, today).catch(() => null);
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
status: "ok",
|
status: "ok",
|
||||||
card: pickCard(profile, today),
|
card: transitCard ?? pickCard(profile, today),
|
||||||
source: "calculation_lite",
|
source: transitCard ? "jyotish_api_transit_lite" : "calculation_lite",
|
||||||
claim_status: "exploratory_unvalidated",
|
claim_status: "exploratory_unvalidated",
|
||||||
boundary: "not_deterministic_prediction",
|
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:
|
def test_daily_starlanguage_api_declares_honest_source_boundary() -> None:
|
||||||
source = DAILY_ROUTE.read_text(encoding="utf-8")
|
source = DAILY_ROUTE.read_text(encoding="utf-8")
|
||||||
assert "status: \"ok\"" in source
|
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 "calculation_lite" in source
|
||||||
assert "exploratory_unvalidated" in source
|
assert "exploratory_unvalidated" in source
|
||||||
assert "not_deterministic_prediction" in source
|
assert "not_deterministic_prediction" in source
|
||||||
|
|||||||
Reference in New Issue
Block a user