fix(home): serve today's starlanguage from daily guidance
Independent Staging Quality Gate / validate (push) Failing after 8m9s
Independent Staging Quality Gate / publish (push) Has been skipped

The homepage was waiting on a full chart plus four extra engines, so the card timed out and showed the failure copy on every visit.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-08-19 15:35:38 +08:00
co-authored by Cursor
parent e635c40224
commit 8db716ca36
10 changed files with 234 additions and 91 deletions
@@ -4,8 +4,8 @@ import {
composeDailyStarlanguageCard,
dailyStarlanguageCacheKey,
dailyStarlanguageCardSchema,
dailyStarlanguageEvidence,
dailyStarlanguagePrompt,
evidenceFromDailyGuidance,
parseDailyStarlanguageText,
type DailyStarlanguageCard,
type DailyStarlanguageEvidence,
@@ -19,7 +19,7 @@ import { createServerSupabaseClient } from "@/lib/supabase/server";
import { getDailyStarlanguageAgent } from "@/mastra";
export const runtime = "nodejs";
export const maxDuration = 30;
export const maxDuration = 20;
type Profile = GlobalBirthProfile & { birthTimeStatus?: string; timezoneId?: string };
type BirthPayload = NonNullable<Awaited<ReturnType<typeof dailyProfilePayload>>>;
@@ -35,7 +35,7 @@ type Generated =
| { readonly kind: "failed"; readonly reason: "chart_unavailable" };
const jyotishApiBase = process.env.JYOTISH_API_BASE ?? "http://127.0.0.1:5200";
const engineTimeoutMs = 20_000;
const engineTimeoutMs = 8_000;
const agentTimeoutMs = 45_000;
const cacheLimit = 500;
@@ -89,37 +89,17 @@ async function fetchEngine(path: string, body: Record<string, unknown>) {
return await response.json() as Record<string, unknown>;
}
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 collectLayers(payload: BirthPayload, today: string) {
const chart = await fetchEngine("/api/chart", payload).catch((error: unknown) => {
console.warn("daily_starlanguage_chart_unavailable", error);
async function loadDailyGuidance(payload: BirthPayload, today: string) {
const packet = await fetchEngine("/api/daily_guidance", {
...payload,
date: today,
reference_date: today,
}).catch((error: unknown) => {
console.warn("daily_starlanguage_guidance_unavailable", error);
return null;
});
const points = chart === null ? null : chartPoints(chart);
if (chart === null || !points) return null;
const tomorrow = new Date(`${today}T00:00:00.000Z`);
tomorrow.setUTCDate(tomorrow.getUTCDate() + 1);
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 };
if (!packet || packet.success !== true) return null;
return packet;
}
async function polishWithAgent(evidence: DailyStarlanguageEvidence): Promise<DailyStarlanguageCard | null> {
@@ -158,9 +138,10 @@ function scheduleAgentPolish(key: string, today: string, evidence: DailyStarlang
}
async function generateCard(payload: BirthPayload, profile: Profile, today: string): Promise<Generated> {
const layers = await collectLayers(payload, today);
if (!layers) return { kind: "failed", reason: "chart_unavailable" };
const evidence = dailyStarlanguageEvidence(layers, today, profile.birthTimeStatus === "confirmed");
const packet = await loadDailyGuidance(payload, today);
if (!packet) return { kind: "failed", reason: "chart_unavailable" };
const evidence = evidenceFromDailyGuidance(packet, today, profile.birthTimeStatus === "confirmed");
if (!evidence) return { kind: "failed", reason: "chart_unavailable" };
return {
kind: "card",
card: composeDailyStarlanguageCard(evidence),
+14
View File
@@ -1297,6 +1297,20 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
text-wrap: pretty;
}
.product-entrypoint-copy p[aria-busy="true"] {
color: var(--color-ink-tertiary);
}
@media (prefers-reduced-motion: no-preference) {
.product-entrypoint-copy p[aria-busy="true"] {
animation: daily-starlanguage-pending 1.6s ease-in-out infinite;
}
}
@keyframes daily-starlanguage-pending {
50% { opacity: .55; }
}
.product-entrypoint-footer {
display: flex;
align-items: flex-end;
+15 -8
View File
@@ -1274,12 +1274,18 @@ export default function Home() {
const dailyStarlanguageFingerprint = dailyStarlanguageProfileKey(profile);
const starterThemes = personalChartAvailable ? themes : generalGuidedJyotishTopics;
const dailyStarlanguageQuestion = !personalChartAvailable
? "请帮我看一下今天的运势,重点告诉我适合推进什么、需要注意什么。"
: dailyStarlanguage.kind === "unavailable"
? "从今日问起"
: "深入看今日";
const dailyStarlanguageTrend = dailyStarlanguage.kind === "ready"
? dailyStarlanguage.card.trend
: dailyStarlanguage.kind === "pending"
? "正在结合你的星盘写今天的星语。"
: "今天的星语没能生成。点这里从今日问起;不会用通用文案顶替。";
? "正在写今天的星语。"
: "今天的星语还没写出来。";
const dailyStarlanguageAction = dailyStarlanguage.kind === "ready" ? dailyStarlanguage.card.action : "";
const dailyStarlanguageBusy = personalChartAvailable && dailyStarlanguage.kind === "pending";
const onboardingPending = profileComplete && !onboarding && !onboardingError;
const currentOnboardingMessage = onboardingJustCompleted
? startGreeting || completedOnboardingMessage(profileDraft.name.trim())
@@ -2448,9 +2454,7 @@ export default function Home() {
function draftDailyStarlanguageQuestion() {
chooseSuggestedQuestion(
personalChartAvailable
? "深入看今日"
: "请帮我看一下今天的运势,重点告诉我适合推进什么、需要注意什么。",
dailyStarlanguageQuestion,
"timing",
personalChartAvailable ? "daily_starlanguage" : null,
);
@@ -3550,17 +3554,20 @@ export default function Home() {
<button
className="product-entrypoint-hitarea"
type="button"
aria-label={personalChartAvailable ? "深入看今日" : "查看今日运势"}
aria-label={personalChartAvailable ? dailyStarlanguageQuestion : "查看今日运势"}
disabled={productEntrypointsDisabled}
onClick={draftDailyStarlanguageQuestion}
/>
<div className="product-entrypoint-copy">
<h2 id="daily-starlanguage-title">{personalChartAvailable ? "今日星语" : "每日运势"}</h2>
<p aria-busy={personalChartAvailable && dailyStarlanguage.kind === "pending"}>{personalChartAvailable ? dailyStarlanguageTrend : "看看今天的整体节奏、适合推进的事和需要留意的地方。"}</p>
<p
aria-busy={dailyStarlanguageBusy}
role={personalChartAvailable && dailyStarlanguage.kind !== "ready" ? "status" : undefined}
>{personalChartAvailable ? dailyStarlanguageTrend : "看看今天的整体节奏、适合推进的事和需要留意的地方。"}</p>
</div>
<div className="product-entrypoint-footer">
<small>{personalChartAvailable ? dailyStarlanguageAction : "不支持的个人判断会明确说明,不会补造出生时间。"}</small>
<span className="product-entrypoint-action" aria-hidden="true">{personalChartAvailable ? "深入看今日" : "查看今日运势"} <ArrowUpRight className="starter-arrow" /></span>
<span className="product-entrypoint-action" aria-hidden="true">{personalChartAvailable ? dailyStarlanguageQuestion : "查看今日运势"} <ArrowUpRight className="starter-arrow" /></span>
</div>
</article>
<article className="birth-rectification-card product-entrypoint-card" aria-labelledby="birth-rectification-title">
+47 -6
View File
@@ -11,6 +11,8 @@ export type DailyStarlanguageEvidence = {
readonly birthTimeVerified: boolean;
readonly ascendantSign: string | null;
readonly moonSign: string | null;
readonly transitMoonSign: string | null;
readonly transitMoonHouse: number | null;
readonly vimshottari: {
readonly mahadasha: string | null;
readonly antardasha: string | null;
@@ -161,6 +163,8 @@ export function dailyStarlanguageEvidence(
birthTimeVerified,
ascendantSign: signOf(asRecord(chart)?.ascendant),
moonSign: planetSign(chart, "Moon"),
transitMoonSign: null,
transitMoonHouse: null,
vimshottari,
narayanaSign: narayana,
divisional,
@@ -171,6 +175,42 @@ export function dailyStarlanguageEvidence(
};
}
export function evidenceFromDailyGuidance(
packet: Record<string, unknown>,
today: string,
birthTimeVerified: boolean,
): DailyStarlanguageEvidence | null {
if (packet.success !== true) return null;
const moonHouse = asCount(packet.moon_house);
const transitMoonHouse = moonHouse !== null && moonHouse >= 1 && moonHouse <= 12 ? moonHouse : null;
const mahadasha = asText(packet.mahadasha_lord);
const transitMoonSign = asText(packet.moon_sign);
if (!transitMoonHouse && !mahadasha && !transitMoonSign) return null;
return {
today: asText(packet.date) ?? today,
birthTimeVerified,
ascendantSign: asText(packet.ascendant_sign),
moonSign: asText(packet.natal_moon_sign),
transitMoonSign,
transitMoonHouse,
vimshottari: mahadasha
? { mahadasha, antardasha: null, remainingDays: null, nakshatra: null }
: null,
narayanaSign: null,
divisional: [],
transit: transitMoonSign
? { totalTriggers: transitMoonHouse ? 1 : 0, top: [`Moon → ${transitMoonSign}`] }
: null,
functionalBenefics: [],
functionalMalefics: [],
missingLayers: [
mahadasha ? "" : "Vimshottari",
"Narayana",
"分盘",
].filter((layer) => layer.length > 0),
};
}
export function dailyStarlanguagePrompt(evidence: DailyStarlanguageEvidence): string {
return [
`今天的日期:${evidence.today}`,
@@ -279,8 +319,9 @@ function planetZh(name: string | null): string | null {
function moonHouse(evidence: DailyStarlanguageEvidence): number | null {
if (!evidence.birthTimeVerified) return null;
if (evidence.transitMoonHouse) return evidence.transitMoonHouse;
const asc = evidence.ascendantSign;
const moon = evidence.moonSign;
const moon = evidence.transitMoonSign ?? evidence.moonSign;
if (!asc || !moon) return null;
const ascIndex = SIGNS.indexOf(asc as typeof SIGNS[number]);
const moonIndex = SIGNS.indexOf(moon as typeof SIGNS[number]);
@@ -292,20 +333,20 @@ export function composeDailyStarlanguageCard(evidence: DailyStarlanguageEvidence
const house = moonHouse(evidence);
const theme = house ? HOUSE_THEMES[house] : undefined;
const mahadasha = planetZh(evidence.vimshottari?.mahadasha ?? null);
const triggers = evidence.transit?.totalTriggers ?? 0;
const hasTransitWindow = Boolean(evidence.transitMoonSign || (evidence.transit?.totalTriggers ?? 0) > 0);
const trend = theme && mahadasha
? `今天${theme.theme}的节奏更明显,${mahadasha}大运里适合先把一件小事做完。`
: theme
? `今天${theme.theme}的节奏更明显,适合把计划压到一件能完成的事。`
: mahadasha
? `今天按${mahadasha}大运的节奏推进,先把一件主事做小、做完。`
: triggers > 0
? "今天有可以观察的过境窗口,先按你当前节奏推进一件主事。"
: hasTransitWindow
? "今天月亮换了观察窗口,先按你当前节奏推进一件主事。"
: "今天按你本命的节奏稳步推进,把计划压到一件主事。";
const action = theme?.action ?? "把今天要做的事收成一件,做完再看下一件。";
const caution = evidence.birthTimeVerified
? (triggers > 0
? "过境只是观察窗口,先别把它当成今天就会发生的结果。"
? (hasTransitWindow
? "这是今天的观察窗口,先别把它当成今天就会发生的结果。"
: "大运描述的是阶段质地,不是今天的结果。")
: "出生时间尚未确认,这里只看大方向,不依赖具体分钟。";
return {