feat: back daily starlanguage with API card

This commit is contained in:
732642856
2026-07-18 04:17:08 +08:00
parent a0f8db36a7
commit a9f354232a
3 changed files with 82 additions and 1 deletions
@@ -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",
});
}
+37 -1
View File
@@ -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<Profile>(emptyProfile);
const [synastryReportCard, setSynastryReportCard] = useState<SynastryReportCard | null>(null);
const [synastryHistory, setSynastryHistory] = useState<SynastryReportCard[]>([]);
const [dailyStarlanguageCard, setDailyStarlanguageCard] = useState<DailyStarlanguageCard | null>(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" });
@@ -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