feat(home): write the daily card with the Agent and vary the starter heading
Independent Staging Quality Gate / validate (push) Failing after 11m34s
Independent Staging Quality Gate / publish (push) Has been skipped

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:
Jesse_Chen
2026-08-17 22:25:52 +08:00
parent fd415e1d11
commit f6715ee4bf
10 changed files with 671 additions and 90 deletions
+147 -55
View File
@@ -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",
});