feat(home): write the daily card with the Agent and vary the starter heading
The daily starlanguage card claimed to be personal but was a four-card rotation picked by hashing the date and birth place, with the same pool duplicated as a client fallback. It now collects chart, Vimshottari and Narayana dasha, D9/D10 and today's transits, hands that evidence to a dedicated Agent, and keeps the result per account per day in process. A failed generation says so instead of printing generic advice. The starter heading is drawn from a pool on each visit, and the rectification card drops its fine print. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,22 +1,79 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import {
|
||||
dailyStarlanguageCacheKey,
|
||||
dailyStarlanguageEvidence,
|
||||
dailyStarlanguagePrompt,
|
||||
parseDailyStarlanguageText,
|
||||
type DailyStarlanguageCard,
|
||||
} from "@/lib/daily-starlanguage";
|
||||
import {
|
||||
dailyProfilePayload,
|
||||
type GlobalBirthProfile as Profile,
|
||||
type GlobalBirthProfile,
|
||||
} from "@/lib/global-birth-payloads";
|
||||
import { loadLanguageModelCatalog } from "@/lib/model-catalog";
|
||||
import { createServerSupabaseClient } from "@/lib/supabase/server";
|
||||
import { getDailyStarlanguageAgent } from "@/mastra";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
export const maxDuration = 60;
|
||||
|
||||
type Profile = GlobalBirthProfile & { birthTimeStatus?: string };
|
||||
type BirthPayload = NonNullable<Awaited<ReturnType<typeof dailyProfilePayload>>>;
|
||||
type CacheEntry = { readonly day: string; readonly card: DailyStarlanguageCard };
|
||||
|
||||
const jyotishApiBase = process.env.JYOTISH_API_BASE ?? "http://127.0.0.1:5200";
|
||||
const engineTimeoutMs = 8_000;
|
||||
const agentTimeoutMs = 30_000;
|
||||
const cacheLimit = 500;
|
||||
|
||||
const cards = [
|
||||
{ trend: "先收束,再推进。适合把一个悬而未决的问题拆小。", action: "选一件最重要的事,给它留出 45 分钟不被打断的时间。", caution: "避免在情绪最满时做承诺。" },
|
||||
{ trend: "适合整理关系与边界。越清楚,越不容易被外界节奏带走。", action: "把今天要回复的人和要推迟的事分开列出来。", caution: "不要把暂时的沉默误读成最终答案。" },
|
||||
{ trend: "执行力比灵感更重要。小步完成会比大计划更有力量。", action: "先完成一个可交付版本,再考虑优化。", caution: "别让完美感拖慢开始。" },
|
||||
{ trend: "适合观察资源流向:时间、注意力、金钱都算。", action: "检查一个正在消耗你的习惯,并给它设上限。", caution: "不要为了短期安心做长期成本高的选择。" },
|
||||
];
|
||||
const state = globalThis as typeof globalThis & {
|
||||
jyotishaDailyStarlanguageCache?: Map<string, CacheEntry>;
|
||||
jyotishaDailyStarlanguagePending?: Map<string, Promise<DailyStarlanguageCard | null>>;
|
||||
};
|
||||
|
||||
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 cache() {
|
||||
state.jyotishaDailyStarlanguageCache ??= new Map();
|
||||
return state.jyotishaDailyStarlanguageCache;
|
||||
}
|
||||
|
||||
function pending() {
|
||||
state.jyotishaDailyStarlanguagePending ??= new Map();
|
||||
return state.jyotishaDailyStarlanguagePending;
|
||||
}
|
||||
|
||||
function readCache(key: string, today: string): DailyStarlanguageCard | null {
|
||||
const entry = cache().get(key);
|
||||
if (!entry) return null;
|
||||
if (entry.day !== today) {
|
||||
cache().delete(key);
|
||||
return null;
|
||||
}
|
||||
return entry.card;
|
||||
}
|
||||
|
||||
function writeCache(key: string, today: string, card: DailyStarlanguageCard) {
|
||||
const store = cache();
|
||||
for (const [storedKey, entry] of store) {
|
||||
if (entry.day !== today) store.delete(storedKey);
|
||||
}
|
||||
while (store.size >= cacheLimit) {
|
||||
const oldest = store.keys().next();
|
||||
if (oldest.done) break;
|
||||
store.delete(oldest.value);
|
||||
}
|
||||
store.set(key, { day: today, card });
|
||||
}
|
||||
|
||||
async function fetchEngine(path: string, body: Record<string, unknown>) {
|
||||
const response = await fetch(`${jyotishApiBase}${path}`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
cache: "no-store",
|
||||
signal: AbortSignal.timeout(engineTimeoutMs),
|
||||
});
|
||||
if (!response.ok) throw new Error(`jyotish_api_${response.status}`);
|
||||
return await response.json() as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function chartPoints(chart: Record<string, unknown>) {
|
||||
@@ -27,57 +84,92 @@ function chartPoints(chart: Record<string, unknown>) {
|
||||
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 = await dailyProfilePayload(profile, today);
|
||||
if (!payload) return null;
|
||||
const chart = await fetchJson("/api/chart", payload);
|
||||
async function collectLayers(payload: BirthPayload, today: string) {
|
||||
const chart = await fetchEngine("/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、分盘和本命承诺交叉确认。",
|
||||
};
|
||||
const withPoints = { ...payload, planets: points.planets, ascendant: points.ascendant };
|
||||
const [vimshottari, narayana, varga, transit] = await Promise.all([
|
||||
fetchEngine("/api/dasha", { ...withPoints, dasha: "vimshottari", today }).catch(() => null),
|
||||
fetchEngine("/api/dasha", { ...withPoints, dasha: "narayana", today }).catch(() => null),
|
||||
fetchEngine("/api/varga_full", { ...withPoints, divisions: ["D9", "D10"] }).catch(() => null),
|
||||
fetchEngine("/api/transit", {
|
||||
natal_planets: points.planets,
|
||||
ascendant: points.ascendant,
|
||||
start: today,
|
||||
end: tomorrow.toISOString().slice(0, 10),
|
||||
planets_to_check: ["Saturn", "Jupiter", "Rahu", "Ketu", "Mars", "Sun"],
|
||||
}).catch(() => null),
|
||||
]);
|
||||
return { chart, vimshottari, narayana, varga, transit };
|
||||
}
|
||||
|
||||
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);
|
||||
async function generateCard(payload: BirthPayload, profile: Profile, today: string) {
|
||||
const layers = await collectLayers(payload, today);
|
||||
if (!layers) return null;
|
||||
const modelId = process.env.DAILY_STARLANGUAGE_MODEL_ID?.trim() || "deepseek-v4-flash";
|
||||
const catalog = await loadLanguageModelCatalog();
|
||||
const model = catalog.models.find((entry) => entry.id === modelId)
|
||||
?? catalog.models.find((entry) => entry.id === catalog.defaultModelId)
|
||||
?? null;
|
||||
if (!model) return null;
|
||||
const evidence = dailyStarlanguageEvidence(layers, today, profile.birthTimeStatus === "confirmed");
|
||||
const result = await getDailyStarlanguageAgent(model).generate(
|
||||
[{ role: "user", content: dailyStarlanguagePrompt(evidence) }],
|
||||
{ abortSignal: AbortSignal.timeout(agentTimeoutMs) },
|
||||
);
|
||||
return parseDailyStarlanguageText(result.text);
|
||||
}
|
||||
|
||||
function unavailable(reason: string) {
|
||||
return NextResponse.json({
|
||||
status: "ok",
|
||||
card: transitCard ?? pickCard(profile, today),
|
||||
source: transitCard ? "jyotish_api_transit_lite" : "calculation_lite",
|
||||
status: "unavailable",
|
||||
reason,
|
||||
claim_status: "exploratory_unvalidated",
|
||||
boundary: "not_deterministic_prediction",
|
||||
});
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const supabase = await createServerSupabaseClient();
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
if (!user) return NextResponse.json({ status: "unauthenticated" }, { status: 401 });
|
||||
|
||||
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 payload = await dailyProfilePayload(profile, today).catch(() => null);
|
||||
if (!payload) return unavailable("birth_profile_incomplete");
|
||||
|
||||
const key = dailyStarlanguageCacheKey(user.id, JSON.stringify(payload), today);
|
||||
const cached = readCache(key, today);
|
||||
if (cached) {
|
||||
return NextResponse.json({
|
||||
status: "ok",
|
||||
card: cached,
|
||||
source: "agent_cache",
|
||||
claim_status: "exploratory_unvalidated",
|
||||
boundary: "not_deterministic_prediction",
|
||||
});
|
||||
}
|
||||
|
||||
const inFlight = pending().get(key) ?? generateCard(payload, profile, today)
|
||||
.catch((error: unknown) => {
|
||||
console.warn("daily_starlanguage_generation_failed", error);
|
||||
return null;
|
||||
})
|
||||
.finally(() => pending().delete(key));
|
||||
pending().set(key, inFlight);
|
||||
|
||||
const card = await inFlight;
|
||||
if (!card) return unavailable("agent_generation_failed");
|
||||
writeCache(key, today, card);
|
||||
return NextResponse.json({
|
||||
status: "ok",
|
||||
card,
|
||||
source: "agent",
|
||||
claim_status: "exploratory_unvalidated",
|
||||
boundary: "not_deterministic_prediction",
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user