From a9f354232acb55d1da67f79ff61beca2d07b4c84 Mon Sep 17 00:00:00 2001 From: 732642856 <732642856@qq.com> Date: Sat, 18 Jul 2026 04:17:08 +0800 Subject: [PATCH] feat: back daily starlanguage with API card --- .../src/app/api/daily-starlanguage/route.ts | 35 +++++++++++++++++ frontend/src/app/page.tsx | 38 ++++++++++++++++++- ...est_daily_and_rectification_entrypoints.py | 10 +++++ 3 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 frontend/src/app/api/daily-starlanguage/route.ts diff --git a/frontend/src/app/api/daily-starlanguage/route.ts b/frontend/src/app/api/daily-starlanguage/route.ts new file mode 100644 index 00000000..1a9394eb --- /dev/null +++ b/frontend/src/app/api/daily-starlanguage/route.ts @@ -0,0 +1,35 @@ +import { NextResponse } from "next/server"; + +type Profile = { + name?: string; + date?: string; + time?: string; + provinceCode?: string; + cityCode?: string; +}; + +const cards = [ + { trend: "先收束,再推进。适合把一个悬而未决的问题拆小。", action: "选一件最重要的事,给它留出 45 分钟不被打断的时间。", caution: "避免在情绪最满时做承诺。" }, + { trend: "适合整理关系与边界。越清楚,越不容易被外界节奏带走。", action: "把今天要回复的人和要推迟的事分开列出来。", caution: "不要把暂时的沉默误读成最终答案。" }, + { trend: "执行力比灵感更重要。小步完成会比大计划更有力量。", action: "先完成一个可交付版本,再考虑优化。", caution: "别让完美感拖慢开始。" }, + { trend: "适合观察资源流向:时间、注意力、金钱都算。", action: "检查一个正在消耗你的习惯,并给它设上限。", caution: "不要为了短期安心做长期成本高的选择。" }, +]; + +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]; +} + +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); + return NextResponse.json({ + status: "ok", + card: pickCard(profile, today), + source: "calculation_lite", + claim_status: "exploratory_unvalidated", + boundary: "not_deterministic_prediction", + }); +} diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index 749990d0..09236803 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -74,6 +74,13 @@ type OnboardingContent = { greeting: string; suggestions: OnboardingSuggestion[] type OnboardingStep = "name" | "birth" | "place"; type GreetingPeriod = "morning" | "noon" | "afternoon" | "evening" | "late-night"; type DailyStarlanguageCard = { trend: string; action: string; caution: string }; +type DailyStarlanguageApiResponse = { + status?: "ok"; + card?: DailyStarlanguageCard; + source?: "calculation_lite"; + claim_status?: "exploratory_unvalidated"; + boundary?: "not_deterministic_prediction"; +}; type SessionReadResult = { readonly sessions: ChatSession[]; readonly fallbackSessionIds: string[] }; type PendingConsultation = { readonly requestId: string; @@ -341,6 +348,18 @@ function buildDailyStarlanguageCard(profile: Profile) { return dailyStarlanguageCards[index]; } +async function fetchDailyStarlanguage(profile: Profile) { + const response = await fetch("/api/daily-starlanguage", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ profile }), + }); + if (!response.ok) throw new Error("daily_starlanguage_unavailable"); + const payload = await response.json().catch(() => null) as DailyStarlanguageApiResponse | null; + if (payload?.status !== "ok" || !payload.card) throw new Error("daily_starlanguage_invalid"); + return payload.card; +} + function buildBirthTimeRectificationQuestion(profile: Profile) { return [ `请为${profile.name || "我"}做生时校正辅助。`, @@ -617,6 +636,7 @@ export default function Home() { const [otherProfileDraft, setOtherProfileDraft] = useState(emptyProfile); const [synastryReportCard, setSynastryReportCard] = useState(null); const [synastryHistory, setSynastryHistory] = useState([]); + const [dailyStarlanguageCard, setDailyStarlanguageCard] = useState(null); const [profileOpen, setProfileOpen] = useState(false); const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false); const [profileNotice, setProfileNotice] = useState(""); @@ -732,7 +752,7 @@ export default function Home() { }, [accountId, profile]); const profileComplete = isProfileComplete(profile); - const dailyStarlanguage = profileComplete ? buildDailyStarlanguageCard(profile) : null; + const dailyStarlanguage = dailyStarlanguageCard ?? (profileComplete ? buildDailyStarlanguageCard(profile) : null); const onboardingPending = profileComplete && !onboarding && !onboardingError; const currentOnboardingMessage = onboardingJustCompleted ? startGreeting || completedOnboardingMessage(profileDraft.name.trim()) @@ -967,6 +987,22 @@ export default function Home() { }; }, [accountId, hydrated, onboarding, onboardingError, profile.name, profileComplete, startGreeting]); + useEffect(() => { + if (!hydrated || !profileComplete) return; + let cancelled = false; + setDailyStarlanguageCard(null); + void fetchDailyStarlanguage(profile) + .then((card) => { + if (!cancelled) setDailyStarlanguageCard(card); + }) + .catch(() => { + if (!cancelled) setDailyStarlanguageCard(buildDailyStarlanguageCard(profile)); + }); + return () => { + cancelled = true; + }; + }, [hydrated, profile.date, profile.time, profile.provinceCode, profile.cityCode, profileComplete]); + useEffect(() => { const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches; conversationEnd.current?.scrollIntoView({ behavior: isLoading || reduceMotion ? "auto" : "smooth", block: "end" }); diff --git a/tests/test_daily_and_rectification_entrypoints.py b/tests/test_daily_and_rectification_entrypoints.py index e2357789..7c2613b9 100644 --- a/tests/test_daily_and_rectification_entrypoints.py +++ b/tests/test_daily_and_rectification_entrypoints.py @@ -2,11 +2,13 @@ from pathlib import Path PAGE = Path("frontend/src/app/page.tsx") +DAILY_ROUTE = Path("frontend/src/app/api/daily-starlanguage/route.ts") 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 @@ -17,6 +19,14 @@ def test_daily_starlanguage_entrypoint_is_productized() -> None: assert "不是确定预测" 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 "calculation_lite" in source + assert "exploratory_unvalidated" in source + assert "not_deterministic_prediction" in source + + def test_birth_time_rectification_entrypoint_is_productized() -> None: source = PAGE.read_text(encoding="utf-8") assert "生时校正" in source