From 921992a492e9c164566ac6d426a2bd30c02a43c8 Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Wed, 30 Sep 2026 01:11:47 +0800 Subject: [PATCH 1/4] =?UTF-8?q?fix(home,chart,ephemeris):=20drop=20the=20u?= =?UTF-8?q?nfinished-rectification=20hint,=20full=20chart-type=20captions,?= =?UTF-8?q?=20orbit=20instead=20of=20=E8=BF=98=E6=B2=A1=E6=8B=BF=E5=88=B0,?= =?UTF-8?q?=20constellation=20icon=20(BUG-1111..1114)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE --- docs/BUG_HISTORY.md | 41 +++++++++++++++++++ frontend/DESIGN.md | 6 ++- frontend/docs/VOICE.md | 4 ++ frontend/src/app/globals.css | 24 +++++++---- .../components/birth-sky/birth-sky-entry.tsx | 21 +++++++++- .../components/chart-page/chart-dasha-tab.tsx | 5 ++- .../chart-page/chart-page-skeleton.tsx | 8 ++-- frontend/src/components/orbit-waiting.tsx | 17 ++++++++ .../src/components/secondary-page-shell.tsx | 3 +- frontend/src/components/starter-home.tsx | 17 +++----- frontend/tests/chart-type-picker.test.tsx | 4 +- .../tests/rectification-agentic-entry.test.ts | 7 +++- .../starter-home-polish-20260926.test.tsx | 21 ++++++---- .../tests/surface-feedback-20260929.test.tsx | 31 ++++++++++++++ 14 files changed, 171 insertions(+), 38 deletions(-) create mode 100644 frontend/src/components/orbit-waiting.tsx diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index 92863c06..b935f482 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -14942,3 +14942,44 @@ - 防复发:真机复测后更新本条状态;若仍复现需真机录屏与 Safari 版本。 - 相关记录:TASK-report-english-edition-20260929 - 修复版本:分支 `codex/surface-feedback-20260929` + +## BUG-1111 | 首页常驻「上次那次校正还没完成,可以在历史对话里接着做。」 + +- 状态:resolved(代码 + 回归测试) +- 首次发现 / 最近更新:2026-09-30 / 2026-09-30 +- 影响面:`components/starter-home.tsx::rectificationEntryHint`。 +- 现象:有未完成的校正时首页入口下方一直挂着这句提示;产品要求去掉。 +- 修复:`rectificationEntryHint(rectificationForSelf)` 只保留「生时校正暂时只支持本人。」一句;未完成的校正仍在历史列表里。 +- 验证:`starter-home-polish-20260926.test.tsx`、`rectification-agentic-entry.test.ts`(断言三栏更新)、`surface-feedback-20260929.test.tsx`。 +- 相关记录:BUG-1041、TASK-starter-home-polish-20260926 D2 +- 修复版本:分支 `codex/surface-feedback-20260929` + +## BUG-1112 | 星盘类型面板里每格的说明文字被截掉下半截 / 显示不全 + +- 状态:resolved(代码 + 回归测试;iPhone 真机欠) +- 首次发现 / 最近更新:2026-09-30 / 2026-09-30 +- 影响面:`globals.css .chart-page-type-cell small`。 +- 根因:说明行 `white-space: nowrap; overflow: hidden`,行高沿用按钮的紧行高,iPhone 上中文字形下半被裁;「以月亮为第 1 宫」这类长说明只能省略。 +- 修复:说明行行高 1.4、最多两行(line-clamp),格子最小高 56 → 64px。 +- 验证:`chart-type-picker.test.tsx`(三栏)、`surface-feedback-20260929.test.tsx`。 +- 修复版本:分支 `codex/surface-feedback-20260929` + +## BUG-1113 | 星盘、星历加载时写「这一张盘还没拿到。」「这一天的星历还没拿到。」 + +- 状态:resolved(代码 + 回归测试 + 本机手机视口截图) +- 首次发现 / 最近更新:2026-09-30 / 2026-09-30 +- 影响面:新增 `components/orbit-waiting.tsx`;`secondary-page-shell.tsx`、`chart-page-skeleton.tsx`、`chart-dasha-tab.tsx`。 +- 现象:几秒内就会加载好的内容,先用一句「还没拿到」占位,读起来像出错。 +- 修复:统一用首页加载屏的轨道环(40px)作为次级页短等待动画:星盘空盘中央、星历 / 报告列表首屏、Chara 大运栏;原句只留给读屏器(`sr-only` + `role="status"`)。 +- 产品决定:此前 AGENTS §6「揭幕后不得再出现加载动画」只对首页;星盘页 Chara 栏等待改为轨道环属产品本次明确要求,记录在此。 +- 验证:`surface-feedback-20260929.test.tsx`;既有「还没拿到」断言因文本仍在读屏节点里而不变。 +- 修复版本:分支 `codex/surface-feedback-20260929` + +## BUG-1114 | 「那一刻的天空」按钮用的是各平台常见的星芒(AI)图标 + +- 状态:resolved(代码 + 回归测试) +- 首次发现 / 最近更新:2026-09-30 / 2026-09-30 +- 修复:改为自绘「小星座」图标(四颗星以细线相连),与点开后的群星汇聚一致;产品在三个方案中选定。 +- 验证:`surface-feedback-20260929.test.tsx`。 +- 相关记录:BUG-1102 +- 修复版本:分支 `codex/surface-feedback-20260929` diff --git a/frontend/DESIGN.md b/frontend/DESIGN.md index 41f58057..0c514564 100644 --- a/frontend/DESIGN.md +++ b/frontend/DESIGN.md @@ -563,6 +563,10 @@ one skeleton, none of them carrying the sidebar. more. A page's own reassurance copy (the chart page's cost-and-speed eyebrow, for one) stays in the body; it is content, not chrome. +### Short waits on secondary pages (2026-09-30, BUG-1113) + +The chart page, the ephemeris and the report list wait with one motion: the home loading screen's orbit ring at 40px (`OrbitWaiting`), centred in the empty chart grid or in the page. No sentence is shown; the old 「……还没拿到。」 copy is kept only as the screen-reader status. The chart's breathing empty grid stays (BUG-1016 exception). + ### Personal report centre - **Structure:** inside the app shell, not a page of its own. The name 「我的报告」 sits alone in the 46px header. The body opens with the **generate card** (`.report-center-create`, 2026-09-29, product sketch): a centred `--color-canvas` sheet, `--space-6` below the header (BUG-1103: it used to sit flush under it), with a file icon, the title 「完整本命报告」 (what you get), one line 「星盘、力量、大运、年运、瑜伽共 6 章,中英两版;生成后可离开,完成时下方自动出现。」 and the filled 「生成报告」 button (what it does) — title and button no longer say the same thing — the page's only generate entry; the header button was deleted rather than kept as a second one. Below it, 「过往的报告」 heads the **row list** of reports; with none yet, a single quiet line 「还没有个人报告。」 replaces the old second big empty card. The supporting paragraph (「有填报到分钟的出生时间即可生成……」) and the 「共 N 份 · 已完成 N 份」 overview line above it were removed on 2026-09-28 (TASK-self-edit-avatar-menu-20260928 S3, product: 「这里的提示去掉」); the minute requirement still lives in the 生成 button's hover title. @@ -1183,7 +1187,7 @@ D40 / D45 / D60 的表格上方,出生时间不是「已校正」时多一句 ### 位置一:星盘页顶栏按钮 -- 星盘页 `ok` 后立即预取 `/api/birth-sky?subject=<当前人物>` 并在屏外画好 PNG;**画好之后**顶栏右侧才出现一枚描边按钮「那一刻的天空」(` {open ? setWanted(false)} /> : null} ); } + +/** + * A small constellation: four stars joined by thin lines (2026-09-30, product: + * the sparkle icon is on every AI product). It says what the button opens — + * the stars of that moment drawing together — and is drawn here, not taken + * from an icon set. + */ +function ConstellationIcon() { + return ( + + ); +} diff --git a/frontend/src/components/chart-page/chart-dasha-tab.tsx b/frontend/src/components/chart-page/chart-dasha-tab.tsx index 82ee5570..47f7a8c9 100644 --- a/frontend/src/components/chart-page/chart-dasha-tab.tsx +++ b/frontend/src/components/chart-page/chart-dasha-tab.tsx @@ -1,4 +1,5 @@ import { chartLayerFailureCopy } from "@/lib/chart-view-failure"; +import { OrbitWaiting } from "@/components/orbit-waiting"; import type { ChartViewFailureStatus, ChartViewOk } from "@/lib/chart-view-contract"; import { CHARA_ZERO_YEAR_LABEL, CHARA_ZERO_YEAR_NOTE, CHART_VIEW_COPY } from "@/lib/chart-view-labels"; @@ -154,7 +155,9 @@ export function ChartDashaTab({ {charaPending || charaFailure ? (
-

{charaPending ? CHART_VIEW_COPY.waitingLayer : chartLayerFailureCopy(charaFailure!)}

