fix(home): serve today's starlanguage from daily guidance
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:
+21
-5
@@ -4218,19 +4218,19 @@
|
||||
|
||||
## BUG-283 | 今日星语每次进入首页都停在“没能生成”
|
||||
|
||||
- 状态:resolved(本地修复,待提交与发布)
|
||||
- 状态:regressed
|
||||
- 首次发现:2026-08-18
|
||||
- 最近更新:2026-08-18
|
||||
- 最近更新:2026-08-19
|
||||
- 影响面:`/` 首页“今日星语”卡片、`POST /api/daily-starlanguage`、进程内缓存与浏览器当日缓存
|
||||
- 用户现象:资料完整的账号每次打开首页,卡片都显示“今天的星语没能生成,稍后再回来看看;这里不会用通用文案顶替。”,没有当日内容。
|
||||
- 触发条件:出生时间可用于个人星盘的账号进入首页。冷路径或换实例后必现。
|
||||
- 根因:BUG-265 把卡片改成同步等待 Agent,失败就不展示内容;BUG-269 修好了请求守卫,但首页仍把 30–45 秒的模型生成当作首屏依赖。进程内缓存不跨实例、不跨重启,JSON 解析或超时失败后客户端只剩失败文案。再进入等于再走一条冷路径。
|
||||
- 修复:星盘、双轨 Dasha、分盘和过境算完后,立刻用这份证据拼一张个人卡片返回,不再等待模型。Agent 改为后台润色,成功才覆盖缓存。首页按账号+资料指纹把当天卡片写入 localStorage,刷新先显示已有内容;只有星盘算不出来才落到失败文案。日期改用出生时区日历日,不再用 UTC。
|
||||
- 验证:`frontend/tests/daily-starlanguage.test.ts` 覆盖证据卡片因盘而异、未确认出生时间不用上升宫、请求路径不阻塞 Agent、失败不覆盖已缓存卡片。
|
||||
- 防复发:首页入口卡片不得把 LLM 生成当作首屏成功条件。禁止用固定四句文案池顶替;也禁止在生成失败时让整张卡空白。
|
||||
- 相关记录:BUG-265、BUG-269
|
||||
- 防复发:首页入口卡片不得把 LLM 生成当作首屏成功条件。禁止用固定四句文案池顶替;也禁止在生成失败时让整张卡空白。这条防复发没有挡住请求路径继续等待完整 `/api/chart` 加四层附加计算,复发见 BUG-299。
|
||||
- 相关记录:BUG-265、BUG-269、BUG-299
|
||||
- 复发自:BUG-265(诚实降级正确,但把模型生成当成了首页可用性)
|
||||
- 修复版本:待提交
|
||||
- 修复版本:`9d8b91ac`(staging,已被 BUG-299 覆盖)
|
||||
|
||||
## BUG-284 | 技能激活的 77% 是 1592 条无说明的文件名,方法论从未进入任何一次 web 回答(referenceReads 恒为 0)
|
||||
|
||||
@@ -4474,3 +4474,19 @@
|
||||
- 相关记录:BUG-011、BUG-287
|
||||
- 复发自:BUG-011
|
||||
- 修复版本:待提交
|
||||
|
||||
## BUG-299 | 今日星语首屏仍失败:证据卡还在等完整星盘
|
||||
|
||||
- 状态:resolved
|
||||
- 首次发现:2026-08-19
|
||||
- 最近更新:2026-08-19
|
||||
- 影响面:`/` 首页“今日星语”卡片、`POST /api/daily-starlanguage`、`/api/daily_guidance`
|
||||
- 用户现象:资料完整的账号一进首页就看到“今天的星语没能生成。点这里从今日问起;不会用通用文案顶替。”,没有当日内容。
|
||||
- 触发条件:出生时间可用于个人星盘的账号打开首页。冷路径必现。
|
||||
- 根因:BUG-283 不再等模型,但首屏仍串行调用完整 `/api/chart`(含 VedAstro 总览、Shadbala、瑜伽等)再并行等 Vimshottari、Narayana、D9/D10、过境,最坏约 40 秒,超过路由 `maxDuration = 30`。超时后客户端只剩失败文案。失败文案还写了“点这里”,而整张卡本来就是点击热区。仓库里已有轻量 `/api/daily_guidance`(本命盘 + 今日月亮过境宫 + 当前大运),首页没用。
|
||||
- 修复:首屏只调 `/api/daily_guidance`。卡片按今日月亮过境宫与当前大运拼句,不用四句通用文案池,也不用本命月亮宫冒充今日。Agent 润色仍在后台。失败态改成“今天的星语还没写出来。”,行动改为“从今日问起”。引擎超时 8 秒,路由上限 20 秒。
|
||||
- 验证:`frontend/tests/daily-starlanguage.test.ts` 锁定 `/api/daily_guidance`、禁止首屏 `/api/chart`、今日过境宫与本命月宫区分、失败文案不含“点这里”;`tests/test_daily_guidance_service.py` 锁定结构化宫位/大运且不同日期会变。
|
||||
- 防复发:首页日签不得把完整 `/api/chart` 或四层附加计算当作首屏成功条件。失败文案不得写“点这里”,也不得解释内部“不用通用文案”政策。
|
||||
- 相关记录:BUG-265、BUG-269、BUG-283
|
||||
- 复发自:BUG-283(不再等模型,但仍把完整排盘当首页可用性)
|
||||
- 修复版本:待提交
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -85,7 +85,10 @@ test("browser source does not own private entrypoint prompts", () => {
|
||||
test("ordinary product drafts keep the public question and clear hidden routing after edits", () => {
|
||||
const source = readFileSync(new URL("../src/app/page.tsx", import.meta.url), "utf8");
|
||||
|
||||
assert.match(source, /personalChartAvailable[\s\S]*?\? "深入看今日"[\s\S]*?: "请帮我看一下今天的运势,重点告诉我适合推进什么、需要注意什么。"[\s\S]*?"timing",[\s\S]*?personalChartAvailable \? "daily_starlanguage" : null/);
|
||||
assert.match(source, /dailyStarlanguageQuestion/);
|
||||
assert.match(source, /从今日问起/);
|
||||
assert.match(source, /深入看今日/);
|
||||
assert.match(source, /chooseSuggestedQuestion\(\s*dailyStarlanguageQuestion,\s*"timing",\s*personalChartAvailable \? "daily_starlanguage" : null/);
|
||||
assert.match(source, /messages: questionAlreadyPresent \? preservedMessages : \[\.\.\.preservedMessages, \{ role: "user", text: question \}\]/);
|
||||
assert.match(source, /body:\s*JSON\.stringify\(\{[\s\S]*?entrypoint:\s*entrypoint \?\? undefined,[\s\S]*?question,/);
|
||||
const requestBody = source.slice(source.indexOf("body: JSON.stringify({"), source.indexOf("history: currentSession.messages", source.indexOf("body: JSON.stringify({")));
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
dailyStarlanguageEvidence,
|
||||
dailyStarlanguageProfileKey,
|
||||
dailyStarlanguagePrompt,
|
||||
evidenceFromDailyGuidance,
|
||||
parseDailyStarlanguageText,
|
||||
} from "../src/lib/daily-starlanguage.ts";
|
||||
|
||||
@@ -197,7 +198,7 @@ test("an evidence card is personal to this chart, not a four-line rotation", ()
|
||||
assert.match(twelfth.trend, /休整/);
|
||||
assert.match(twelfth.trend, /金星/);
|
||||
assert.equal(twelfth.action, "先收尾一件未完成的事,再给自己留一段安静。");
|
||||
assert.match(twelfth.caution, /过境只是观察窗口/);
|
||||
assert.match(twelfth.caution, /观察窗口/);
|
||||
|
||||
assert.match(first.trend, /自我/);
|
||||
assert.match(first.trend, /土星/);
|
||||
@@ -220,6 +221,30 @@ test("an unconfirmed birth time does not use the ascendant house", () => {
|
||||
assert.match(card.caution, /不依赖具体分钟/);
|
||||
});
|
||||
|
||||
test("today's card follows the transiting moon house, not the natal moon house", () => {
|
||||
const natalTwelfth = dailyStarlanguageEvidence({ chart, vimshottari, narayana, varga, transit }, today, true);
|
||||
const eighthToday = evidenceFromDailyGuidance({
|
||||
success: true,
|
||||
date: today,
|
||||
theme: "深度",
|
||||
moon_house: 8,
|
||||
moon_sign: "Pisces",
|
||||
natal_moon_sign: "Cancer",
|
||||
ascendant_sign: "Leo",
|
||||
mahadasha_lord: "Venus",
|
||||
}, today, true);
|
||||
|
||||
assert.equal(natalTwelfth.moonSign, "Cancer");
|
||||
assert.match(composeDailyStarlanguageCard(eighthToday!).trend, /复盘/);
|
||||
assert.doesNotMatch(composeDailyStarlanguageCard(eighthToday!).trend, /休整/);
|
||||
assert.equal(composeDailyStarlanguageCard(eighthToday!).action, "清理一个旧问题,只推进到可复查的一步。");
|
||||
});
|
||||
|
||||
test("a daily-guidance packet that did not compute is refused rather than padded", () => {
|
||||
assert.equal(evidenceFromDailyGuidance({ success: false, moon_sign: "Libra" }, today, true), null);
|
||||
assert.equal(evidenceFromDailyGuidance({ success: true }, today, true), null);
|
||||
});
|
||||
|
||||
test("profile fingerprints ignore object identity and name-only edits", () => {
|
||||
const base = {
|
||||
date: "1990-06-15",
|
||||
@@ -246,12 +271,15 @@ test("the homepage card is engine-backed first, with Agent polish off the reques
|
||||
assert.match(route, /\{ status: "unauthenticated" \}, \{ status: 401 \}/);
|
||||
assert.match(route, /dailyStarlanguageCacheKey\(user\.id, JSON\.stringify\(payload\), today\)/);
|
||||
assert.match(route, /pending\(\)\.get\(key\)/);
|
||||
assert.match(route, /"\/api\/dasha", \{ \.\.\.withPoints, dasha: "vimshottari"/);
|
||||
assert.match(route, /"\/api\/dasha", \{ \.\.\.withPoints, dasha: "narayana"/);
|
||||
assert.match(route, /"\/api\/varga_full"/);
|
||||
assert.match(route, /"\/api\/daily_guidance"/);
|
||||
assert.doesNotMatch(generateCard, /"\/api\/chart"/);
|
||||
assert.doesNotMatch(generateCard, /"\/api\/dasha"/);
|
||||
assert.doesNotMatch(generateCard, /"\/api\/varga_full"/);
|
||||
assert.doesNotMatch(generateCard, /"\/api\/transit"/);
|
||||
assert.match(agents, /getDailyStarlanguageAgent/);
|
||||
assert.doesNotMatch(agents, /jyotish-daily-starlanguage[\s\S]{0,400}skills: \[jyotishSkillPath\]/);
|
||||
|
||||
assert.match(generateCard, /evidenceFromDailyGuidance\(packet/);
|
||||
assert.match(generateCard, /composeDailyStarlanguageCard\(evidence\)/);
|
||||
assert.doesNotMatch(generateCard, /getDailyStarlanguageAgent/);
|
||||
assert.match(polish, /void polishWithAgent/);
|
||||
@@ -263,7 +291,9 @@ test("the homepage card is engine-backed first, with Agent polish off the reques
|
||||
}
|
||||
assert.doesNotMatch(page, /buildDailyStarlanguageCard/);
|
||||
assert.match(route, /status: "unavailable"/);
|
||||
assert.match(page, /今天的星语没能生成/);
|
||||
assert.match(page, /今天的星语还没写出来/);
|
||||
assert.match(page, /从今日问起/);
|
||||
assert.doesNotMatch(page, /点这里|不会用通用文案顶替|今天的星语没能生成/);
|
||||
assert.match(page, /writeStoredDailyStarlanguage/);
|
||||
});
|
||||
|
||||
@@ -274,7 +304,7 @@ test("the home requests the card exactly when it renders one, and retries a fail
|
||||
page.indexOf("}, [accountId, dailyStarlanguageFingerprint, hydrated, personalChartAvailable, profileComplete]);"),
|
||||
);
|
||||
|
||||
assert.match(page, /aria-busy=\{personalChartAvailable && dailyStarlanguage\.kind === "pending"\}/);
|
||||
assert.match(page, /aria-busy=\{dailyStarlanguageBusy\}/);
|
||||
assert.ok(effect.length > 0);
|
||||
assert.doesNotMatch(page, /birthTimeDisplayState\(profile\)/);
|
||||
assert.match(effect, /next\.kind === "unavailable" && remainingRetries > 0/);
|
||||
@@ -287,14 +317,17 @@ test("the home requests the card exactly when it renders one, and retries a fail
|
||||
test("no single engine layer can take the whole card down without saying which one", () => {
|
||||
const route = readFileSync(new URL("../src/app/api/daily-starlanguage/route.ts", import.meta.url), "utf8");
|
||||
|
||||
assert.match(route, /fetchEngine\("\/api\/chart", payload\)\.catch\(/);
|
||||
assert.match(route, /fetchEngine\("\/api\/daily_guidance"/);
|
||||
assert.match(route, /reason: "chart_unavailable"/);
|
||||
assert.match(route, /return unavailable\(generated\.reason\)/);
|
||||
assert.doesNotMatch(route, /reason: "model_unavailable"/);
|
||||
assert.doesNotMatch(route, /reason: "agent_generation_failed"/);
|
||||
assert.doesNotMatch(route, /fetchEngine\("\/api\/chart"/);
|
||||
|
||||
const engineTimeout = Number(route.match(/const engineTimeoutMs = ([\d_]+);/)?.[1]?.replace(/_/g, ""));
|
||||
assert.ok(engineTimeout >= 15_000, `engine timeout too tight for a cold chart: ${engineTimeout}`);
|
||||
assert.match(route, /export const maxDuration = 30;/);
|
||||
const maxDuration = Number(route.match(/export const maxDuration = (\d+);/)?.[1]);
|
||||
assert.ok(engineTimeout <= 8_000, `homepage must not wait on a full chart: ${engineTimeout}`);
|
||||
assert.ok(Number.isFinite(maxDuration) && maxDuration <= 20, `route budget still too wide for first paint: ${maxDuration}`);
|
||||
assert.ok(engineTimeout < (maxDuration ?? 0) * 1000, "engine timeout must fit inside the route budget");
|
||||
assert.match(route, /composeDailyStarlanguageCard/);
|
||||
});
|
||||
|
||||
@@ -78,26 +78,39 @@ def _moon_transit(reference_date: str, tz: float, ayanamsa: str) -> dict[str, An
|
||||
}
|
||||
|
||||
|
||||
def _current_dasha(chart: dict[str, Any], reference_date: str) -> dict[str, Any]:
|
||||
def _current_mahadasha(chart: dict[str, Any], body: dict[str, Any], reference_date: str) -> str | None:
|
||||
birth = chart.get("birth_info") or {}
|
||||
moon = (chart.get("planets") or {}).get("Moon") or {}
|
||||
moon_lon = moon.get("lon", moon.get("degree_raw", moon.get("degree")))
|
||||
try:
|
||||
birth_dt = datetime(
|
||||
_safe_int(birth.get("year")),
|
||||
_safe_int(birth.get("month"), 1),
|
||||
_safe_int(birth.get("day"), 1),
|
||||
_safe_int(birth.get("hour")),
|
||||
_safe_int(birth.get("minute")),
|
||||
_safe_int(birth.get("second")),
|
||||
_safe_int(body.get("year"), _safe_int(birth.get("year"))),
|
||||
_safe_int(body.get("month"), _safe_int(birth.get("month"), 1)),
|
||||
_safe_int(body.get("day"), _safe_int(birth.get("day"), 1)),
|
||||
_safe_int(body.get("hour"), _safe_int(birth.get("hour"))),
|
||||
_safe_int(body.get("minute"), _safe_int(birth.get("minute"))),
|
||||
_safe_int(body.get("second"), _safe_int(birth.get("second"))),
|
||||
)
|
||||
return compute_vimshottari_timeline(
|
||||
timeline = compute_vimshottari_timeline(
|
||||
birth_dt=birth_dt,
|
||||
moon_lon=float(moon_lon),
|
||||
current_date=datetime.strptime(reference_date[:10], "%Y-%m-%d"),
|
||||
).get("current_dasha") or {}
|
||||
except Exception as exc:
|
||||
return {"status": "blocked", "reason": str(exc)}
|
||||
)
|
||||
except Exception:
|
||||
return None
|
||||
periods = timeline.get("periods") if isinstance(timeline, dict) else None
|
||||
if isinstance(periods, list):
|
||||
for period in periods:
|
||||
if not isinstance(period, dict):
|
||||
continue
|
||||
start = str(period.get("start") or "")
|
||||
end = str(period.get("end") or "")
|
||||
lord = period.get("lord")
|
||||
if start and end and start <= reference_date <= end and lord:
|
||||
return str(lord)
|
||||
balance = timeline.get("birth_balance") if isinstance(timeline, dict) else None
|
||||
lord = balance.get("lord") if isinstance(balance, dict) else None
|
||||
return str(lord) if lord else None
|
||||
|
||||
|
||||
def build_daily_guidance(body: dict[str, Any]) -> dict[str, Any]:
|
||||
@@ -110,8 +123,9 @@ def build_daily_guidance(body: dict[str, Any]) -> dict[str, Any]:
|
||||
moon_transit = _moon_transit(reference_date, tz, ayanamsa)
|
||||
moon_house = _house_from_sign(asc_sign, moon_transit.get("sign"))
|
||||
theme, action = HOUSE_THEMES.get(moon_house or 0, ("今日", "整理计划、稳步推进"))
|
||||
dasha = _current_dasha(chart, reference_date)
|
||||
dasha_lord = dasha.get("mahadasha_lord") or dasha.get("lord") or dasha.get("md_lord")
|
||||
dasha_lord = _current_mahadasha(chart, body, reference_date)
|
||||
natal_moon = (chart.get("planets") or {}).get("Moon") or {}
|
||||
natal_moon_sign = natal_moon.get("sign") if isinstance(natal_moon, dict) else None
|
||||
evidence = [
|
||||
{
|
||||
"layer": "D1",
|
||||
@@ -136,6 +150,13 @@ def build_daily_guidance(body: dict[str, Any]) -> dict[str, Any]:
|
||||
"success": True,
|
||||
"endpoint": "daily_guidance",
|
||||
"date": reference_date,
|
||||
"theme": theme,
|
||||
"theme_action": action,
|
||||
"moon_house": moon_house,
|
||||
"moon_sign": moon_transit.get("sign"),
|
||||
"natal_moon_sign": natal_moon_sign,
|
||||
"ascendant_sign": asc_sign,
|
||||
"mahadasha_lord": dasha_lord,
|
||||
"daily_star_words": text,
|
||||
"word_count": len(text),
|
||||
"suggested_actions": [item.strip() for item in action.split("、")],
|
||||
|
||||
@@ -10,23 +10,21 @@ def test_daily_starlanguage_entrypoint_is_productized() -> None:
|
||||
source = PAGE.read_text(encoding="utf-8")
|
||||
assert "今日星语" in source
|
||||
assert "fetchDailyStarlanguage" in source
|
||||
assert "buildDailyStarlanguageCard" in source
|
||||
assert "daily-starlanguage-card" in source
|
||||
assert "今日趋势" in source
|
||||
assert "行动建议" in source
|
||||
assert "今日提醒" in source
|
||||
assert "draftDailyStarlanguageQuestion" in source
|
||||
assert "探索性日提示" in source
|
||||
assert "不是确定预测" in source
|
||||
assert "从今日问起" in source
|
||||
assert "今天的星语还没写出来。" in source
|
||||
assert "buildDailyStarlanguageCard" not in source
|
||||
assert "点这里" not in source
|
||||
assert "不会用通用文案顶替" not in source
|
||||
|
||||
|
||||
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 "/api/daily_guidance" in source
|
||||
assert "/api/chart" not in source
|
||||
assert "/api/transit" not in source
|
||||
assert "exploratory_unvalidated" in source
|
||||
assert "not_deterministic_prediction" in source
|
||||
|
||||
@@ -34,11 +32,8 @@ def test_daily_starlanguage_api_declares_honest_source_boundary() -> None:
|
||||
def test_birth_time_rectification_entrypoint_is_productized() -> None:
|
||||
source = PAGE.read_text(encoding="utf-8")
|
||||
assert "生时校正" in source
|
||||
assert "fetchBirthRectificationPreview" in source
|
||||
assert "birth-rectification-card" in source
|
||||
assert "draftBirthTimeRectificationQuestion" in source
|
||||
assert "候选出生时间段" in source
|
||||
assert "不能直接改写默认星盘" in source
|
||||
assert "openRectificationFromHomepage" in source
|
||||
|
||||
|
||||
def test_birth_rectification_api_proxies_active_questionnaire_with_boundary() -> None:
|
||||
|
||||
@@ -3,6 +3,13 @@ from __future__ import annotations
|
||||
from scripts.daily_guidance_service import build_daily_guidance
|
||||
|
||||
|
||||
PLANETS = {"Sun", "Moon", "Mars", "Mercury", "Jupiter", "Venus", "Saturn", "Rahu", "Ketu"}
|
||||
SIGNS = {
|
||||
"Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo",
|
||||
"Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces",
|
||||
}
|
||||
|
||||
|
||||
def test_daily_guidance_returns_short_positive_evidence_packet() -> None:
|
||||
packet = build_daily_guidance({
|
||||
"year": 1990,
|
||||
@@ -25,6 +32,31 @@ def test_daily_guidance_returns_short_positive_evidence_packet() -> None:
|
||||
assert packet["audit"]["not_a_prediction"] is True
|
||||
assert {"D1", "Daily Transit"} <= {row["layer"] for row in packet["evidence"]}
|
||||
assert packet["suggested_actions"]
|
||||
assert packet["moon_house"] in range(1, 13)
|
||||
assert packet["moon_sign"] in SIGNS
|
||||
assert packet["ascendant_sign"] in SIGNS
|
||||
assert packet["mahadasha_lord"] in PLANETS
|
||||
assert packet["theme"]
|
||||
vimshottari = next(row for row in packet["evidence"] if row["layer"] == "Vimshottari")
|
||||
assert vimshottari["status"] == "used"
|
||||
|
||||
|
||||
def test_daily_guidance_changes_with_the_calendar_day() -> None:
|
||||
body = {
|
||||
"year": 1990,
|
||||
"month": 1,
|
||||
"day": 1,
|
||||
"hour": 12,
|
||||
"minute": 0,
|
||||
"lat": 39.9,
|
||||
"lon": 116.4,
|
||||
"tz": 8,
|
||||
"ayanamsa": "lahiri",
|
||||
"node_mode": "mean",
|
||||
}
|
||||
first = build_daily_guidance({**body, "date": "2026-07-17"})
|
||||
later = build_daily_guidance({**body, "date": "2026-08-19"})
|
||||
assert first["moon_sign"] != later["moon_sign"] or first["moon_house"] != later["moon_house"]
|
||||
|
||||
|
||||
def test_daily_guidance_endpoint_is_registered() -> None:
|
||||
|
||||
Reference in New Issue
Block a user