diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index cfca3390..aa82eb20 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -3978,10 +3978,11 @@ ## BUG-269 | “今日星语”永久停在“正在结合你的星盘写今天的星语”:请求被一个反向的出生时间守卫拦住,从未发出 -- 状态:resolved(本地修复,待提交与发布) +- 状态:resolved(已发布 staging) - 首次发现:2026-08-18 - 最近更新:2026-08-18 - 影响面:`/` 首页“今日星语”卡片、首页 hero 的问候与标题、`/api/daily-starlanguage` 的失败归因与超时预算、Onboarding Agent 生成的欢迎语。 +- 后续修正:本记录「Agent 欢迎语落到 hero 说明行」的处置已被 BUG-270 推翻——该说明行经用户评审判定为累赘并删除,`onboarding.greeting` 因此重新回到无渲染点状态,处理见 BUG-270。 - 用户现象:staging 上出生资料完整的账号打开首页,“今日星语”卡片一直显示“正在结合你的星盘写今天的星语。”,永远不出现 Agent 写的文案。同一次反馈里还问到:首页那三个问题是不是写死的,以及首页标题“今天想先理清什么?”为什么没换成登录后按时段问候的形式。 - 触发条件:`birth_time_status` 为 `candidate`、`accepted` 或 `confirmed` 且已有可用出生时间的任何账号打开首页。也就是说,越是资料完整的账号越必然命中。 - 根因:三处独立问题,都在同一屏上。 @@ -3991,7 +3992,25 @@ - 修复:守卫改成 `!hydrated || !profileComplete || !personalChartAvailable`,与卡片渲染个人内容的条件对齐,并在服务端报 unavailable 时延迟 5 秒重试一次后才落到失败态,卸载时清理定时器。路由给 `/api/chart` 补 `.catch()`,把生成结果改成判别联合,失败原因区分 `chart_unavailable` / `model_unavailable` / `agent_generation_failed`;`engineTimeoutMs` 提到 20 秒、`agentTimeoutMs` 提到 45 秒,仍在 `maxDuration = 60` 之内。问候收敛成一套:`createStartGreeting` 拆出 `createStartGreetingParts`,返回 `{salutation, question}`,hero 第一行用 salutation、`h1` 用 question,选中变体在离开首页时重抽;删除 `starter-prompt.ts` 与 `greetingForHour`。客户端不再覆盖 Agent 欢迎语,`onboarding.greeting` 落到 hero 说明行并保留原静态文案作为兜底。 - 验证:线上先证明后端是好的——带登录 Cookie 直接调 staging `/api/daily-starlanguage`,冷路径 30.6 秒返回真实 `{trend, action, caution}`,第二次 1.6 秒命中 `agent_cache`;`/api/health` 确认部署 SHA 就是 `origin/staging` 头部,`jyotishApi` 检查为 ok,因此排除部署落后与引擎不可用。新增 4 条回归:每日星语请求条件必须与 `personalChartAvailable` 一致且源码中不得再出现 `birthTimeDisplayState(profile)`、失败必须重试一次且清理定时器、`/api/chart` 必须带 catch 且三种失败原因各自可辨、两个超时预算有下限断言;hero 断言 salutation/question 拆分与 Agent 欢迎语落点,并禁止 `starterPrompt`/`createStarterPrompt`/`greetingForHour` 复活。前端非数据库套件 1711/1724 通过,13 个失败全部是本机 Docker/PostgreSQL fixture(与 BUG-265 同一类环境失败,与本次无关);`tsc --noEmit` 与改动文件 ESLint 清洁。未做的验证:**没有在浏览器里看过修好后的首页**——本机没有 Python 引擎与模型密钥,无法起完整栈,卡片从 `pending` 到 `ready` 的实际观感、重试是否够用、以及 hero 换行后的排版都要等 staging 发布后确认。 - 防复发:一个界面元素的「取数条件」必须和它的「渲染条件」写成同一个表达式,不能一边用 `personalChartAvailable` 渲染、一边用另一个语义相反的谓词决定是否请求。删除兜底文案时必须回头检查被兜底遮住的空状态路径是否本来就是坏的——BUG-265 删兜底是对的,但没有验证删掉之后真实账号能不能拿到内容,代价是上线即空转。同一个概念(这里是「登录后的问候」)不允许存在多套并行实现,否则改动必然落在没被渲染的那一套上。Agent 生成的字段如果没有渲染点,就不要生成,更不能在客户端覆盖后还继续消耗 token。 -- 相关记录:BUG-265(本次修复的直接前序:Agent 化改造正确但守卫未同步,且其「待跟进」已经预告了首屏等待态问题)、BUG-201(每日星语 effect 依赖完整 Profile 对象的既有决定,本次沿用其引用保持策略,未改依赖形状)、BUG-200(首页文案第一人称与真实性边界) +- 相关记录:BUG-265(本次修复的直接前序:Agent 化改造正确但守卫未同步,且其「待跟进」已经预告了首屏等待态问题)、BUG-201(每日星语 effect 依赖完整 Profile 对象的既有决定,本次沿用其引用保持策略,未改依赖形状)、BUG-200(首页文案第一人称与真实性边界)、BUG-270(本次 hero 说明行处置的后续推翻) +- 复发自:无 +- 修复版本:`a12f5797`(staging) + +## BUG-270 | 首页 hero 第三行重复问候被删除,真实性边界随之迁移;同时暴露首页十个主题里只有三个由 Agent 生成 + +- 状态:resolved(本地修复,待提交与发布) +- 首次发现:2026-08-18 +- 最近更新:2026-08-18 +- 影响面:`/` 首页 hero 文案层级、主题区小标题承载的出生时间边界声明、Onboarding 起点加载文案、`onboarding.greeting` 字段的渲染状态。 +- 用户现象:BUG-269 把 Agent 欢迎语放进 hero 说明行后,用户评审首页时指出这一行「有点累赘」,要求删掉整段 `starter-hero-note`。同一次反馈里还指出「首页可不是三个问题啊,有好多问题」。 +- 触发条件:任何账号打开首页;hero 连续三行都在做同一件事(称呼、提问、再一次欢迎并再问一次「想从哪里开始」)。 +- 根因:两处独立问题。 + 1. hero 的信息层级在 BUG-269 之后变成三行同义内容。`starter-greeting` 已经完成称呼、`h1` 已经完成提问,Agent 欢迎语在句式上又重复了这两件事,因此第三行没有新增信息。但这一行同时是「无可用出生分钟」账号的真实性边界声明所在(BUG-200 约束,`starter-questions.test.ts` 有断言钉住),直接整段删除会连边界声明一起删掉。 + 2. 首页主题卡渲染的是 `consultationDomainRegistry` 的全部十个域,而 `/api/onboarding` 的 `suggestions` 是 `career`/`marriage`/`timing` 的固定三元组。其余七个域(wealth、health、education、migration、family、annual、general)恒定回落到 registry 里的静态 `prompt`。加载态文案「根据你的资料整理三个起点。」也因此与实际渲染的十张卡不符。 +- 修复:删除 `starter-hero-note` 元素及其两处 CSS 规则,hero 收敛为称呼加提问两行。出生时间边界声明迁到主题区小标题,按 `personalChartAvailable` 分支——读者正要挑主题时才看到这句限制,位置比 hero 更贴近实际动作。加载态文案改为「根据你的资料整理今天的起点。」,不再声明具体条数。`onboarding.greeting` 由此重新失去渲染点,本次保留字段未动 Agent 契约,因为移除它需要提升 onboarding 缓存版本并让所有用户的三个问题重新生成一次;此事作为待跟进项,不在本次改动内。 +- 验证:`onboarding-presentation.test.ts` 的 hero 断言改为同时检查 `page.tsx` 与 `globals.css` 中不再出现 `starter-hero-note`,避免只删元素留下死样式;`starter-questions.test.ts` 的边界声明断言从「文件里存在这句话」收紧为「这句话出现在主题区小标题且受 `personalChartAvailable` 分支控制」,防止下一次挪动文案时悄悄丢掉。四个相关套件 54/54 通过,`tsc --noEmit` 与 `page.tsx` ESLint 清洁。未做的验证:**没有在浏览器里确认删掉第三行后 hero 的留白比例**,以及边界声明迁到小标题后在窄屏上的折行;两者都要等 staging 发布后目视确认。 +- 防复发:删除一个 UI 元素前必须先确认它有没有在承载与自身样式无关的合规或真实性文案——`starter-hero-note` 表面是装饰性说明行,实际是 BUG-200 边界声明的唯一落点。删元素时同步删样式,并用测试同时钉住两个文件,否则死 CSS 会在下一次改版时被误当作现有设计复用。声明数量的文案(「三个起点」)不要写死在与数据源无关的地方;主题卡数量由 registry 决定,Agent 只覆盖其中三个域,任何写死条数的文案都会在 registry 增删域时失真。 +- 相关记录:BUG-269(本次推翻其 hero 说明行处置)、BUG-200(首页真实性边界声明的原始约束,本次迁移未削弱) - 复发自:无 - 修复版本:`a12f5797`(staging) diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index 3b4b3f77..51477311 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -1057,15 +1057,6 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class text-wrap: balance; } -.starter-hero-note { - max-width: 590px; - margin: var(--space-4) 0 0; - color: var(--color-ink-secondary); - font-size: var(--type-body-sm); - line-height: 1.65; - text-wrap: pretty; -} - .product-entrypoints { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); @@ -1431,10 +1422,6 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class padding: var(--space-6) var(--space-5); } - .starter-hero-note { - margin-top: var(--space-4); - } - .product-entrypoint-footer { display: grid; gap: var(--space-4); diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index 8dd05f35..22fdebff 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -3384,7 +3384,7 @@ export default function Home() { {profileComplete && presetMessageFinished && !rectificationSurfaceOpen && (onboardingPending ? (
- +
) : (
@@ -3392,10 +3392,6 @@ export default function Home() {

{starterGreeting.salutation}

{starterGreeting.question}

-

{onboarding?.greeting - || (personalChartAvailable - ? "从此刻最在意的事开始,我会结合你的星盘证据,帮你把问题拆得更清楚。" - : "从你现在最关心的事开始;出生时间不足以支持的部分,我会明确说明,不会补造具体分钟。")}

@@ -3447,7 +3443,9 @@ export default function Home() {

从一个主题开始

-

选择一个你现在想解决的问题。

+

{personalChartAvailable + ? "选择一个你现在想解决的问题。" + : "选择一个你现在想解决的问题;出生时间不足以支持的部分,我会明确说明,不会补造具体分钟。"}

{starterSuggestions.map((item) => { diff --git a/frontend/tests/onboarding-presentation.test.ts b/frontend/tests/onboarding-presentation.test.ts index c621dec5..5534ede0 100644 --- a/frontend/tests/onboarding-presentation.test.ts +++ b/frontend/tests/onboarding-presentation.test.ts @@ -59,17 +59,19 @@ test("the starter hero splits one time-aware greeting instead of drawing from a assert.ok(questions.size >= 12); }); -test("the starter hero shows the Agent greeting rather than discarding it", () => { +test("the starter hero stays at a salutation and a question", () => { const source = readFileSync(new URL("../src/app/page.tsx", import.meta.url), "utf8"); // Given: the hero heading is the question half of the time-aware greeting. assert.match(source, /

\{starterGreeting\.salutation\}<\/p>/); assert.match(source, /

\{starterGreeting\.question\}<\/h1>/); - // Then: the served onboarding greeting reaches the hero note and is never overwritten locally. - assert.match(source, /starter-hero-note">\{onboarding\?\.greeting/); - assert.doesNotMatch(source, /greeting: createStartGreeting\(/); + // Then: no third line repeats the welcome the two lines above already carry. + assert.doesNotMatch(source, /starter-hero-note/); + const styles = readFileSync(new URL("../src/app/globals.css", import.meta.url), "utf8"); + assert.doesNotMatch(styles, /starter-hero-note/); // And: the separate static heading pool is gone for good. assert.doesNotMatch(source, /starterPrompt|createStarterPrompt|greetingForHour/); + assert.doesNotMatch(source, /greeting: createStartGreeting\(/); }); diff --git a/frontend/tests/starter-questions.test.ts b/frontend/tests/starter-questions.test.ts index 1b4f9b40..57dfef39 100644 --- a/frontend/tests/starter-questions.test.ts +++ b/frontend/tests/starter-questions.test.ts @@ -98,7 +98,13 @@ test("profiles without a usable birth minute receive user-centered starter promp assert.ok(generalGuidedJyotishTopics.every((topic) => /我/.test(topic.prompt))); assert.ok(generalGuidedJyotishTopics.every((topic) => !/印度占星|一般如何|通常|哪些因素|证据层/.test(topic.prompt))); assert.match(pageSource, /const starterThemes = personalChartAvailable \? themes : generalGuidedJyotishTopics/); - assert.match(pageSource, /出生时间不足以支持的部分,我会明确说明,不会补造具体分钟/); + // The birth-time boundary is disclosed next to the topics the reader is about to pick from. + const themeSectionHeading = sourceBetween( + pageSource, + 'id="starter-themes-heading"', + "starter-theme-accordion"); + assert.match(themeSectionHeading, /personalChartAvailable/); + assert.match(themeSectionHeading, /出生时间不足以支持的部分,我会明确说明,不会补造具体分钟/); assert.match(pageSource, /请帮我看一下今天的运势,重点告诉我适合推进什么、需要注意什么/); assert.match(pageSource, /personalChartAvailable \? "daily_starlanguage" : null/); });