+ {charaPending + ? + :

{chartLayerFailureCopy(charaFailure!)}

}
) : ( diff --git a/frontend/src/components/chart-page/chart-page-skeleton.tsx b/frontend/src/components/chart-page/chart-page-skeleton.tsx index 6eeafc5a..e9b6ee13 100644 --- a/frontend/src/components/chart-page/chart-page-skeleton.tsx +++ b/frontend/src/components/chart-page/chart-page-skeleton.tsx @@ -1,5 +1,6 @@ import { EMPTY_NORTH_INDIAN_CHART, VedicChartSvg } from "@/components/personal-report/vedic-chart-svg"; import { EMPTY_WESTERN_WHEEL, WesternWheelSvg } from "./western-wheel-svg"; +import { OrbitWaiting } from "@/components/orbit-waiting"; /** Only chart positions opt in; the shared page shell remains static. */ export function ChartPageSkeleton({ copy, western = false }: { copy: string; western?: boolean }) { @@ -10,9 +11,10 @@ export function ChartPageSkeleton({ copy, western = false }: { copy: string; wes {western ? : } - {/* Over the empty drawing, not under it: the finished chart has no line - below it any more (BUG-1101), so a line here would make it jump. */} -

{copy}

+ {/* Over the empty drawing, not under it, so the waiting stage and the + finished one are the same height (BUG-1101). An orbit, not a sentence + (2026-09-30); the sentence is for screen readers. */} + ); diff --git a/frontend/src/components/orbit-waiting.tsx b/frontend/src/components/orbit-waiting.tsx new file mode 100644 index 00000000..ec621804 --- /dev/null +++ b/frontend/src/components/orbit-waiting.tsx @@ -0,0 +1,17 @@ +/** + * The one short-wait animation for secondary pages (2026-09-30, product: while + * a chart or a day's ephemeris loads, show motion instead of 「……还没拿到。」). + * It is the home loading screen's orbit ring at a smaller size, so the app has + * one waiting motif. The sentence stays for screen readers only. + */ +export function OrbitWaiting({ label, className = "" }: Readonly<{ label: string; className?: string }>) { + return ( +
+
+ ); +} diff --git a/frontend/src/components/secondary-page-shell.tsx b/frontend/src/components/secondary-page-shell.tsx index 9da7a2bd..5a7cc552 100644 --- a/frontend/src/components/secondary-page-shell.tsx +++ b/frontend/src/components/secondary-page-shell.tsx @@ -3,6 +3,7 @@ import type { ReactNode } from "react"; import { CurrentSubjectSwitcher } from "@/components/current-subject-switcher"; +import { OrbitWaiting } from "@/components/orbit-waiting"; import { SecondaryHeader } from "@/components/secondary-header"; export type SecondaryPageShellProps = { @@ -40,7 +41,7 @@ export function SecondaryPageShell({ actions={actions} />
- {waiting ?

{waiting}

: children} + {waiting ? : children}
); diff --git a/frontend/src/components/starter-home.tsx b/frontend/src/components/starter-home.tsx index 324d28da..0bb1853b 100644 --- a/frontend/src/components/starter-home.tsx +++ b/frontend/src/components/starter-home.tsx @@ -45,16 +45,11 @@ export function StarterGreeting({ starterGreeting, dailyTrend = null, dailyTrend } /* The line under the pills only appears when there is something to do about - it. First runs and finished cases say nothing: the pill already names the - action. The branch reads the entry summary itself, not the derived - start/restart label, and a non-self subject wins because the case summary - belongs to the account owner. */ -export function rectificationEntryHint( - rectificationForSelf: boolean, - summary: RectificationEntrySummary | null, -): string | null { + it: a non-self subject cannot start a rectification. A resumable case says + nothing here any more (2026-09-30, product: 「这个提示可以去掉」); the case + is still in the history list. */ +export function rectificationEntryHint(rectificationForSelf: boolean): string | null { if (!rectificationForSelf) return "生时校正暂时只支持本人。"; - if (summary?.hasResumableCase) return "上次那次校正还没完成,可以在历史对话里接着做。"; return null; } @@ -67,6 +62,7 @@ export type StarterEntriesProps = { readonly rectificationLoading: boolean; readonly rectificationMutationPending: boolean; readonly openRectificationFromHomepage: () => void; + /** Still passed by the page; the hint no longer reads it (2026-09-30). */ readonly rectificationEntrySummary: RectificationEntrySummary | null; readonly rectificationError: string; readonly rectificationSurfaceOpen: boolean; @@ -82,7 +78,6 @@ export function StarterEntries({ rectificationLoading, rectificationMutationPending, openRectificationFromHomepage, - rectificationEntrySummary, rectificationError, rectificationSurfaceOpen, rectificationErrorMessage, @@ -90,7 +85,7 @@ export function StarterEntries({ const subjectId = useCurrentSubject(); const rectificationForSelf = subjectId === "self"; const dailyLabel = natalMinuteAvailable ? "今日星语" : "每日运势"; - const rectificationHint = rectificationEntryHint(rectificationForSelf, rectificationEntrySummary); + const rectificationHint = rectificationEntryHint(rectificationForSelf); return (
diff --git a/frontend/tests/chart-type-picker.test.tsx b/frontend/tests/chart-type-picker.test.tsx index d8f4eaba..964a38e2 100644 --- a/frontend/tests/chart-type-picker.test.tsx +++ b/frontend/tests/chart-type-picker.test.tsx @@ -69,7 +69,9 @@ test("the strip is one sideways-scrolling line and the sheet is a three-column g assert.match(strip, /min-width:\s*0/); assert.match(cssDeclarations(".chart-page-type-grid", globalStyles), /grid-template-columns:\s*repeat\(3, minmax\(0, 1fr\)\)/); assert.match(cssDeclarations(".chart-page-type-menu", globalStyles), /width:\s*44px/); - assert.match(cssDeclarations(".chart-page-type-cell", globalStyles), /min-height:\s*56px/); + // 原值:min-height 56px;新值:64px;原因:2026-09-30 真机截图说明文字被截底,说明行改为最多两行、行高 1.4,格子加高以容纳 + assert.match(cssDeclarations(".chart-page-type-cell", globalStyles), /min-height:\s*64px/); + assert.match(cssDeclarations(".chart-page-type-cell small", globalStyles), /line-clamp:\s*2/); assert.doesNotMatch(globalStyles, /\.chart-page-varga-chips/); }); diff --git a/frontend/tests/rectification-agentic-entry.test.ts b/frontend/tests/rectification-agentic-entry.test.ts index 8978e749..3f58201c 100644 --- a/frontend/tests/rectification-agentic-entry.test.ts +++ b/frontend/tests/rectification-agentic-entry.test.ts @@ -986,8 +986,11 @@ test("homepage CTA is server-driven from the entry summary, not the session list // 「restart」分支对应的那句已删除,改锁 hint 读的是 summary 本身、不读会话列表 // 原因:TASK-starter-home-polish-20260926 D2 删掉「上次已经校正完…」一句;BUG-1041 查明 // `=== "restart"` 从未命中(解析器读错键名)。「CTA 与提示由服务端 entry summary 驱动」这条意图不变。 - assert.match(page, /rectificationEntryHint\(rectificationForSelf, rectificationEntrySummary\)/); - assert.match(page, /summary\?\.hasResumableCase/); + // 原值:首页小字 rectificationEntryHint(rectificationForSelf, rectificationEntrySummary) 读 summary?.hasResumableCase + // 新值:rectificationEntryHint(rectificationForSelf),不再读 summary(未完成校正的提示句已删) + // 原因:2026-09-30 产品「首页的这个提示可以去掉」;CTA 仍由 entry summary 驱动(下方 resolveRectificationEntryAction 断言不变) + assert.match(page, /rectificationEntryHint\(rectificationForSelf\)/); + assert.doesNotMatch(page, /上次那次校正还没完成/); assert.doesNotMatch(page, /rectificationCardAction === "restart"/); assert.match(entryLib, /开始新的生时校正/); assert.match(entryLib, /再次校正/); diff --git a/frontend/tests/starter-home-polish-20260926.test.tsx b/frontend/tests/starter-home-polish-20260926.test.tsx index 451d2ff1..18d3dc17 100644 --- a/frontend/tests/starter-home-polish-20260926.test.tsx +++ b/frontend/tests/starter-home-polish-20260926.test.tsx @@ -73,19 +73,21 @@ test("D2: the line under the pills only appears when the user has something to d assert.doesNotMatch(markup, REMOVED_HINTS); } - // An unfinished case: one sentence, the resumable one. + // 原值:未完成的校正显示一句「上次那次校正还没完成,可以在历史对话里接着做。」 + // 新值:未完成的校正也不显示任何提示 + // 原因:2026-09-30 产品「首页的这个提示可以去掉」;校正会话仍在历史列表里 const resumable = renderToStaticMarkup(); - assert.equal(hintOf(resumable), RESUMABLE_HINT); + assert.equal(hintOf(resumable), null); + assert.doesNotMatch(resumable, new RegExp(RESUMABLE_HINT)); assert.doesNotMatch(resumable, REMOVED_HINTS); }); test("D2: a non-self subject gets the self-only sentence, even with an unfinished case on record", async () => { - assert.equal(rectificationEntryHint(false, null), NON_SELF_HINT); - assert.equal(rectificationEntryHint(false, summaries.resumable), NON_SELF_HINT); - assert.equal(rectificationEntryHint(true, summaries.resumable), RESUMABLE_HINT); - assert.equal(rectificationEntryHint(true, summaries.rectified), null); - assert.equal(rectificationEntryHint(true, summaries.firstRun), null); - assert.equal(rectificationEntryHint(true, null), null); + // 原值:rectificationEntryHint(rectificationForSelf, summary),本人 + 未完成时返回 RESUMABLE_HINT + // 新值:rectificationEntryHint(rectificationForSelf),本人一律无提示;非本人仍是「生时校正暂时只支持本人。」 + // 原因:2026-09-30 产品删除未完成校正提示,入参 summary 不再使用 + assert.equal(rectificationEntryHint(false), NON_SELF_HINT); + assert.equal(rectificationEntryHint(true), null); // Rendered through the real subject store, not just the helper. const harness = createClientLifecycleHarness(); @@ -194,5 +196,6 @@ test("BUG-1041: the homepage parser reads what the entry-summary route actually const resumable = entrySummaryFromResponse(resumableWire); assert.equal(resumable.hasResumableCase, true); assert.equal(resumable.latestResumable?.lastActivityAt, "2026-09-26T00:00:00.000Z"); - assert.equal(rectificationEntryHint(true, resumable), RESUMABLE_HINT); + // 原值:rectificationEntryHint(true, resumable) === RESUMABLE_HINT;新值:解析仍读出 hasResumableCase,但首页不再据此出提示;原因同上 + assert.equal(rectificationEntryHint(true), null); }); diff --git a/frontend/tests/surface-feedback-20260929.test.tsx b/frontend/tests/surface-feedback-20260929.test.tsx index d6da0201..c3025078 100644 --- a/frontend/tests/surface-feedback-20260929.test.tsx +++ b/frontend/tests/surface-feedback-20260929.test.tsx @@ -130,3 +130,34 @@ test("switching editions keeps the reader on the same chapter (BUG-1108)", () => assert.match(view, /drawn=\{drawnThrough !== null && chapter <= drawnThrough\}/); assert.match(view, /useState\(section\.eager \|\| drawn\)/); }); + +// Product feedback 2026-09-30 01:00. +test("the home no longer says an unfinished rectification is waiting in history", async () => { + const { rectificationEntryHint } = await import("../src/components/starter-home"); + assert.equal(rectificationEntryHint(true), null); + assert.doesNotMatch(read("../src/components/starter-home.tsx"), /上次那次校正还没完成/); +}); + +test("chart-type cells show the whole meaning line (two lines, CJK-safe line height)", () => { + const css = read("../src/app/globals.css"); + const small = css.slice(css.indexOf(".chart-page-type-cell small {"), css.indexOf("}", css.indexOf(".chart-page-type-cell small {"))); + assert.match(small, /line-height: 1\.4/); + assert.match(small, /-webkit-line-clamp: 2/); + assert.doesNotMatch(small, /white-space: nowrap/); +}); + +test("chart, ephemeris and report waits show the orbit, the sentence only for screen readers", () => { + const orbit = read("../src/components/orbit-waiting.tsx"); + assert.match(orbit, /\{label\}<\/span>/); + assert.match(orbit, /app-loading-orbit/); + assert.match(read("../src/components/secondary-page-shell.tsx"), //); + assert.match(read("../src/components/chart-page/chart-page-skeleton.tsx"), //); + assert.doesNotMatch(read("../src/components/chart-page/chart-page-skeleton.tsx"), /

{ + const entry = read("../src/components/birth-sky/birth-sky-entry.tsx"); + assert.doesNotMatch(entry, /Sparkles/); + assert.match(entry, //); + assert.equal((entry.match(/ Date: Wed, 30 Sep 2026 01:25:25 +0800 Subject: [PATCH 2/4] feat(home): daily line from a fixed sentence library; prefetch it so landing on home never swaps text (BUG-1109, BUG-1110) Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE --- CHANGELOG.md | 6 + docs/BUG_HISTORY.md | 28 +++ .../PROGRESS-daily-line-library-20260930.md | 218 ++++++++++++++++++ frontend/DESIGN.md | 2 + frontend/docs/VOICE.md | 9 + .../src/app/api/daily-starlanguage/route.ts | 62 +---- .../src/lib/daily-starlanguage-library.ts | 207 +++++++++++++++++ frontend/src/lib/home-types.ts | 2 +- frontend/src/lib/home-warm-prefetch.ts | 45 +++- frontend/src/lib/session-list-context.tsx | 12 +- .../tests/daily-starlanguage-library.test.ts | 116 ++++++++++ frontend/tests/daily-starlanguage.test.ts | 14 +- .../tests/home-warm-return-lifecycle.test.tsx | 25 +- 13 files changed, 670 insertions(+), 76 deletions(-) create mode 100644 docs/tasks/PROGRESS-daily-line-library-20260930.md create mode 100644 frontend/src/lib/daily-starlanguage-library.ts create mode 100644 frontend/tests/daily-starlanguage-library.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b59026f..d00650e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # 印度占星 Skill 更新日志 +## 2026-09-30 — 首页今日星语改成自己写的句库;从别的页面进首页不再「先一句、再换一句」(待验收) + +- 问候下面那句今日星语不再调用大模型,改为按「今天月亮落在你本命的哪一宫 × 月相 × 日期」从一套固定句库里取,秒出、不花钱;同一天同一个人看到的是同一句,第二天换一句(BUG-1109)。 +- 从星盘、星历、报告、档案页点「新建对话」进首页,今日星语在第一眼就是真句子,不再先显示「今天的星语还没写出来。」再换掉(BUG-1110)。 +- Skill 版本不 bump。不改数据库结构。 + ## 2026-09-30 — 报告导出面板重新设计(待验收) - 点「导出」后先选语言(中文 / English,下面一句建议问 AI 时选 English),再选「整篇报告」或「自选章节」。选整篇直接点底部按钮就导出;选自选才展开清单,按正文 / 分盘 / 核对表分组,每组一个勾选框可整组选,分盘和核对表默认收起。 diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index b935f482..aadc3b1e 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -14943,6 +14943,34 @@ - 相关记录:TASK-report-english-edition-20260929 - 修复版本:分支 `codex/surface-feedback-20260929` +## BUG-1109 | 首页今日星语靠模型改写,读起来像固定句,还会在同一天里被后台改写换掉 + +- 状态:resolved(代码 + 回归测试;未部署) +- 首次发现 / 最近更新:2026-09-30 / 2026-09-30 +- 影响面:`app/api/daily-starlanguage/route.ts`、新增 `lib/daily-starlanguage-library.ts`、`lib/home-types.ts`。 +- 用户现象:产品反馈「标题后面这句好像都是固定的,可以不走大模型,咱们设计一套」。 +- 根因:路由先用 `composeDailyStarlanguageCard` 按 12 宫主题出一张确定句卡,再 `scheduleAgentPolish` 让模型在后台改写并覆盖同日缓存;模型只拿到「月亮第几宫、大运主星」等几个字段,改写结果同质;覆盖后下一次请求拿到的是另一句,页面上会换字。 +- 修复:句库 `composeDailyCard`(12 宫 × 3 句 + 4 月相 × 2 尾句 + 每宫 action / caution 各 2 句;无宫位按星座 12 句;再无按旧证据句),路由去掉模型目录读取与模型调用,`source: "library"`。模型 agent 定义(`mastra/index.ts`)保留未删,仍被既有测试引用。 +- 验证:`frontend/tests/daily-starlanguage-library.test.ts`(6 条:库完整、144 组合全部合法且 ≤90 字、同日同句且相邻日不同、月相与已知新月/满月对得上、未确认出生时间走星座兜底、路由无模型调用);`daily-starlanguage.test.ts` 两处断言按三栏更新。 +- 防复发:今日星语的句子只来自句库文件;路由测试禁止重新引入模型调用。 +- 相关记录:BUG-1110 +- 复发自:无 +- 修复版本:分支 `codex/daily-line-library-20260930` + +## BUG-1110 | 从次级页点「新建对话」进首页,今日星语先显示占位句再突然换成真句子 + +- 状态:resolved(代码 + 生命周期回归测试;真机欠) +- 首次发现 / 最近更新:2026-09-30 / 2026-09-30 +- 影响面:`lib/home-warm-prefetch.ts`、`lib/session-list-context.tsx`。 +- 用户现象:产品「现在跳转的时候还会突然变一下,很反直觉」。 +- 根因:BUG-1104 的次级页预取只填模型目录与校正入口摘要,暖快照的今日星语槽位为空;首页暖启动 `warmDaily`(`lib/home-warm-start.ts`)读不到本日卡片即给 `pending`,首帧显示「今天的星语还没写出来。」,随后 `DailyStarlanguageBinder` 请求返回再替换。另一来源是 BUG-1109 的后台模型改写使同日两次请求句子不同。 +- 修复:预取时再为本人按同一键(人物 + 资料指纹 + 当地日期)调用同一个 `fetchDailyStarlanguage`,结果写入 `rememberHomeWarmDaily` 与本地存储;失败或无卡片不写任何东西。他人档案为当前人物时暂不预取。 +- 验证:`home-warm-return-lifecycle.test.tsx` 两条 BUG-1104 用例扩展为同时断言首帧已含今日星语、无占位句(请求清单断言按三栏更新:多一条只读的 `POST /api/daily-starlanguage`);关掉预取后该用例失败。 +- 防复发:首页首帧会显示的每一项,都要能从预取或暖快照拿到;新增首页首帧元素时同时加进预取。 +- 相关记录:BUG-1040、BUG-1104、BUG-1109 +- 复发自:BUG-1104 的遗漏 +- 修复版本:分支 `codex/daily-line-library-20260930` + ## BUG-1111 | 首页常驻「上次那次校正还没完成,可以在历史对话里接着做。」 - 状态:resolved(代码 + 回归测试) diff --git a/docs/tasks/PROGRESS-daily-line-library-20260930.md b/docs/tasks/PROGRESS-daily-line-library-20260930.md new file mode 100644 index 00000000..ec8f2784 --- /dev/null +++ b/docs/tasks/PROGRESS-daily-line-library-20260930.md @@ -0,0 +1,218 @@ +# PROGRESS · 今日星语句库 + 进首页不换字 · 2026-09-30 + +> 产品 2026-09-30 01:00 反馈;产品选定「自己写一套句库」。执行:fork 子代理(直接执行模式)。分支 `codex/daily-line-library-20260930`,基线 `origin/staging` = `82db6373`。BUG-1109 / BUG-1110。 + +## 句库审阅表(请产品过目,改句只改 `frontend/src/lib/daily-starlanguage-library.ts`) + +月相尾句每种两条、按三天一换;下表每行第 1、3 变体用第一条尾句、第 2 变体用第二条,仅为展示全部句子。 + +| 宫 | 主题 | 月相 | 变体 | 今日星语(trend,完整一句) | 字数 | +|---|---|---|---|---|---| +| 1 | 自身/身体 | 新月前后 | 1 | 今天注意力更多回到自己身上,精神和身体的状态都更容易被察觉。新月前后,适合定一个小开头。 | 44 | +| 1 | 自身/身体 | 新月前后 | 2 | 今天适合照顾自己的节奏,先把状态调顺,再去应付别的事。月亮刚要重新变圆,先想清楚再动手。 | 44 | +| 1 | 自身/身体 | 新月前后 | 3 | 今天你的想法和情绪都更直接,别人也更容易注意到你。新月前后,适合定一个小开头。 | 39 | +| 1 | 自身/身体 | 渐盈 | 1 | 今天注意力更多回到自己身上,精神和身体的状态都更容易被察觉。月亮渐盈,适合往前推一步。 | 43 | +| 1 | 自身/身体 | 渐盈 | 2 | 今天适合照顾自己的节奏,先把状态调顺,再去应付别的事。月亮渐盈,做加法比做减法顺。 | 41 | +| 1 | 自身/身体 | 渐盈 | 3 | 今天你的想法和情绪都更直接,别人也更容易注意到你。月亮渐盈,适合往前推一步。 | 38 | +| 1 | 自身/身体 | 满月前后 | 1 | 今天注意力更多回到自己身上,精神和身体的状态都更容易被察觉。满月前后,情绪容易满,先稳住再回应。 | 48 | +| 1 | 自身/身体 | 满月前后 | 2 | 今天适合照顾自己的节奏,先把状态调顺,再去应付别的事。满月前后,适合把结果摆出来看看。 | 43 | +| 1 | 自身/身体 | 满月前后 | 3 | 今天你的想法和情绪都更直接,别人也更容易注意到你。满月前后,情绪容易满,先稳住再回应。 | 43 | +| 1 | 自身/身体 | 渐亏 | 1 | 今天注意力更多回到自己身上,精神和身体的状态都更容易被察觉。月亮渐亏,适合收尾和清理。 | 43 | +| 1 | 自身/身体 | 渐亏 | 2 | 今天适合照顾自己的节奏,先把状态调顺,再去应付别的事。月亮渐亏,做减法比开新头顺。 | 41 | +| 1 | 自身/身体 | 渐亏 | 3 | 今天你的想法和情绪都更直接,别人也更容易注意到你。月亮渐亏,适合收尾和清理。 | 38 | +| 2 | 钱/说话 | 新月前后 | 1 | 今天钱和说话这两件事更容易被放大,花出去的和说出口的都值得多想一下。新月前后,适合定一个小开头。 | 48 | +| 2 | 钱/说话 | 新月前后 | 2 | 今天适合盘一盘手头的资源,也适合把想说的话说得更稳一点。月亮刚要重新变圆,先想清楚再动手。 | 45 | +| 2 | 钱/说话 | 新月前后 | 3 | 今天对价格、收入这些具体数字更敏感,适合算清楚再决定。新月前后,适合定一个小开头。 | 41 | +| 2 | 钱/说话 | 渐盈 | 1 | 今天钱和说话这两件事更容易被放大,花出去的和说出口的都值得多想一下。月亮渐盈,适合往前推一步。 | 47 | +| 2 | 钱/说话 | 渐盈 | 2 | 今天适合盘一盘手头的资源,也适合把想说的话说得更稳一点。月亮渐盈,做加法比做减法顺。 | 42 | +| 2 | 钱/说话 | 渐盈 | 3 | 今天对价格、收入这些具体数字更敏感,适合算清楚再决定。月亮渐盈,适合往前推一步。 | 40 | +| 2 | 钱/说话 | 满月前后 | 1 | 今天钱和说话这两件事更容易被放大,花出去的和说出口的都值得多想一下。满月前后,情绪容易满,先稳住再回应。 | 52 | +| 2 | 钱/说话 | 满月前后 | 2 | 今天适合盘一盘手头的资源,也适合把想说的话说得更稳一点。满月前后,适合把结果摆出来看看。 | 44 | +| 2 | 钱/说话 | 满月前后 | 3 | 今天对价格、收入这些具体数字更敏感,适合算清楚再决定。满月前后,情绪容易满,先稳住再回应。 | 45 | +| 2 | 钱/说话 | 渐亏 | 1 | 今天钱和说话这两件事更容易被放大,花出去的和说出口的都值得多想一下。月亮渐亏,适合收尾和清理。 | 47 | +| 2 | 钱/说话 | 渐亏 | 2 | 今天适合盘一盘手头的资源,也适合把想说的话说得更稳一点。月亮渐亏,做减法比开新头顺。 | 42 | +| 2 | 钱/说话 | 渐亏 | 3 | 今天对价格、收入这些具体数字更敏感,适合算清楚再决定。月亮渐亏,适合收尾和清理。 | 40 | +| 3 | 行动/联络/短途 | 新月前后 | 1 | 今天行动力不错,适合跑腿、联络,把拖着的小事一件件办掉。新月前后,适合定一个小开头。 | 42 | +| 3 | 行动/联络/短途 | 新月前后 | 2 | 今天消息往来多,短途出行和身边的兄弟朋友都可能找上你。月亮刚要重新变圆,先想清楚再动手。 | 44 | +| 3 | 行动/联络/短途 | 新月前后 | 3 | 今天适合主动一点,写、说、发,都比等别人来找更有效。新月前后,适合定一个小开头。 | 40 | +| 3 | 行动/联络/短途 | 渐盈 | 1 | 今天行动力不错,适合跑腿、联络,把拖着的小事一件件办掉。月亮渐盈,适合往前推一步。 | 41 | +| 3 | 行动/联络/短途 | 渐盈 | 2 | 今天消息往来多,短途出行和身边的兄弟朋友都可能找上你。月亮渐盈,做加法比做减法顺。 | 41 | +| 3 | 行动/联络/短途 | 渐盈 | 3 | 今天适合主动一点,写、说、发,都比等别人来找更有效。月亮渐盈,适合往前推一步。 | 39 | +| 3 | 行动/联络/短途 | 满月前后 | 1 | 今天行动力不错,适合跑腿、联络,把拖着的小事一件件办掉。满月前后,情绪容易满,先稳住再回应。 | 46 | +| 3 | 行动/联络/短途 | 满月前后 | 2 | 今天消息往来多,短途出行和身边的兄弟朋友都可能找上你。满月前后,适合把结果摆出来看看。 | 43 | +| 3 | 行动/联络/短途 | 满月前后 | 3 | 今天适合主动一点,写、说、发,都比等别人来找更有效。满月前后,情绪容易满,先稳住再回应。 | 44 | +| 3 | 行动/联络/短途 | 渐亏 | 1 | 今天行动力不错,适合跑腿、联络,把拖着的小事一件件办掉。月亮渐亏,适合收尾和清理。 | 41 | +| 3 | 行动/联络/短途 | 渐亏 | 2 | 今天消息往来多,短途出行和身边的兄弟朋友都可能找上你。月亮渐亏,做减法比开新头顺。 | 41 | +| 3 | 行动/联络/短途 | 渐亏 | 3 | 今天适合主动一点,写、说、发,都比等别人来找更有效。月亮渐亏,适合收尾和清理。 | 39 | +| 4 | 家/情绪 | 新月前后 | 1 | 今天心思更容易回到家里,情绪起伏也更容易被家人察觉。新月前后,适合定一个小开头。 | 40 | +| 4 | 家/情绪 | 新月前后 | 2 | 今天适合把住处或自己的情绪收拾一下,里面安定了,外面的事才顺。月亮刚要重新变圆,先想清楚再动手。 | 48 | +| 4 | 家/情绪 | 新月前后 | 3 | 今天更需要一个让自己安心的角落,别把所有事都排得太满。新月前后,适合定一个小开头。 | 41 | +| 4 | 家/情绪 | 渐盈 | 1 | 今天心思更容易回到家里,情绪起伏也更容易被家人察觉。月亮渐盈,适合往前推一步。 | 39 | +| 4 | 家/情绪 | 渐盈 | 2 | 今天适合把住处或自己的情绪收拾一下,里面安定了,外面的事才顺。月亮渐盈,做加法比做减法顺。 | 45 | +| 4 | 家/情绪 | 渐盈 | 3 | 今天更需要一个让自己安心的角落,别把所有事都排得太满。月亮渐盈,适合往前推一步。 | 40 | +| 4 | 家/情绪 | 满月前后 | 1 | 今天心思更容易回到家里,情绪起伏也更容易被家人察觉。满月前后,情绪容易满,先稳住再回应。 | 44 | +| 4 | 家/情绪 | 满月前后 | 2 | 今天适合把住处或自己的情绪收拾一下,里面安定了,外面的事才顺。满月前后,适合把结果摆出来看看。 | 47 | +| 4 | 家/情绪 | 满月前后 | 3 | 今天更需要一个让自己安心的角落,别把所有事都排得太满。满月前后,情绪容易满,先稳住再回应。 | 45 | +| 4 | 家/情绪 | 渐亏 | 1 | 今天心思更容易回到家里,情绪起伏也更容易被家人察觉。月亮渐亏,适合收尾和清理。 | 39 | +| 4 | 家/情绪 | 渐亏 | 2 | 今天适合把住处或自己的情绪收拾一下,里面安定了,外面的事才顺。月亮渐亏,做减法比开新头顺。 | 45 | +| 4 | 家/情绪 | 渐亏 | 3 | 今天更需要一个让自己安心的角落,别把所有事都排得太满。月亮渐亏,适合收尾和清理。 | 40 | +| 5 | 创作/恋爱/孩子 | 新月前后 | 1 | 今天适合做点让自己开心的事,创作、玩乐或和喜欢的人相处都更顺。新月前后,适合定一个小开头。 | 45 | +| 5 | 创作/恋爱/孩子 | 新月前后 | 2 | 今天表达欲更强,适合把一个想法做成小作品拿出来。月亮刚要重新变圆,先想清楚再动手。 | 41 | +| 5 | 创作/恋爱/孩子 | 新月前后 | 3 | 今天和孩子、恋人或兴趣有关的事,更容易带来好心情。新月前后,适合定一个小开头。 | 39 | +| 5 | 创作/恋爱/孩子 | 渐盈 | 1 | 今天适合做点让自己开心的事,创作、玩乐或和喜欢的人相处都更顺。月亮渐盈,适合往前推一步。 | 44 | +| 5 | 创作/恋爱/孩子 | 渐盈 | 2 | 今天表达欲更强,适合把一个想法做成小作品拿出来。月亮渐盈,做加法比做减法顺。 | 38 | +| 5 | 创作/恋爱/孩子 | 渐盈 | 3 | 今天和孩子、恋人或兴趣有关的事,更容易带来好心情。月亮渐盈,适合往前推一步。 | 38 | +| 5 | 创作/恋爱/孩子 | 满月前后 | 1 | 今天适合做点让自己开心的事,创作、玩乐或和喜欢的人相处都更顺。满月前后,情绪容易满,先稳住再回应。 | 49 | +| 5 | 创作/恋爱/孩子 | 满月前后 | 2 | 今天表达欲更强,适合把一个想法做成小作品拿出来。满月前后,适合把结果摆出来看看。 | 40 | +| 5 | 创作/恋爱/孩子 | 满月前后 | 3 | 今天和孩子、恋人或兴趣有关的事,更容易带来好心情。满月前后,情绪容易满,先稳住再回应。 | 43 | +| 5 | 创作/恋爱/孩子 | 渐亏 | 1 | 今天适合做点让自己开心的事,创作、玩乐或和喜欢的人相处都更顺。月亮渐亏,适合收尾和清理。 | 44 | +| 5 | 创作/恋爱/孩子 | 渐亏 | 2 | 今天表达欲更强,适合把一个想法做成小作品拿出来。月亮渐亏,做减法比开新头顺。 | 38 | +| 5 | 创作/恋爱/孩子 | 渐亏 | 3 | 今天和孩子、恋人或兴趣有关的事,更容易带来好心情。月亮渐亏,适合收尾和清理。 | 38 | +| 6 | 日常/作息 | 新月前后 | 1 | 今天适合处理日常杂事和待办清单,按部就班反而最省力。新月前后,适合定一个小开头。 | 40 | +| 6 | 日常/作息 | 新月前后 | 2 | 今天身体和作息的小信号更明显,适合把一个习惯调整回来。月亮刚要重新变圆,先想清楚再动手。 | 44 | +| 6 | 日常/作息 | 新月前后 | 3 | 今天工作里的琐碎事会多一些,一件件处理掉就好。新月前后,适合定一个小开头。 | 37 | +| 6 | 日常/作息 | 渐盈 | 1 | 今天适合处理日常杂事和待办清单,按部就班反而最省力。月亮渐盈,适合往前推一步。 | 39 | +| 6 | 日常/作息 | 渐盈 | 2 | 今天身体和作息的小信号更明显,适合把一个习惯调整回来。月亮渐盈,做加法比做减法顺。 | 41 | +| 6 | 日常/作息 | 渐盈 | 3 | 今天工作里的琐碎事会多一些,一件件处理掉就好。月亮渐盈,适合往前推一步。 | 36 | +| 6 | 日常/作息 | 满月前后 | 1 | 今天适合处理日常杂事和待办清单,按部就班反而最省力。满月前后,情绪容易满,先稳住再回应。 | 44 | +| 6 | 日常/作息 | 满月前后 | 2 | 今天身体和作息的小信号更明显,适合把一个习惯调整回来。满月前后,适合把结果摆出来看看。 | 43 | +| 6 | 日常/作息 | 满月前后 | 3 | 今天工作里的琐碎事会多一些,一件件处理掉就好。满月前后,情绪容易满,先稳住再回应。 | 41 | +| 6 | 日常/作息 | 渐亏 | 1 | 今天适合处理日常杂事和待办清单,按部就班反而最省力。月亮渐亏,适合收尾和清理。 | 39 | +| 6 | 日常/作息 | 渐亏 | 2 | 今天身体和作息的小信号更明显,适合把一个习惯调整回来。月亮渐亏,做减法比开新头顺。 | 41 | +| 6 | 日常/作息 | 渐亏 | 3 | 今天工作里的琐碎事会多一些,一件件处理掉就好。月亮渐亏,适合收尾和清理。 | 36 | +| 7 | 伴侣/合作 | 新月前后 | 1 | 今天身边的人更重要,合作和相处里的细节更容易被放大。新月前后,适合定一个小开头。 | 40 | +| 7 | 伴侣/合作 | 新月前后 | 2 | 今天适合和伴侣或合作方把事情对齐,一起商量比自己硬扛好。月亮刚要重新变圆,先想清楚再动手。 | 45 | +| 7 | 伴侣/合作 | 新月前后 | 3 | 今天别人的态度会影响你的心情,先听清对方想要什么。新月前后,适合定一个小开头。 | 39 | +| 7 | 伴侣/合作 | 渐盈 | 1 | 今天身边的人更重要,合作和相处里的细节更容易被放大。月亮渐盈,适合往前推一步。 | 39 | +| 7 | 伴侣/合作 | 渐盈 | 2 | 今天适合和伴侣或合作方把事情对齐,一起商量比自己硬扛好。月亮渐盈,做加法比做减法顺。 | 42 | +| 7 | 伴侣/合作 | 渐盈 | 3 | 今天别人的态度会影响你的心情,先听清对方想要什么。月亮渐盈,适合往前推一步。 | 38 | +| 7 | 伴侣/合作 | 满月前后 | 1 | 今天身边的人更重要,合作和相处里的细节更容易被放大。满月前后,情绪容易满,先稳住再回应。 | 44 | +| 7 | 伴侣/合作 | 满月前后 | 2 | 今天适合和伴侣或合作方把事情对齐,一起商量比自己硬扛好。满月前后,适合把结果摆出来看看。 | 44 | +| 7 | 伴侣/合作 | 满月前后 | 3 | 今天别人的态度会影响你的心情,先听清对方想要什么。满月前后,情绪容易满,先稳住再回应。 | 43 | +| 7 | 伴侣/合作 | 渐亏 | 1 | 今天身边的人更重要,合作和相处里的细节更容易被放大。月亮渐亏,适合收尾和清理。 | 39 | +| 7 | 伴侣/合作 | 渐亏 | 2 | 今天适合和伴侣或合作方把事情对齐,一起商量比自己硬扛好。月亮渐亏,做减法比开新头顺。 | 42 | +| 7 | 伴侣/合作 | 渐亏 | 3 | 今天别人的态度会影响你的心情,先听清对方想要什么。月亮渐亏,适合收尾和清理。 | 38 | +| 8 | 变化/内在压力 | 新月前后 | 1 | 今天情绪可能更深一点,适合面对一件一直不愿碰的事。新月前后,适合定一个小开头。 | 39 | +| 8 | 变化/内在压力 | 新月前后 | 2 | 今天容易想得多,把担心写下来,比在脑子里打转轻松。月亮刚要重新变圆,先想清楚再动手。 | 42 | +| 8 | 变化/内在压力 | 新月前后 | 3 | 今天适合处理共同的钱物或旧账,慢一点、细一点。新月前后,适合定一个小开头。 | 37 | +| 8 | 变化/内在压力 | 渐盈 | 1 | 今天情绪可能更深一点,适合面对一件一直不愿碰的事。月亮渐盈,适合往前推一步。 | 38 | +| 8 | 变化/内在压力 | 渐盈 | 2 | 今天容易想得多,把担心写下来,比在脑子里打转轻松。月亮渐盈,做加法比做减法顺。 | 39 | +| 8 | 变化/内在压力 | 渐盈 | 3 | 今天适合处理共同的钱物或旧账,慢一点、细一点。月亮渐盈,适合往前推一步。 | 36 | +| 8 | 变化/内在压力 | 满月前后 | 1 | 今天情绪可能更深一点,适合面对一件一直不愿碰的事。满月前后,情绪容易满,先稳住再回应。 | 43 | +| 8 | 变化/内在压力 | 满月前后 | 2 | 今天容易想得多,把担心写下来,比在脑子里打转轻松。满月前后,适合把结果摆出来看看。 | 41 | +| 8 | 变化/内在压力 | 满月前后 | 3 | 今天适合处理共同的钱物或旧账,慢一点、细一点。满月前后,情绪容易满,先稳住再回应。 | 41 | +| 8 | 变化/内在压力 | 渐亏 | 1 | 今天情绪可能更深一点,适合面对一件一直不愿碰的事。月亮渐亏,适合收尾和清理。 | 38 | +| 8 | 变化/内在压力 | 渐亏 | 2 | 今天容易想得多,把担心写下来,比在脑子里打转轻松。月亮渐亏,做减法比开新头顺。 | 39 | +| 8 | 变化/内在压力 | 渐亏 | 3 | 今天适合处理共同的钱物或旧账,慢一点、细一点。月亮渐亏,适合收尾和清理。 | 36 | +| 9 | 学习/信念/远行 | 新月前后 | 1 | 今天适合学点新东西,读一篇长文或听一次课都会有收获。新月前后,适合定一个小开头。 | 40 | +| 9 | 学习/信念/远行 | 新月前后 | 2 | 今天视野更容易打开,远方的人和事可能带来新想法。月亮刚要重新变圆,先想清楚再动手。 | 41 | +| 9 | 学习/信念/远行 | 新月前后 | 3 | 今天适合想想更长远的方向,别只盯着眼前这一件。新月前后,适合定一个小开头。 | 37 | +| 9 | 学习/信念/远行 | 渐盈 | 1 | 今天适合学点新东西,读一篇长文或听一次课都会有收获。月亮渐盈,适合往前推一步。 | 39 | +| 9 | 学习/信念/远行 | 渐盈 | 2 | 今天视野更容易打开,远方的人和事可能带来新想法。月亮渐盈,做加法比做减法顺。 | 38 | +| 9 | 学习/信念/远行 | 渐盈 | 3 | 今天适合想想更长远的方向,别只盯着眼前这一件。月亮渐盈,适合往前推一步。 | 36 | +| 9 | 学习/信念/远行 | 满月前后 | 1 | 今天适合学点新东西,读一篇长文或听一次课都会有收获。满月前后,情绪容易满,先稳住再回应。 | 44 | +| 9 | 学习/信念/远行 | 满月前后 | 2 | 今天视野更容易打开,远方的人和事可能带来新想法。满月前后,适合把结果摆出来看看。 | 40 | +| 9 | 学习/信念/远行 | 满月前后 | 3 | 今天适合想想更长远的方向,别只盯着眼前这一件。满月前后,情绪容易满,先稳住再回应。 | 41 | +| 9 | 学习/信念/远行 | 渐亏 | 1 | 今天适合学点新东西,读一篇长文或听一次课都会有收获。月亮渐亏,适合收尾和清理。 | 39 | +| 9 | 学习/信念/远行 | 渐亏 | 2 | 今天视野更容易打开,远方的人和事可能带来新想法。月亮渐亏,做减法比开新头顺。 | 38 | +| 9 | 学习/信念/远行 | 渐亏 | 3 | 今天适合想想更长远的方向,别只盯着眼前这一件。月亮渐亏,适合收尾和清理。 | 36 | +| 10 | 工作/对外 | 新月前后 | 1 | 今天工作和对外的事更显眼,你的表现更容易被人看到。新月前后,适合定一个小开头。 | 39 | +| 10 | 工作/对外 | 新月前后 | 2 | 今天适合把一项成果推进一步,让该知道的人知道。月亮刚要重新变圆,先想清楚再动手。 | 40 | +| 10 | 工作/对外 | 新月前后 | 3 | 今天责任感会重一点,先做最要紧的那件,别被杂事拉走。新月前后,适合定一个小开头。 | 40 | +| 10 | 工作/对外 | 渐盈 | 1 | 今天工作和对外的事更显眼,你的表现更容易被人看到。月亮渐盈,适合往前推一步。 | 38 | +| 10 | 工作/对外 | 渐盈 | 2 | 今天适合把一项成果推进一步,让该知道的人知道。月亮渐盈,做加法比做减法顺。 | 37 | +| 10 | 工作/对外 | 渐盈 | 3 | 今天责任感会重一点,先做最要紧的那件,别被杂事拉走。月亮渐盈,适合往前推一步。 | 39 | +| 10 | 工作/对外 | 满月前后 | 1 | 今天工作和对外的事更显眼,你的表现更容易被人看到。满月前后,情绪容易满,先稳住再回应。 | 43 | +| 10 | 工作/对外 | 满月前后 | 2 | 今天适合把一项成果推进一步,让该知道的人知道。满月前后,适合把结果摆出来看看。 | 39 | +| 10 | 工作/对外 | 满月前后 | 3 | 今天责任感会重一点,先做最要紧的那件,别被杂事拉走。满月前后,情绪容易满,先稳住再回应。 | 44 | +| 10 | 工作/对外 | 渐亏 | 1 | 今天工作和对外的事更显眼,你的表现更容易被人看到。月亮渐亏,适合收尾和清理。 | 38 | +| 10 | 工作/对外 | 渐亏 | 2 | 今天适合把一项成果推进一步,让该知道的人知道。月亮渐亏,做减法比开新头顺。 | 37 | +| 10 | 工作/对外 | 渐亏 | 3 | 今天责任感会重一点,先做最要紧的那件,别被杂事拉走。月亮渐亏,适合收尾和清理。 | 39 | +| 11 | 朋友/收获 | 新月前后 | 1 | 今天朋友和圈子更活跃,一次交流可能带来有用的消息。新月前后,适合定一个小开头。 | 39 | +| 11 | 朋友/收获 | 新月前后 | 2 | 今天适合联系老朋友或参加一次聚会,关系会带来机会。月亮刚要重新变圆,先想清楚再动手。 | 42 | +| 11 | 朋友/收获 | 新月前后 | 3 | 今天心里的愿望更容易说出口,也更容易得到别人的回应。新月前后,适合定一个小开头。 | 40 | +| 11 | 朋友/收获 | 渐盈 | 1 | 今天朋友和圈子更活跃,一次交流可能带来有用的消息。月亮渐盈,适合往前推一步。 | 38 | +| 11 | 朋友/收获 | 渐盈 | 2 | 今天适合联系老朋友或参加一次聚会,关系会带来机会。月亮渐盈,做加法比做减法顺。 | 39 | +| 11 | 朋友/收获 | 渐盈 | 3 | 今天心里的愿望更容易说出口,也更容易得到别人的回应。月亮渐盈,适合往前推一步。 | 39 | +| 11 | 朋友/收获 | 满月前后 | 1 | 今天朋友和圈子更活跃,一次交流可能带来有用的消息。满月前后,情绪容易满,先稳住再回应。 | 43 | +| 11 | 朋友/收获 | 满月前后 | 2 | 今天适合联系老朋友或参加一次聚会,关系会带来机会。满月前后,适合把结果摆出来看看。 | 41 | +| 11 | 朋友/收获 | 满月前后 | 3 | 今天心里的愿望更容易说出口,也更容易得到别人的回应。满月前后,情绪容易满,先稳住再回应。 | 44 | +| 11 | 朋友/收获 | 渐亏 | 1 | 今天朋友和圈子更活跃,一次交流可能带来有用的消息。月亮渐亏,适合收尾和清理。 | 38 | +| 11 | 朋友/收获 | 渐亏 | 2 | 今天适合联系老朋友或参加一次聚会,关系会带来机会。月亮渐亏,做减法比开新头顺。 | 39 | +| 11 | 朋友/收获 | 渐亏 | 3 | 今天心里的愿望更容易说出口,也更容易得到别人的回应。月亮渐亏,适合收尾和清理。 | 39 | +| 12 | 休息/独处/花销 | 新月前后 | 1 | 今天更需要休息和独处,把节奏放慢不是偷懒。新月前后,适合定一个小开头。 | 35 | +| 12 | 休息/独处/花销 | 新月前后 | 2 | 今天适合收尾和整理,不急着开新头。月亮刚要重新变圆,先想清楚再动手。 | 34 | +| 12 | 休息/独处/花销 | 新月前后 | 3 | 今天花钱和精力都容易悄悄流走,留意一下去向。新月前后,适合定一个小开头。 | 36 | +| 12 | 休息/独处/花销 | 渐盈 | 1 | 今天更需要休息和独处,把节奏放慢不是偷懒。月亮渐盈,适合往前推一步。 | 34 | +| 12 | 休息/独处/花销 | 渐盈 | 2 | 今天适合收尾和整理,不急着开新头。月亮渐盈,做加法比做减法顺。 | 31 | +| 12 | 休息/独处/花销 | 渐盈 | 3 | 今天花钱和精力都容易悄悄流走,留意一下去向。月亮渐盈,适合往前推一步。 | 35 | +| 12 | 休息/独处/花销 | 满月前后 | 1 | 今天更需要休息和独处,把节奏放慢不是偷懒。满月前后,情绪容易满,先稳住再回应。 | 39 | +| 12 | 休息/独处/花销 | 满月前后 | 2 | 今天适合收尾和整理,不急着开新头。满月前后,适合把结果摆出来看看。 | 33 | +| 12 | 休息/独处/花销 | 满月前后 | 3 | 今天花钱和精力都容易悄悄流走,留意一下去向。满月前后,情绪容易满,先稳住再回应。 | 40 | +| 12 | 休息/独处/花销 | 渐亏 | 1 | 今天更需要休息和独处,把节奏放慢不是偷懒。月亮渐亏,适合收尾和清理。 | 34 | +| 12 | 休息/独处/花销 | 渐亏 | 2 | 今天适合收尾和整理,不急着开新头。月亮渐亏,做减法比开新头顺。 | 31 | +| 12 | 休息/独处/花销 | 渐亏 | 3 | 今天花钱和精力都容易悄悄流走,留意一下去向。月亮渐亏,适合收尾和清理。 | 35 | + +每宫的 action / caution(各 2 条,按日期交替): + +| 宫 | 建议(action) | 留意(caution) | +|---|---|---| +| 1 | 早点睡或出门走二十分钟,把状态先养好。 / 给自己定一件今天一定做完的小事。 | 别因为一时情绪就做决定。 / 别把自己排得太满。 | +| 2 | 记下今天的每笔开销,或把一个价格重新核一遍。 / 开口前先想好一句最想表达的话。 | 冲动消费和气话都容易在今天冒出来。 / 别为了面子答应超出预算的事。 | +| 3 | 把拖了最久的那条消息今天发出去。 / 列三件小事,逐一办掉。 | 消息多,容易看漏细节,重要的事再确认一遍。 / 别同时开太多头。 | +| 4 | 收拾一个角落,或给家里人打个电话。 / 晚上留半小时给自己,什么都不排。 | 别把外面的烦心事带回家发泄。 / 情绪低的时候别做大决定。 | +| 5 | 花一小时做一件纯粹因为喜欢而做的事。 / 把一个点子写下来或画出来。 | 开心归开心,别在兴头上许下太大的承诺。 / 别拿自己和别人比。 | +| 6 | 从待办里挑一件今天能做完的,先做它。 / 调一个作息:早睡半小时,或少喝一杯咖啡。 | 别让琐事占满整天,留出做正事的时间。 / 身体有小不适别硬撑,该休息就休息。 | +| 7 | 主动约对方,把一件没说清的事聊清楚。 / 先问一句「你怎么看」,再说自己的想法。 | 别把对方的一句话想得太重。 / 意见不合时,先别急着争对错。 | +| 8 | 把最担心的一件事写下来,再写下一步怎么做。 / 整理一笔共同账目或一份旧文件。 | 别在焦虑的时候翻旧账。 / 涉及钱物的约定,写清楚再定。 | +| 9 | 读完一篇收藏很久的文章。 / 给远方的人发一条问候。 | 别急着说服别人接受你的看法。 / 远途安排多留一点余量。 | +| 10 | 把这周的一项进展整理成一段话,发给相关的人。 / 先完成最要紧的一件,再回消息。 | 别为了表现把自己逼得太紧。 / 对外说话前,再看一遍措辞。 | +| 11 | 联系一个很久没聊的朋友。 / 在群里分享一条对别人有用的信息。 | 人多的场合,别轻易答应帮忙。 / 别被别人的节奏带着跑。 | +| 12 | 给自己留一段不看手机的时间。 / 清一清订阅和自动扣费。 | 别用熬夜来换独处的时间。 / 留意那些看不见的开销。 | + +出生时间未确认或没有宫位时,按月亮所在星座(12 条): + +| 星座 | 句子 | +|---|---| +| Aries | 今天月亮在…,整体节奏偏快,适合先动起来。 | +| Taurus | 今天月亮在…,整体节奏偏稳,适合把手上的事做扎实。 | +| Gemini | 今天月亮在…,消息和想法都多,适合沟通和学习。 | +| Cancer | 今天月亮在…,情绪更柔软,适合照顾家和身边的人。 | +| Leo | 今天月亮在…,表现欲更强,适合把自己的想法拿出来。 | +| Virgo | 今天月亮在…,适合整理细节,把清单一件件勾掉。 | +| Libra | 今天月亮在…,更在意关系和分寸,适合商量和协调。 | +| Scorpio | 今天月亮在…,感受更深,适合专注处理一件要紧事。 | +| Sagittarius | 今天月亮在…,心情更开阔,适合学习或出门走走。 | +| Capricorn | 今天月亮在…,更务实,适合推进需要耐心的事。 | +| Aquarius | 今天月亮在…,更想跳出常规,适合换个角度想问题。 | +| Pisces | 今天月亮在…,更感性也更容易累,适合放慢一点。 | + +## 根因(代码核实) + +- 跳转「突然变一下」:`frontend/src/lib/home-warm-start.ts` 的 `warmDaily` 在暖快照与本地存储都没有当日卡片时返回 `{ kind: "pending" }`,首帧显示「今天的星语还没写出来。」(`frontend/src/app/(app)/page.tsx` 的 `dailyStarlanguageTrend`),随后 `DailyStarlanguageBinder` 请求返回替换。BUG-1104 的预取(`lib/home-warm-prefetch.ts`)不取今日星语。 +- 另一来源:`app/api/daily-starlanguage/route.ts` 的 `scheduleAgentPolish` 在后台让模型改写并覆盖同日缓存,同一天第二次请求拿到另一句,首页替换成新句。 +- 其他首帧元素:问候语每次进入首页随机挑一次、之后不变(`page.tsx` `starterGreetingSelection`),未发现落地后再换;校正入口卡由 BUG-1104 预取覆盖。 + +## 既有断言改动(原值 / 新值 / 原因) + +| 文件 | 原值 | 新值 | 原因 | +|---|---|---|---| +| `tests/daily-starlanguage.test.ts`(路由用例) | generateCard 用 `composeDailyStarlanguageCard(evidence)`,存在 `polishWithAgent` / `scheduleAgentPolish` / `getDailyStarlanguageAgent(model).generate(` | generateCard 用 `composeDailyCard(evidence, today)`;路由中不得出现模型调用与改写 | BUG-1109 | +| `tests/daily-starlanguage.test.ts`(引擎层用例) | route 引用 `composeDailyStarlanguageCard` | route 引用 `composeDailyCard`(其兜底仍是旧函数) | BUG-1109 | +| `tests/home-warm-return-lifecycle.test.tsx`(两条 BUG-1104 用例) | 预取请求只有 2 个 GET;挂载前只有 GET | 另有只读的 `POST /api/daily-starlanguage`;新增首帧含今日星语、无占位句的断言 | BUG-1110 | + +## 验证(Linux,Node 22.14) + +| 项 | 结果 | +|---|---| +| tsc | 0 错 | +| lint | 0 error,126 warning(同基线) | +| npm test 全量 | 基线 `82db6373`:4,408 / fail 24;本分支 4,414 / fail 24,失败名单逐条相同(无 Docker 的数据库 / 部署套件);新增 6 条,消失 0 条 | +| next build | `/` 仍 `○ Static` | +| 首屏 gzip-9 | 645,198 → 646,525 B(+1,327 B,+0.21%;预取引入今日星语键函数进入布局公共块) | + +## 未验证 + +- 真机:从星盘页刷新后点「新建对话」,首帧是否直接显示今日星语(需登录态与浏览器)。 +- 句子质量:请产品过目上面的审阅表。 +- 他人档案为当前人物时不预取今日星语,这种情况首帧仍可能先显示占位句。 diff --git a/frontend/DESIGN.md b/frontend/DESIGN.md index 0c514564..18a47e70 100644 --- a/frontend/DESIGN.md +++ b/frontend/DESIGN.md @@ -817,6 +817,8 @@ Agent 的 live 标记只有 `InlineSpinner` 一种。曾经并存的 canvas 小 **打开 `/` 落在哪里(2026-09-27,BUG-1052)。** 地址栏带 `?c=` 就进那条对话(分享链接、侧栏历史、在对话里刷新都靠它);带 `?new=1` 是新建对话;其余——登录后、手输网址、书签、刷新裸 `/`——一律是当前人物的空白首页(开场问候 + 输入框),已有一个没发过消息的新对话就复用它。不再记「上次在哪条对话」:离开首页、登录过期都不存任何返回记录,后台还在恢复的旧回答也不把空白首页切走。 +**今日星语改为固定句库,并随次级页预取(2026-09-30,BUG-1109 / BUG-1110)。** 首页问候下面那句不再由模型写:`lib/daily-starlanguage-library.ts` 按「今日月亮落在本命第几宫(1–12,只在出生时间已确认时用)× 月相(新月前后 / 渐盈 / 满月前后 / 渐亏,按平均朔望月推算,误差约 ±1 天)× 按日期轮换的变体」拼出 trend,action / caution 按宫各两条交替;没有宫位时按月亮所在星座写一句,再没有就用原来的证据句。服务端不再在后台让模型改写并覆盖缓存,所以同一天同一个人看到的永远是同一句。次级页空闲时的预取(BUG-1104)同时取本人今天这句,写进同一个暖快照槽位与本地存储;从星盘等页点「新建对话」进首页,首帧就是真句子,不再先显示「今天的星语还没写出来。」再换掉。他人档案作为当前人物时暂不预取(其资料在人物目录里),首帧仍按原规则。 + 首页只揭幕一次。揭幕前的加载屏先并行取账户 / 会话、模型目录和咨询状态,再等待 lookup、会话详情等依赖数据全部到齐;不设 8 秒硬超时,超过 `HOME_BOOTSTRAP_SLOW_MS = 20_000` 才进入现有全屏错误屏。错误屏的「重试」只取消旧请求、重载 provider 并局部重跑 bootstrap,不调用 `location.reload()`。揭幕后不得再出现任何阻塞等待或组件级 spinner:今日星语未到时问候下方副行显示静态文案「今天的星语还没写出来。」(不带 `aria-busy`),校正卡用无摘要文案,内容到达后静默替换。登录后的空首页只剩问候语、今日星语卡、生时校正卡和输入框,没有主题卡。切换到消息尚未缓存的会话时消息区留白并只给 `sr-only` 文案,不转圈;揭幕后按侧栏顺序后台预取最近 5 条会话(`SESSION_PREFETCH_COUNT`)让常见切换零等待。轨道环消失后不得再换一套动效继续等。 ### 首屏死屏兜底 diff --git a/frontend/docs/VOICE.md b/frontend/docs/VOICE.md index c6d6445b..6db8a520 100644 --- a/frontend/docs/VOICE.md +++ b/frontend/docs/VOICE.md @@ -356,3 +356,12 @@ Jyotisha 的可见文案是产品的一部分。正确性红线(真实性、 |---|---|---| | 你的事业宫在第十宫,D10 上升与本命一致。当前大运土星、副运木星,从度数上看今明两年有岗位变动的窗口。下面进入统一参数与原始结构。 | 今年你的事业是「外松内紧」:机会都在明面上,底下是规矩和身体在收紧。推这一年的是土星,木星只负责把结果修得体面。别去应土星的「磨」,去扮演它的「守」。
- 上半年只接写下来的承诺。 | 先回人问的方向,再按形状展开;度数留在骨架里。要点不是公文枚举。 | | 事业、财富、变动窗口、土星大运 | 推这一年的是土星,木星只负责把结果修得体面。 | 句子是完整句子,不是公文枚举。 | + +## 今日星语句库(2026-09-30,BUG-1109) + +- 首页问候下面的今日星语来自固定句库 `frontend/src/lib/daily-starlanguage-library.ts`,不走模型。改句子只改这个文件,全部句子的审阅表在 `docs/tasks/PROGRESS-daily-line-library-20260930.md`。 +- 一句 trend = 宫位句(说今天的心思落在哪类事上)+ 月相尾句(说今天适合推进还是收尾)。宫位句不写「第几宫」「宫主」「相位」「大运」,用生活里的词:工作、家、钱、朋友、休息。 +- action 是一件今天就能做完的具体小事;caution 是一件要留意的事,不吓人、不断言会发生什么。 +- 不写健康诊断、投资建议、法律意见;不写「一定」「必然」「注定」。 +- 出生时间没确认时不用宫位,只按月亮所在星座写,caution 固定为「出生时间尚未确认,这里只看大方向,不依赖具体分钟。」 + diff --git a/frontend/src/app/api/daily-starlanguage/route.ts b/frontend/src/app/api/daily-starlanguage/route.ts index 14288258..c8308c7a 100644 --- a/frontend/src/app/api/daily-starlanguage/route.ts +++ b/frontend/src/app/api/daily-starlanguage/route.ts @@ -1,42 +1,31 @@ import { NextResponse } from "next/server"; import { calendarDateInTimeZone, - composeDailyStarlanguageCard, dailyStarlanguageCacheKey, - dailyStarlanguageCardSchema, - dailyStarlanguagePrompt, evidenceFromDailyGuidance, - parseDailyStarlanguageText, type DailyStarlanguageCard, - type DailyStarlanguageEvidence, } from "@/lib/daily-starlanguage"; +import { composeDailyCard } from "@/lib/daily-starlanguage-library"; import { dailyProfilePayload } from "@/lib/global-birth-payloads"; -import { loadLanguageModelCatalog } from "@/lib/model-catalog"; import { consumeUserRequestRateLimit } from "@/lib/request-rate-limit"; import { globalBirthProfileFromAccountRow } from "@/lib/server-owned-birth-profile"; import { ConsultationSubjectError, loadSubjectBirth, subjectIdFromQuery } from "@/lib/subject-birth"; import { createServerSupabaseClient } from "@/lib/supabase/server"; -import { getDailyStarlanguageAgent } from "@/mastra"; export const runtime = "nodejs"; export const maxDuration = 20; type Profile = ReturnType; type BirthPayload = NonNullable>>; -type CardSource = "engine_evidence" | "agent"; +/** The line comes from a fixed library (BUG-1109); no model rewrites it later. */ +type CardSource = "library"; type CacheEntry = { readonly day: string; readonly card: DailyStarlanguageCard; readonly source: CardSource }; type Generated = - | { - readonly kind: "card"; - readonly card: DailyStarlanguageCard; - readonly source: CardSource; - readonly evidence: DailyStarlanguageEvidence; - } + | { readonly kind: "card"; readonly card: DailyStarlanguageCard; readonly source: CardSource } | { readonly kind: "failed"; readonly reason: "chart_unavailable" }; const jyotishApiBase = process.env.JYOTISH_API_BASE ?? "http://127.0.0.1:5200"; const engineTimeoutMs = 8_000; -const agentTimeoutMs = 45_000; const cacheLimit = 500; const state = globalThis as typeof globalThis & { @@ -102,52 +91,12 @@ async function loadDailyGuidance(payload: BirthPayload, today: string) { return packet; } -async function polishWithAgent(evidence: DailyStarlanguageEvidence): Promise { - 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 result = await getDailyStarlanguageAgent(model).generate( - [{ role: "user", content: dailyStarlanguagePrompt(evidence) }], - { - abortSignal: AbortSignal.timeout(agentTimeoutMs), - structuredOutput: { - schema: dailyStarlanguageCardSchema, - jsonPromptInjection: "inline", - }, - }, - ); - const structured = dailyStarlanguageCardSchema.safeParse(result.object); - if (structured.success) return structured.data; - return parseDailyStarlanguageText(result.text); -} - -function scheduleAgentPolish(key: string, today: string, evidence: DailyStarlanguageEvidence) { - void polishWithAgent(evidence) - .then((card) => { - if (!card) return; - const current = readCache(key, today); - if (current?.source === "agent") return; - writeCache(key, today, card, "agent"); - }) - .catch((error: unknown) => { - console.warn("daily_starlanguage_agent_polish_failed", error); - }); -} - async function generateCard(payload: BirthPayload, profile: Profile, today: string): Promise { 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), - source: "engine_evidence", - evidence, - }; + return { kind: "card", card: composeDailyCard(evidence, today), source: "library" }; } function unavailable(reason: string) { @@ -218,6 +167,5 @@ export async function POST(request: Request) { const generated = await inFlight; if (generated.kind === "failed") return unavailable(generated.reason); writeCache(key, today, generated.card, generated.source); - scheduleAgentPolish(key, today, generated.evidence); return ok(generated.card, generated.source); } diff --git a/frontend/src/lib/daily-starlanguage-library.ts b/frontend/src/lib/daily-starlanguage-library.ts new file mode 100644 index 00000000..6227472a --- /dev/null +++ b/frontend/src/lib/daily-starlanguage-library.ts @@ -0,0 +1,207 @@ +/** + * Home 「今日星语」 from a fixed sentence library (BUG-1109, product 2026-09-30: + * 「这句好像都是固定的,可以不走大模型,咱们设计一套」). No model call. + * + * Keys: the transit Moon's house counted from the natal ascendant (1–12, only + * when the birth time is confirmed — the house depends on the ascendant) × the + * lunar phase of the day (4) × a variant chosen from the date, so two days in a + * row in the same house read differently. Without a usable house the line falls + * back to the Moon's sign; without that, to the older evidence composer. + * + * Copy rules live in docs/VOICE.md (今日星语): plain, concrete, no fate, no + * promises, no medical / legal / money advice, no 宫主 / 相位 in the trend line. + */ + +import { composeDailyStarlanguageCard, type DailyStarlanguageCard, type DailyStarlanguageEvidence } from "./daily-starlanguage"; + +type Line2 = readonly [string, string]; +type Line3 = readonly [string, string, string]; + +export type LunarPhase = "new" | "waxing" | "full" | "waning"; + +export const HOUSE_LINES: Readonly> = { + 1: [ + "今天注意力更多回到自己身上,精神和身体的状态都更容易被察觉。", + "今天适合照顾自己的节奏,先把状态调顺,再去应付别的事。", + "今天你的想法和情绪都更直接,别人也更容易注意到你。", + ], + 2: [ + "今天钱和说话这两件事更容易被放大,花出去的和说出口的都值得多想一下。", + "今天适合盘一盘手头的资源,也适合把想说的话说得更稳一点。", + "今天对价格、收入这些具体数字更敏感,适合算清楚再决定。", + ], + 3: [ + "今天行动力不错,适合跑腿、联络,把拖着的小事一件件办掉。", + "今天消息往来多,短途出行和身边的兄弟朋友都可能找上你。", + "今天适合主动一点,写、说、发,都比等别人来找更有效。", + ], + 4: [ + "今天心思更容易回到家里,情绪起伏也更容易被家人察觉。", + "今天适合把住处或自己的情绪收拾一下,里面安定了,外面的事才顺。", + "今天更需要一个让自己安心的角落,别把所有事都排得太满。", + ], + 5: [ + "今天适合做点让自己开心的事,创作、玩乐或和喜欢的人相处都更顺。", + "今天表达欲更强,适合把一个想法做成小作品拿出来。", + "今天和孩子、恋人或兴趣有关的事,更容易带来好心情。", + ], + 6: [ + "今天适合处理日常杂事和待办清单,按部就班反而最省力。", + "今天身体和作息的小信号更明显,适合把一个习惯调整回来。", + "今天工作里的琐碎事会多一些,一件件处理掉就好。", + ], + 7: [ + "今天身边的人更重要,合作和相处里的细节更容易被放大。", + "今天适合和伴侣或合作方把事情对齐,一起商量比自己硬扛好。", + "今天别人的态度会影响你的心情,先听清对方想要什么。", + ], + 8: [ + "今天情绪可能更深一点,适合面对一件一直不愿碰的事。", + "今天容易想得多,把担心写下来,比在脑子里打转轻松。", + "今天适合处理共同的钱物或旧账,慢一点、细一点。", + ], + 9: [ + "今天适合学点新东西,读一篇长文或听一次课都会有收获。", + "今天视野更容易打开,远方的人和事可能带来新想法。", + "今天适合想想更长远的方向,别只盯着眼前这一件。", + ], + 10: [ + "今天工作和对外的事更显眼,你的表现更容易被人看到。", + "今天适合把一项成果推进一步,让该知道的人知道。", + "今天责任感会重一点,先做最要紧的那件,别被杂事拉走。", + ], + 11: [ + "今天朋友和圈子更活跃,一次交流可能带来有用的消息。", + "今天适合联系老朋友或参加一次聚会,关系会带来机会。", + "今天心里的愿望更容易说出口,也更容易得到别人的回应。", + ], + 12: [ + "今天更需要休息和独处,把节奏放慢不是偷懒。", + "今天适合收尾和整理,不急着开新头。", + "今天花钱和精力都容易悄悄流走,留意一下去向。", + ], +}; + +export const PHASE_TAILS: Readonly> = { + new: ["新月前后,适合定一个小开头。", "月亮刚要重新变圆,先想清楚再动手。"], + waxing: ["月亮渐盈,适合往前推一步。", "月亮渐盈,做加法比做减法顺。"], + full: ["满月前后,情绪容易满,先稳住再回应。", "满月前后,适合把结果摆出来看看。"], + waning: ["月亮渐亏,适合收尾和清理。", "月亮渐亏,做减法比开新头顺。"], +}; + +export const HOUSE_ACTIONS: Readonly> = { + 1: ["早点睡或出门走二十分钟,把状态先养好。", "给自己定一件今天一定做完的小事。"], + 2: ["记下今天的每笔开销,或把一个价格重新核一遍。", "开口前先想好一句最想表达的话。"], + 3: ["把拖了最久的那条消息今天发出去。", "列三件小事,逐一办掉。"], + 4: ["收拾一个角落,或给家里人打个电话。", "晚上留半小时给自己,什么都不排。"], + 5: ["花一小时做一件纯粹因为喜欢而做的事。", "把一个点子写下来或画出来。"], + 6: ["从待办里挑一件今天能做完的,先做它。", "调一个作息:早睡半小时,或少喝一杯咖啡。"], + 7: ["主动约对方,把一件没说清的事聊清楚。", "先问一句「你怎么看」,再说自己的想法。"], + 8: ["把最担心的一件事写下来,再写下一步怎么做。", "整理一笔共同账目或一份旧文件。"], + 9: ["读完一篇收藏很久的文章。", "给远方的人发一条问候。"], + 10: ["把这周的一项进展整理成一段话,发给相关的人。", "先完成最要紧的一件,再回消息。"], + 11: ["联系一个很久没聊的朋友。", "在群里分享一条对别人有用的信息。"], + 12: ["给自己留一段不看手机的时间。", "清一清订阅和自动扣费。"], +}; + +export const HOUSE_CAUTIONS: Readonly> = { + 1: ["别因为一时情绪就做决定。", "别把自己排得太满。"], + 2: ["冲动消费和气话都容易在今天冒出来。", "别为了面子答应超出预算的事。"], + 3: ["消息多,容易看漏细节,重要的事再确认一遍。", "别同时开太多头。"], + 4: ["别把外面的烦心事带回家发泄。", "情绪低的时候别做大决定。"], + 5: ["开心归开心,别在兴头上许下太大的承诺。", "别拿自己和别人比。"], + 6: ["别让琐事占满整天,留出做正事的时间。", "身体有小不适别硬撑,该休息就休息。"], + 7: ["别把对方的一句话想得太重。", "意见不合时,先别急着争对错。"], + 8: ["别在焦虑的时候翻旧账。", "涉及钱物的约定,写清楚再定。"], + 9: ["别急着说服别人接受你的看法。", "远途安排多留一点余量。"], + 10: ["别为了表现把自己逼得太紧。", "对外说话前,再看一遍措辞。"], + 11: ["人多的场合,别轻易答应帮忙。", "别被别人的节奏带着跑。"], + 12: ["别用熬夜来换独处的时间。", "留意那些看不见的开销。"], +}; + +const SIGN_ZH: Readonly> = { + Aries: "白羊座", Taurus: "金牛座", Gemini: "双子座", Cancer: "巨蟹座", Leo: "狮子座", Virgo: "处女座", + Libra: "天秤座", Scorpio: "天蝎座", Sagittarius: "射手座", Capricorn: "摩羯座", Aquarius: "水瓶座", Pisces: "双鱼座", +}; + +/** Used when the house cannot be counted (birth time not confirmed, or no ascendant). */ +export const SIGN_MOODS: Readonly> = { + Aries: "整体节奏偏快,适合先动起来。", + Taurus: "整体节奏偏稳,适合把手上的事做扎实。", + Gemini: "消息和想法都多,适合沟通和学习。", + Cancer: "情绪更柔软,适合照顾家和身边的人。", + Leo: "表现欲更强,适合把自己的想法拿出来。", + Virgo: "适合整理细节,把清单一件件勾掉。", + Libra: "更在意关系和分寸,适合商量和协调。", + Scorpio: "感受更深,适合专注处理一件要紧事。", + Sagittarius: "心情更开阔,适合学习或出门走走。", + Capricorn: "更务实,适合推进需要耐心的事。", + Aquarius: "更想跳出常规,适合换个角度想问题。", + Pisces: "更感性也更容易累,适合放慢一点。", +}; + +const SIGN_ACTIONS: Line2 = ["把今天要做的事收成一件,做完再看下一件。", "给自己留一段安静的时间,想清楚再动。"]; +const UNVERIFIED_CAUTION = "出生时间尚未确认,这里只看大方向,不依赖具体分钟。"; +const SIGN_CAUTIONS: Line2 = ["这是今天的大致气氛,不是一定会发生的事。", "别让一时的情绪替你做决定。"]; + +const DAY_MS = 86_400_000; +/** A known new moon: 2000-01-06 18:14 UTC. */ +const NEW_MOON_EPOCH_MS = Date.UTC(2000, 0, 6, 18, 14); +/** Mean synodic month in days. Mean phase, so accurate to about ±1 day — enough for four broad phases. */ +const SYNODIC_DAYS = 29.530588853; + +/** Days since 1970-01-01 for a `YYYY-MM-DD` date, or 0 if it does not parse. */ +function dayNumber(today: string): number { + const match = /^(\d{4})-(\d{2})-(\d{2})/.exec(today); + if (!match) return 0; + return Math.floor(Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3])) / DAY_MS); +} + +/** Moon age in days at local noon of `today`, from the mean synodic month. */ +export function moonAgeDays(today: string): number { + const noon = dayNumber(today) * DAY_MS + DAY_MS / 2; + const age = ((noon - NEW_MOON_EPOCH_MS) / DAY_MS) % SYNODIC_DAYS; + return age < 0 ? age + SYNODIC_DAYS : age; +} + +export function lunarPhase(today: string): LunarPhase { + const age = moonAgeDays(today); + if (age < 3.7 || age >= 25.8) return "new"; + if (age < 11.1) return "waxing"; + if (age < 18.5) return "full"; + return "waning"; +} + +function houseOf(evidence: DailyStarlanguageEvidence): number | null { + if (!evidence.birthTimeVerified) return null; + const house = evidence.transitMoonHouse; + return house && house >= 1 && house <= 12 ? house : null; +} + +/** Deterministic pick from the date; consecutive days never repeat a three-variant line. */ +function pick(lines: readonly T[], day: number, stride = 1): T { + return lines[Math.floor(day / stride) % lines.length]!; +} + +export function composeDailyCard(evidence: DailyStarlanguageEvidence, today: string = evidence.today): DailyStarlanguageCard { + const day = dayNumber(today); + const tail = pick(PHASE_TAILS[lunarPhase(today)], day, 3); + const house = houseOf(evidence); + if (house) { + return { + trend: `${pick(HOUSE_LINES[house]!, day)}${tail}`, + action: pick(HOUSE_ACTIONS[house]!, day), + caution: pick(HOUSE_CAUTIONS[house]!, day), + }; + } + const sign = evidence.transitMoonSign; + const mood = sign ? SIGN_MOODS[sign] : undefined; + if (sign && mood) { + return { + trend: `今天月亮在${SIGN_ZH[sign]},${mood}${tail}`, + action: pick(SIGN_ACTIONS, day), + caution: evidence.birthTimeVerified ? pick(SIGN_CAUTIONS, day) : UNVERIFIED_CAUTION, + }; + } + return composeDailyStarlanguageCard(evidence); +} diff --git a/frontend/src/lib/home-types.ts b/frontend/src/lib/home-types.ts index 9fa9512b..852c42d2 100644 --- a/frontend/src/lib/home-types.ts +++ b/frontend/src/lib/home-types.ts @@ -165,7 +165,7 @@ export type DailyStarlanguageCard = { trend: string; action: string; caution: st export type DailyStarlanguageApiResponse = { status?: "ok" | "unavailable" | "unauthenticated"; card?: DailyStarlanguageCard; - source?: "engine_evidence" | "engine_evidence_cache" | "agent" | "agent_cache"; + source?: "library" | "library_cache" | "engine_evidence" | "engine_evidence_cache" | "agent" | "agent_cache"; claim_status?: "exploratory_unvalidated"; boundary?: "not_deterministic_prediction"; }; diff --git a/frontend/src/lib/home-warm-prefetch.ts b/frontend/src/lib/home-warm-prefetch.ts index caa9df70..968a745d 100644 --- a/frontend/src/lib/home-warm-prefetch.ts +++ b/frontend/src/lib/home-warm-prefetch.ts @@ -16,8 +16,10 @@ * Pure module state, no React. */ -import { fetchModelCatalog, fetchRectificationEntrySummary } from "./home-cloud-sync"; -import { isCompleteHomeWarmSnapshot, readHomeWarmSnapshot, writeHomeWarmSnapshot } from "./home-warm-snapshot"; +import { fetchDailyStarlanguage, fetchModelCatalog, fetchRectificationEntrySummary, writeStoredDailyStarlanguage } from "./home-cloud-sync"; +import { dailyStarlanguageWarmKey } from "./home-warm-start"; +import { isCompleteHomeWarmSnapshot, readHomeWarmDaily, readHomeWarmSnapshot, rememberHomeWarmDaily, writeHomeWarmSnapshot } from "./home-warm-snapshot"; +import type { DailyStarlanguageState, Profile } from "./home-types"; import type { PublicLanguageModelCatalog } from "./public-models"; import type { RectificationEntrySummary } from "./rectification-entry"; @@ -71,3 +73,42 @@ export function prefetchHomeWarmSnapshot( export function resetHomeWarmPrefetchForTests(): void { inflight = null; } + +export type HomeWarmDailyIo = Readonly<{ + fetchDailyStarlanguage: (signal: AbortSignal | undefined, subjectId: string) => Promise; + now: () => Date; +}>; + +const defaultDailyIo: HomeWarmDailyIo = { + fetchDailyStarlanguage: (signal, subjectId) => fetchDailyStarlanguage(signal ?? new AbortController().signal, subjectId), + now: () => new Date(), +}; + +/** + * Today's home line for the current person (BUG-1110). Without it, Home's first + * frame after 「新建对话」 from a secondary page showed the placeholder 「今天的星语 + * 还没写出来。」 and swapped in the line a moment later. Same fetch and the same + * key the binder uses; stored exactly where the binder stores it. Read-only; + * nothing is written when the person has no card or the request fails. + */ +export async function prefetchHomeWarmDaily( + accountId: string, + subjectId: string, + profile: Profile | null, + isCurrent: () => boolean, + io: HomeWarmDailyIo = defaultDailyIo, + signal?: AbortSignal, +): Promise { + const key = dailyStarlanguageWarmKey(subjectId, profile, io.now()); + if (!accountId || !key) return false; + if (readHomeWarmDaily({ accountId, subjectId, ...key })) return false; + try { + const next = await io.fetchDailyStarlanguage(signal, subjectId); + if (signal?.aborted || !isCurrent() || next.kind !== "ready") return false; + rememberHomeWarmDaily({ accountId, subjectId, ...key, card: next.card }); + writeStoredDailyStarlanguage(accountId, { ...key, card: next.card }); + return true; + } catch { + return false; + } +} diff --git a/frontend/src/lib/session-list-context.tsx b/frontend/src/lib/session-list-context.tsx index a24a229f..4bf2d568 100644 --- a/frontend/src/lib/session-list-context.tsx +++ b/frontend/src/lib/session-list-context.tsx @@ -19,7 +19,7 @@ import { bindCurrentSubjectAccount, invalidateAccountFlight, invalidateSubjectCa import { sessionMatchesSubject } from "@/lib/chat-profile-picker-model"; import { readSessions } from "@/lib/home-cloud-sync"; import { clearHomeWarmSnapshot } from "@/lib/home-warm-snapshot"; -import { prefetchHomeWarmSnapshot } from "@/lib/home-warm-prefetch"; +import { prefetchHomeWarmDaily, prefetchHomeWarmSnapshot } from "@/lib/home-warm-prefetch"; import { isProfileComplete, readProfile } from "@/lib/home-profile"; import type { Account, ChatSession } from "@/lib/home-types"; import { SESSION_PAGE_SIZE } from "@/lib/session-cursor"; @@ -224,7 +224,13 @@ export function SessionListProvider({ children }: { children: ReactNode }) { if (!prefetchAccountId || window.location.pathname === "/") return; const controller = new AbortController(); const run = () => { - void prefetchHomeWarmSnapshot(prefetchAccountId, () => !controller.signal.aborted, undefined, controller.signal); + const isCurrent = () => !controller.signal.aborted; + void prefetchHomeWarmSnapshot(prefetchAccountId, isCurrent, undefined, controller.signal); + // Only the account holder's own line: other people's profiles live in the + // subject catalog, and a missing line there just keeps the old behaviour. + if (readCurrentSubjectId() === "self" && account) { + void prefetchHomeWarmDaily(prefetchAccountId, "self", readProfile(account.profile), isCurrent, undefined, controller.signal); + } }; // Safari has no requestIdleCallback; a short timeout keeps it off the first paint. const idle = "requestIdleCallback" in window; @@ -234,7 +240,7 @@ export function SessionListProvider({ children }: { children: ReactNode }) { if (idle) window.cancelIdleCallback(handle); else window.clearTimeout(handle); }; - }, [prefetchAccountId]); + }, [account, prefetchAccountId]); const reload = useCallback(() => reloadRef.current(), []); diff --git a/frontend/tests/daily-starlanguage-library.test.ts b/frontend/tests/daily-starlanguage-library.test.ts new file mode 100644 index 00000000..be3d1d22 --- /dev/null +++ b/frontend/tests/daily-starlanguage-library.test.ts @@ -0,0 +1,116 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import test from "node:test"; + +import { dailyStarlanguageCardSchema, type DailyStarlanguageEvidence } from "../src/lib/daily-starlanguage.ts"; +import { + HOUSE_ACTIONS, + HOUSE_CAUTIONS, + HOUSE_LINES, + PHASE_TAILS, + SIGN_MOODS, + composeDailyCard, + lunarPhase, + moonAgeDays, + type LunarPhase, +} from "../src/lib/daily-starlanguage-library.ts"; + +// BUG-1109: the home 「今日星语」 comes from a fixed library, not a model. + +function evidence(overrides: Partial = {}): DailyStarlanguageEvidence { + return { + today: "2026-09-30", + birthTimeVerified: true, + ascendantSign: "Aries", + moonSign: "Cancer", + transitMoonSign: "Capricorn", + transitMoonHouse: 10, + vimshottari: { mahadasha: "Saturn", antardasha: null, remainingDays: null, nakshatra: null }, + narayanaSign: null, + divisional: [], + transit: null, + functionalBenefics: [], + functionalMalefics: [], + missingLayers: [], + ...overrides, + }; +} + +/** One date per phase and per variant: walk forward until every (phase, variant) pair is covered. */ +function datesCovering(): Map { + const found = new Map(); + const start = Date.UTC(2026, 0, 1); + for (let offset = 0; offset < 400 && found.size < 12; offset += 1) { + const date = new Date(start + offset * 86_400_000).toISOString().slice(0, 10); + const day = Math.floor((start + offset * 86_400_000) / 86_400_000); + const key = `${lunarPhase(date)}:${day % 3}`; + if (!found.has(key)) found.set(key, date); + } + return found; +} + +test("the library is complete: 12 houses × 3 lines, 4 phases × 2 tails, 2 actions and 2 cautions per house, 12 sign moods", () => { + for (let house = 1; house <= 12; house += 1) { + assert.equal(HOUSE_LINES[house]?.length, 3, `house ${house}`); + assert.equal(HOUSE_ACTIONS[house]?.length, 2); + assert.equal(HOUSE_CAUTIONS[house]?.length, 2); + } + for (const phase of ["new", "waxing", "full", "waning"] as LunarPhase[]) assert.equal(PHASE_TAILS[phase].length, 2); + assert.equal(Object.keys(SIGN_MOODS).length, 12); +}); + +test("every house × phase × variant composes a valid card within the schema limits", () => { + const dates = datesCovering(); + assert.equal(dates.size, 12, "all four phases × three variants are reachable"); + let cards = 0; + for (let house = 1; house <= 12; house += 1) { + for (const date of dates.values()) { + const card = composeDailyCard(evidence({ transitMoonHouse: house, today: date }), date); + const parsed = dailyStarlanguageCardSchema.safeParse(card); + assert.ok(parsed.success, `${house} ${date}: ${JSON.stringify(card)}`); + assert.ok(card.trend.length <= 90 && card.action.length <= 90 && card.caution.length <= 90); + assert.doesNotMatch(card.trend, /宫主|相位|大运|第\d+宫/, "no jargon in the trend line"); + cards += 1; + } + } + assert.equal(cards, 144); +}); + +test("same date, same card; consecutive days in the same house read differently", () => { + const today = composeDailyCard(evidence(), "2026-09-30"); + assert.deepEqual(composeDailyCard(evidence(), "2026-09-30"), today); + const tomorrow = composeDailyCard(evidence(), "2026-10-01"); + assert.notEqual(tomorrow.trend, today.trend); +}); + +test("the lunar phase follows the synodic month from a known new moon", () => { + // 2000-01-06 was a new moon; about 14.8 days later a full moon. + assert.equal(lunarPhase("2000-01-06"), "new"); + assert.equal(lunarPhase("2000-01-21"), "full"); + assert.equal(lunarPhase("2000-01-13"), "waxing"); + assert.equal(lunarPhase("2000-01-28"), "waning"); + // Known new moon 2026-09-11 (±1 day) and full moon 2026-09-26. + assert.equal(lunarPhase("2026-09-11"), "new"); + assert.equal(lunarPhase("2026-09-26"), "full"); + const age = moonAgeDays("2026-09-11"); + assert.ok(age < 1.5 || age > 28, `new moon within about a day, age ${age}`); +}); + +test("without a confirmed birth time the house is not used; the line falls back to the Moon's sign", () => { + const card = composeDailyCard(evidence({ birthTimeVerified: false }), "2026-09-30"); + assert.match(card.trend, /^今天月亮在摩羯座,/); + assert.match(card.caution, /出生时间尚未确认/); + assert.ok(dailyStarlanguageCardSchema.safeParse(card).success); + const noHouse = composeDailyCard(evidence({ transitMoonHouse: null }), "2026-09-30"); + assert.match(noHouse.trend, /^今天月亮在摩羯座,/); + // Neither house nor sign: the older evidence composer still gives a valid card. + const bare = composeDailyCard(evidence({ transitMoonHouse: null, transitMoonSign: null }), "2026-09-30"); + assert.ok(dailyStarlanguageCardSchema.safeParse(bare).success); +}); + +test("the route calls no model for the daily line", () => { + const route = readFileSync(new URL("../src/app/api/daily-starlanguage/route.ts", import.meta.url), "utf8"); + assert.match(route, /composeDailyCard\(evidence, today\)/); + assert.match(route, /source: "library"/); + assert.doesNotMatch(route, /@\/mastra|getDailyStarlanguageAgent|loadLanguageModelCatalog|generate\(/); +}); diff --git a/frontend/tests/daily-starlanguage.test.ts b/frontend/tests/daily-starlanguage.test.ts index 96094f7a..357775ed 100644 --- a/frontend/tests/daily-starlanguage.test.ts +++ b/frontend/tests/daily-starlanguage.test.ts @@ -293,11 +293,12 @@ test("the homepage card is engine-backed first, with Agent polish off the reques 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/); - assert.match(route, /scheduleAgentPolish\(key, today, generated\.evidence\)/); - assert.match(route, /getDailyStarlanguageAgent\(model\)\.generate\(/); + // 原值:generateCard 用 composeDailyStarlanguageCard(evidence) 先出一张,随后 scheduleAgentPolish 让模型改写并覆盖缓存 + // 新值:generateCard 用句库 composeDailyCard(evidence, today);路由里不再有模型调用、改写或覆盖 + // 原因:BUG-1109,产品 2026-09-30 选定「自己写一套句库」,今日星语不走大模型 + assert.match(generateCard, /composeDailyCard\(evidence, today\)/); + assert.doesNotMatch(route, /getDailyStarlanguageAgent|polishWithAgent|scheduleAgentPolish|loadLanguageModelCatalog/); + assert.equal(polish, "", "no model polish step remains"); for (const source of [route, page]) { assert.doesNotMatch(source, /先收束,再推进|执行力比灵感更重要|适合观察资源流向/); @@ -365,5 +366,6 @@ test("no single engine layer can take the whole card down without saying which o 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/); + // 原值:route 引用 composeDailyStarlanguageCard;新值:引用句库 composeDailyCard(其兜底仍是 composeDailyStarlanguageCard);原因:BUG-1109 + assert.match(route, /composeDailyCard/); }); diff --git a/frontend/tests/home-warm-return-lifecycle.test.tsx b/frontend/tests/home-warm-return-lifecycle.test.tsx index b2823662..6bd44bd3 100644 --- a/frontend/tests/home-warm-return-lifecycle.test.tsx +++ b/frontend/tests/home-warm-return-lifecycle.test.tsx @@ -46,6 +46,8 @@ const terminalSummary = { has_resumable_case: false, has_terminal_case_with_time const emptySummary = { has_resumable_case: false, has_terminal_case_with_time: false, latest_resumable: null, latest_terminal: null }; type Mode = { + /** Today's home line; null answers 204 (no card), as before BUG-1110. */ + daily: { trend: string; action: string; caution: string } | null; /** Paths whose response never arrives (to prove nothing waits for them). */ hang: Set; /** Paths answered with 500 (a prefetch that fails must leave nothing behind). */ @@ -59,7 +61,7 @@ type Mode = { function installApi() { const originalFetch = globalThis.fetch; const requests: string[] = []; - const mode: Mode = { hang: new Set(), fail: new Set(), models: catalogPro, summary: terminalSummary, consultStatus: null, accountUser: USER }; + const mode: Mode = { daily: null, hang: new Set(), fail: new Set(), models: catalogPro, summary: terminalSummary, consultStatus: null, accountUser: USER }; const json = (body: unknown, status = 200) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } }); globalThis.fetch = (async (input: unknown, init?: RequestInit) => { const url = new URL(String(input), "http://localhost"); @@ -90,7 +92,9 @@ function installApi() { return json({ disposition: "readonly", caseId: CASE, sessionId: RECT, status: "completed", shouldStartOpening: false, skillVersion: "test" }); } if (path === `/api/rectification/cases/${CASE}`) return json({ turns: [], snapshot: null }); - if (path === "/api/daily-starlanguage") return new Response(null, { status: 204 }); + if (path === "/api/daily-starlanguage") { + return mode.daily ? json({ status: "ok", card: mode.daily, source: "library" }) : new Response(null, { status: 204 }); + } return json({ error: "not_mocked" }, 404); }) as typeof fetch; return { requests, mode, restore: () => { globalThis.fetch = originalFetch; } }; @@ -132,7 +136,7 @@ function createTab() { } type Tab = ReturnType; -type FirstCommit = { loading: boolean; title: string | null; url: string; requests: number }; +type FirstCommit = { loading: boolean; title: string | null; url: string; requests: number; text: string }; async function openDocument(tab: Tab, href: string, requests: string[]) { const h = createClientLifecycleHarness(); @@ -256,7 +260,7 @@ async function openDocument(tab: Tab, href: string, requests: string[]) { function FirstCommitProbe() { useLayoutEffect(() => { const v = view(); - firstCommits.push({ loading: v.loading, title: v.title, url: v.url, requests: requests.length }); + firstCommits.push({ loading: v.loading, title: v.title, url: v.url, requests: requests.length, text: h.container.text }); }, []); return null; } @@ -650,12 +654,17 @@ for (const path of ["/chart", "/reports"] as const) { const api = installApi(); let doc: Doc | undefined; try { + api.mode.daily = { trend: "今天工作和对外的事更显眼,你的表现更容易被人看到。月亮渐盈,适合往前推一步。", action: "先完成最要紧的一件,再回消息。", caution: "别为了表现把自己逼得太紧。" }; doc = await openDocument(tab, path, api.requests); const { readHomeWarmSnapshot, isCompleteHomeWarmSnapshot } = await import("../src/lib/home-warm-snapshot.ts"); - await doc.settle(() => isCompleteHomeWarmSnapshot(readHomeWarmSnapshot(USER)), "the background prefetch"); + await doc.settle(() => isCompleteHomeWarmSnapshot(readHomeWarmSnapshot(USER)) && api.requests.includes("POST /api/daily-starlanguage"), "the background prefetch"); + for (let i = 0; i < 3; i += 1) await doc.h.idle(); const prefetch = api.requests.filter((line) => !line.startsWith("GET /api/account") && !line.startsWith("GET /api/sessions") && !line.startsWith("GET /api/chart-profiles")); - assert.deepEqual(prefetch.sort(), ["GET /api/models", "GET /api/rectification/cases/entry-summary"], `read-only prefetch only: ${api.requests.join(", ")}`); - assert.ok(api.requests.every((line) => line.startsWith("GET ")), "nothing but GETs before Home mounts"); + // 原值:预取只有 GET /api/models 与 GET /api/rectification/cases/entry-summary,且挂载前只有 GET + // 新值:另加 POST /api/daily-starlanguage(只读:按日计算并返回今天这句,不写任何数据);其余请求仍全是 GET + // 原因:BUG-1110——没有预取今天这句时,首页首帧先显示「今天的星语还没写出来。」再换成真句子 + assert.deepEqual(prefetch.sort(), ["GET /api/models", "GET /api/rectification/cases/entry-summary", "POST /api/daily-starlanguage"], `read-only prefetch only: ${api.requests.join(", ")}`); + assert.ok(api.requests.every((line) => line.startsWith("GET ") || line === "POST /api/daily-starlanguage"), "nothing but GETs (and the read-only daily line) before Home mounts"); for (const waited of WAITED_PATHS) api.mode.hang.add(waited); const before = api.requests.length; await doc.followLink("新建对话"); @@ -665,6 +674,8 @@ for (const path of ["/chart", "/reports"] as const) { assert.equal(first.title, "新对话"); assert.equal(first.requests, before, "ready before any request was sent"); assert.equal(doc.view().rectificationCard, "再次校正", "the prefetched summary drives the card"); + assert.ok(first.text.includes("你的表现更容易被人看到"), "today's line is in the very first frame"); + assert.ok(!first.text.includes("今天的星语还没写出来"), "no placeholder that later swaps (BUG-1110)"); assert.deepEqual(doc.errors, []); } finally { await doc?.close(); From ce5054c003be76a83ba75f56b569abe6d0522958 Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Wed, 30 Sep 2026 01:26:24 +0800 Subject: [PATCH 3/4] docs: changelog, progress and device checklist for the 09-30 01:00 feedback (BUG-1109..1114) Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE --- CHANGELOG.md | 8 ++++++++ docs/tasks/PROGRESS-surface-feedback-20260929.md | 14 ++++++++++++++ docs/testing/surface-feedback-20260929.md | 11 +++++++++++ 3 files changed, 33 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d00650e5..5ece7281 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # 印度占星 Skill 更新日志 +## 2026-09-30 — 首页去掉校正提示、星盘类型文字完整显示、加载改为轨道环动画、「那一刻的天空」换小星座图标(待验收) + +- 首页不再显示「上次那次校正还没完成,可以在历史对话里接着做。」(BUG-1111)。 +- 星盘类型面板里每格的说明不再被截断,最多两行(BUG-1112)。 +- 星盘、星历、报告列表加载时显示首页同款的轨道环动画,不再写「这一张盘还没拿到。」这类句子(BUG-1113)。 +- 「那一刻的天空」按钮图标换成自绘的小星座(BUG-1114)。 +- Skill 版本不 bump。不改数据库结构。 + ## 2026-09-30 — 首页今日星语改成自己写的句库;从别的页面进首页不再「先一句、再换一句」(待验收) - 问候下面那句今日星语不再调用大模型,改为按「今天月亮落在你本命的哪一宫 × 月相 × 日期」从一套固定句库里取,秒出、不花钱;同一天同一个人看到的是同一句,第二天换一句(BUG-1109)。 diff --git a/docs/tasks/PROGRESS-surface-feedback-20260929.md b/docs/tasks/PROGRESS-surface-feedback-20260929.md index 1af5714a..2c2b04c6 100644 --- a/docs/tasks/PROGRESS-surface-feedback-20260929.md +++ b/docs/tasks/PROGRESS-surface-feedback-20260929.md @@ -62,3 +62,17 @@ - 既有断言改动:`report-export-drawer.test.tsx` 7 条测试名全部保留、主体按新设计重写(每条开头写原值 / 新值 / 原因);`report-english-edition.test.tsx`「the export drawer suggests English…」、`surface-feedback-20260929.test.tsx` 手机面板断言、`report-buttons-scope-20260928.test.ts` 选中态不得用 action-soft 的约束迁到新选中态,均写明三栏。 验证(第二、三轮合并,Claude 实跑):tsc 0;lint 0 error / 126 warning(同基线);全量 4,408 / fail 24,失败名单与基线逐条相同;新增测试名 3 条均为 BUG 号改号(1105~1107 → 1106~1108,另一会话已预留 1105)后的同一组测试,无其他名单变化;`/` Static;首屏 gzip 645,198 B(较上一轮 +20 B)。 + +## 第四轮:产品 2026-09-30 01:00 反馈 + +| # | 反馈 | 处理 | BUG | +|---|---|---|---| +| 1 | 首页「上次那次校正还没完成……」去掉 | `rectificationEntryHint(rectificationForSelf)`,本人一律无提示 | 1111 | +| 2 | 星盘类型格子文字溢出 / 显示不全 | 说明行行高 1.4、最多两行,格子 56 → 64px | 1112 | +| 3 | 星盘加载不要写「这一张盘还没拿到」,做加载动效 | 共用 `OrbitWaiting`(首页轨道环 40px),句子只给读屏器 | 1113 | +| 4 | 星历页同样问题 | 同上(`SecondaryPageShell` 的 waiting,报告列表一并) | 1113 | +| 5 | 「那一刻的天空」图标换掉 | 自绘小星座(产品选定) | 1114 | +| 6 | 今日星语不走大模型,设计一套 | fork 子代理:句库 `lib/daily-starlanguage-library.ts`,审阅表见 `PROGRESS-daily-line-library-20260930.md` | 1109 | +| 7 | 跳转时突然变一下 | fork 子代理:暖启动没有当天星语 → 先占位句后换;后台模型改写又会换句。预取当天星语、去掉后台改写 | 1110 | + +既有断言改动(本轮 Claude 部分):`starter-home-polish-20260926.test.tsx` 三处、`rectification-agentic-entry.test.ts` 一处(未完成校正提示删除)、`chart-type-picker.test.tsx` 一处(56 → 64px),均写三栏。 diff --git a/docs/testing/surface-feedback-20260929.md b/docs/testing/surface-feedback-20260929.md index eef9fb99..20034309 100644 --- a/docs/testing/surface-feedback-20260929.md +++ b/docs/testing/surface-feedback-20260929.md @@ -27,3 +27,14 @@ | 16 | 点「导出」 | 面板从下往上弹出:标题「导出报告」和 ×;「中文 / English」;「整篇报告」(已选中)和「自选章节」两张卡;底部按钮「导出整篇 · 约 … KB」 | | | 17 | 点「自选章节」 | 下面出现清单:「正文 7/7」展开、「分盘」「核对表」收起;取消一章后「正文」前的框变成 −,按钮变成「导出 6 项 · 约 … KB」 | | | 18 | 在面板里点 English,再点导出 | 下载的文件名以 `-EN.md` 结尾,内容是英文;页面本身仍是原来的语言 | | + +## 第四轮(2026-09-30 01:00 反馈) + +| # | 操作 | 应该看到 | 结果 | +|---|---|---|---| +| 19 | 有未完成校正时打开首页 | 入口下方没有「上次那次校正还没完成……」 | | +| 20 | 星盘页点「≡」打开星盘类型 | 每格下面的说明完整显示(长的折两行),字不被切 | | +| 21 | 刷新星盘页 / 星历页 | 加载时是转动的轨道环,没有「还没拿到」字样 | | +| 22 | 看星盘页右上角 | 「那一刻的天空」前面是一个小星座图标(几颗星连线),不是星芒 | | +| 23 | 首页「欢迎回来」下面那句 | 类似「今天工作和对外的事更显眼……渐盈,……」,同一天多次进首页是同一句,隔天换一句 | | +| 24 | 在星盘页刷新,等 3 秒,点「新建对话」 | 首页第一眼就是那句星语,不先出现「今天的星语还没写出来」再换掉 | | From 5208f19b7609211c5de799ca26b17da403f4b23e Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Wed, 30 Sep 2026 08:23:36 +0800 Subject: [PATCH 4/4] docs(tasks): verification for the 09-30 01:00 feedback round Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE --- docs/tasks/PROGRESS-surface-feedback-20260929.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/tasks/PROGRESS-surface-feedback-20260929.md b/docs/tasks/PROGRESS-surface-feedback-20260929.md index 2c2b04c6..58da0360 100644 --- a/docs/tasks/PROGRESS-surface-feedback-20260929.md +++ b/docs/tasks/PROGRESS-surface-feedback-20260929.md @@ -76,3 +76,5 @@ | 7 | 跳转时突然变一下 | fork 子代理:暖启动没有当天星语 → 先占位句后换;后台模型改写又会换句。预取当天星语、去掉后台改写 | 1110 | 既有断言改动(本轮 Claude 部分):`starter-home-polish-20260926.test.tsx` 三处、`rectification-agentic-entry.test.ts` 一处(未完成校正提示删除)、`chart-type-picker.test.tsx` 一处(56 → 64px),均写三栏。 + +验证(第四轮,含 fork 提交 `ab51e038` 合并后):tsc 0;lint 0 error / 126 warning(同基线);全量 4,418 / fail 24,失败名单与基线逐条相同,新增 10 条、消失 0 条;`/` Static;首屏 gzip 646,480 B(较上轮 +0.2%,主要是次级页预取当天星语的键函数)。