Files
Jyotisha/docs/tasks/TASK-birth-sky-polish-20260928.md
T

13 KiB
Raw Blame History

TASK · 天空封面第三轮:弹层只留图、印出生日期时刻城市、过场放慢、兑换弹窗改到首条付费消息(2026-09-28)

基线与依赖

  • 基线:origin/staging = be9e4dbe(天空封面五个提交 + 验收文档,已部署 staging)。开工前 git fetch origin --prune,基线 SHA 写进 PROGRESS。
  • 分支 / 工作树:codex/birth-sky-pacing-20260928 / .worktrees/birth-sky-pacing-20260928(Claude 已从 be9e4dbe 建好,空分支)。
  • 进度记录:docs/tasks/PROGRESS-birth-sky-polish-20260928.md。
  • 前置任务书:TASK-birth-sky-cover-20260928.md、TASK-birth-sky-followup-20260928.md。
  • 本单会改 frontend/src/app/(app)/page.tsx,与其他同日改 page.tsx 的单串行:本单开工时若有别的分支在改 page.tsx,先停下告知 Claude。

事实(产品 2026-09-28 staging 真机反馈)

  1. 星盘页「那一刻的天空」弹层难看(截图,桌面窄窗口):
    • 结构是灰色毛玻璃大面板 + 标题栏「那一刻的天空 / 关闭」+ 图 + 「保存图片」;
    • 面板被拉得比图高很多,图下方一大片空白灰底;
    • 实现:frontend/src/components/birth-sky/birth-sky-dialog.tsx(.birth-sky-panel / .birth-sky-header),样式 birth-sky.css。
  2. 封面底部没有出生资料:drawBirthSky 在事实句下只画 footnote「按你填写的时间 / 按校正后的时间」。这是第一份任务书「不印任何出生资料」的结果。
  3. 新账号第一次建盘看不到群星过场,只看到「解锁完整咨询」弹窗:
    • 兑换弹窗由 page.tsx 里 setOnboardingPaywallOpen(true) 所在的 effect 打开,条件是 starterHomeVisible && credits <= 0 && 无有效订阅;
    • 本人保存出生地后,首页立刻变成 starter home,所以兑换弹窗和过场在同一刻被触发;
    • 兑换弹窗打开时 modalOpen 为真,SidebarInset 变 inert(BUG-968 的设计),而过场的 <dialog> 渲染在 Home() 里,也就是在这个 inert 子树内部;
    • DESIGN §17 写「过场在兑换弹窗之上」,但这一点只在单元测试里断言过,没有在浏览器里验证过。真实原因要执行方在无头 Chrome 里复现后才能定(见 P4)。
  4. 过场太快:lib/birth-sky/converge.ts 里汇聚 1.5 s、文字 1 s、淡出 0.5 s,总长 3 s。

决策记录(产品 2026-09-28 授权)

  1. 星盘页弹层只留图:去掉灰色面板、标题栏、「关闭」文字按钮。点开后,压暗的页面上只放这张图本身,按视口完整显示;图下方一个小的「保存图片」。点图外、按 Esc 关闭。
  2. 封面印出生资料:日期 + 时刻 + 城市。
    • 这推翻了 TASK-birth-sky-cover-20260928 的「不印任何出生资料」和对应的 BFF 合同测试断言。执行方不得以既有红线为由拒改。
    • 只印到城市;不印区县、经纬度、时区、名字。
    • 格式:1994年5月18日 07:40 · 杭州。取不到可靠的城市就只印 1994年5月18日 07:40,不猜。
    • 三处都印:星盘页图片、报告封面、首次建盘过场(它们共用 drawBirthSky)。
  3. 过场放慢:汇聚 3 s → 文字淡入并停留 2.5 s → 淡出 1 s,总长约 6.5 s。随时点一下或按 Esc 仍然立刻结束。「天空数据 3 秒内就绪才放,否则静默跳过」这条不变。
  4. 「解锁完整咨询」弹窗改到第一次付费消息被拒时再弹:
    • 删掉「进入 starter home 就弹」的 effect;
    • 用户发消息后,服务端以 402(点数不足)拒绝时:本页第一次弹兑换弹窗(OnboardingRedeemPaywall),之后再遇到 402 照旧打开设置弹窗的 billing 页;
    • 以服务端 402 为准,前端不自己预判点数。这样免费的闲聊轮(「你好」,BUG-976)和入门问题照常免费,不会被弹窗拦住;
    • 402 时草稿照旧退回输入框(现有 rollback 逻辑),兑换成功后用户点发送即可,不自动重发。

硬红线

  1. BFF 只下发一个已格式化的 caption 字符串,例如 1994年5月18日 07:40 · 杭州。仍然不得下发经纬度、时区、名字、区县、原始日期字段。
  2. page.tsx 行数不增长;Home() 的 useState / useRef 数不增长。兑换弹窗沿用现有的 onboardingPaywallOpen / onboardingPaywallShown,不新增状态。
  3. 过场、报告封面不得变成等待,沿用原预算,不加 spinner / 骨架(DESIGN §9)。
  4. 汇聚最后一帧仍与导出封面逐调用一致(同一个 drawBirthSky,只插值位置)。
  5. 不新增 npm 依赖,不改表、不加迁移。
  6. 改任何既有断言都写「原值 / 新值 / 原因」三栏;动组件前先 grep frontend/tests/ 与 tests/ 里按源码文本断言的合同测试。
  7. 隐私:测试与样张只用虚构资料(1994-05-18 07:40 UTC+8,杭州,30.27N 120.15E)。

任务分解

P1 · 星盘页弹层只留图

  • 改 birth-sky-dialog.tsx 与 birth-sky.css:
    • 原生 <dialog> 保留(焦点与 Esc 都靠它);::backdrop 压暗(深色半透明),dialog 本身透明、无边框、无面板。
    • 图 <img>(仍是画好的 PNG,iOS 长按可存):3:4,按视口取最大,宽高都不超过视口减去上下留白和按钮高度;圆角沿用 DESIGN token。
    • 图下方居中一个「保存图片」,在深色底上可读(用 DESIGN 已有的深色描边按钮样式,不新造颜色);保存失败的提示文案不变。
    • 右上角保留一个只有图标的 × 关闭(aria-label="关闭"),给键盘和读屏用;点图外、按 Esc 也关闭。
    • 删掉 .birth-sky-panel、.birth-sky-header 及其样式,确认没有别处引用。
  • 验收:
    • 无头 Chrome 在 390×844、1280×900、1280×600 三个视口各截一张(样稿虚构资料),图完整可见、下方没有空白面板,放进 scratchpad,路径写 PROGRESS;
    • 测试:弹层源码不再有标题栏与面板;有 × 和「保存图片」;点 backdrop 与 Esc 关闭。

P2 · 封面印日期 + 时刻 + 城市

  • 服务端(lib/birth-sky/service-core.ts):从服务端读到的出生资料生成 caption,加进 lib/birth-sky/contract.ts 的合同。
    • 日期与时刻用出生地当地时间(就是用户填的那个),24 小时制,时刻补零:1994年5月18日 07:40。
    • 城市:写一个纯函数从地点标签里取城市,规则写在函数注释里,并各有测试:
      • 河北省 · 邯郸市 · 峰峰矿区 → 邯郸;
      • 直辖市(如 北京市 · 朝阳区)→ 北京;
      • 取不到可靠城市(空、只有省、看不懂的格式)→ 不带城市。
      • 海外地名按现有标签格式定一条保守规则;拿不准就不带城市。
    • 城市去掉末尾的「市」。
  • 画法(lib/birth-sky/draw.ts):caption 画在事实句下面,替换原来 footnote 那一行的位置。
    • 时间来源只在采用了校正时间时出现:caption 后面接 · 校正后;按填写时间的不加后缀(「按你填写的时间」这句去掉)。
    • 字号、颜色与原 footnote 一致;paper palette 同样生效。
  • VOICE.md 更新:去掉「按你填写的时间 / 按校正后的时间」,写入 caption 格式与「· 校正后」后缀。
  • 合同测试改动:frontend/tests/birth-sky-route.test.ts 里「响应不含 1994、07:40、placeLabel……」的断言,改成「只在 caption 里出现日期、时刻、城市;不含经纬度、时区、名字、区县、原始日期字段」,三栏说明写进 PROGRESS。tests/test_birth_sky.py 如有相关断言同样处理。
  • golden:scripts/generate_birth_sky_golden.py 重新生成夹具,前端测试读新夹具。
  • 验收:caption 各格式一条测试;导出 PNG 与过场停留帧里都有 caption;报告封面 paper palette 有 caption;重新截三张海报(白天 / 夜里 adopted / 傍晚 paper)放 scratchpad。

P3 · 过场放慢

  • converge.ts:BIRTH_SKY_CONVERGE_MS = 3_000、BIRTH_SKY_TEXT_MS = 2_500、BIRTH_SKY_FADE_MS = 1_000。文字淡入从 400 ms 放到 600 ms;整层前段淡入从 200 ms 放到 400 ms;reduced-motion 淡入 600 ms、淡出 1 s。
  • 时间轴测试随之改数值(三栏说明);「t=0 散开、t=1 真实位置」和「停留帧 = 导出封面」两条测试原样保留。
  • DESIGN §17 时序段落同步。
  • 验收:重新取 t=0、汇聚过半、汇聚完成、停留四帧,路径写 PROGRESS。

P4 · 兑换弹窗改到首条付费消息被拒(含 BUG-1080)

  • 先复现,再定根因(写进 BUG-1080):
    • 在无头 Chrome 里搭一个最小页面:一个带 inert 的容器,里面 showModal() 一个 <dialog>,外面 portal 一个 z-index 很高的兑换弹层。截图看过场是否可见、pointerdown 能不能收到。
    • 同时查首页在「保存出生地 → starter home」这一刻:兑换 effect 和 startBirthSkyReveal 各自何时触发、有没有别的原因让过场被跳过(例如 3 秒预算内拿不到数据)。
    • 结论写进 BUG-1080:确认的就写确认,查不清的写 investigating 并列出剩下的可能。
  • 修复(不依赖根因结论,都要做):
    1. 删掉 page.tsx 里「进入 starter home 就弹兑换弹窗」的 effect。
    2. 402 分流:hooks/use-consultation-run.ts 里 402 时现在调用 openAccountDialog("billing", { source: "insufficient-credits" })(rejectRun 与另一处 source: "insufficient-credits")。改成:本页第一次 402 打开 OnboardingRedeemPaywall,并置 onboardingPaywallShown;之后的 402 照旧打开 billing。
      • 建议做法:给 useConsultationRun 加一个可选依赖,例如 onInsufficientCredits,返回是否已处理;page.tsx 传一个复用现有 ref / state 的回调,同时删掉旧 effect,保证行数不增长。执行方可换更简单的写法,但不得新增 useState / useRef。
    3. 过场的 <dialog> 用 createPortal(…, document.body) 挂到 SidebarInset 之外,与兑换弹窗、账户弹窗同一做法(BUG-968),让它不受 inert 影响。
    4. 兑换弹窗里「入门问题已经准备好。」这句要改,因为弹出的时机已经不是入门问题做完的时候。先对照 VOICE 改成与新时机相符的一句,写进 PROGRESS 让 Claude 验收。
  • 生时校正对话里的 402(lib/rectification-chat-turn-run.ts)不在本单范围,保持现状。
  • 验收:
    • 测试:page.tsx 不再有进入首页就弹的 effect;第一次 402 打开兑换弹窗、第二次打开 billing;402 后草稿退回输入框;免费轮不触发。
    • membership-page.test.ts 的 “finished starter questions open the redeem paywall…” 按新时机改写,三栏说明。
    • 过场 portal 到 document.body 有源码合同测试。
    • 无头 Chrome 最小复现的截图与结论写进 PROGRESS 与 BUG-1080。

P5 · 文档

  • frontend/DESIGN.md §17:弹层只留图;caption 格式;新时序;删掉「过场在兑换弹窗之上」这句,改写兑换弹窗的新时机。
  • frontend/docs/VOICE.md:caption 格式与「· 校正后」后缀;兑换弹窗新文案。
  • CHANGELOG.md:一条。
  • docs/BUG_HISTORY.md:BUG-1080(过场被兑换弹窗挡住)。
  • docs/testing/birth-sky-cover-checklist.md 追加真机条目:
    • 新账号(0 点)首次建盘:完整看到约 6.5 秒的过场,期间不弹兑换弹窗;
    • 之后发一条正式问题:弹「解锁完整咨询」,草稿还在输入框;关掉后再发一次,打开的是 billing;
    • 发「你好」这类闲聊:不弹;
    • 星盘页点「那一刻的天空」:只看到图和「保存图片」,没有灰色面板;手机和电脑各看一次;
    • 图底部是「日期 时刻 · 城市」;用直辖市和海外地点各试一次。

验证

  • tsc --noEmit 0 错;npm run lint 0 error。
  • npm test 全量:与基线 be9e4dbe 比测试名清单,不得有测试名消失;失败清单与基线(24 条 Docker / 部署套件)逐条同名。
  • Python:python3 -m pytest tests/test_birth_sky.py tests/test_api_server_growth_contract.py tests/test_repo_privacy_markers.py;python3 scripts/run_quality_gate.py --profile quick。
  • rm -rf .next && next build:/ 仍 ○ Static;首屏 JS / CSS gzip 与基线 be9e4dbe 同口径对比(同一台机器、同一脚本各建一次),变化在 ±2% 内。

让步顺序

  1. P4 第 4 条(兑换弹窗文案)可以先保留原句,写进 PROGRESS 留给产品。
  2. 海外地名的城市规则可以一律不带城市。
  3. P1 弹层只留图、P2 caption(至少日期 + 时刻)、P3 放慢、P4 首页不再自动弹兑换弹窗,都不可让步。

交付

  • 一个或两个提交,推到本地分支即可,不推送;由 Claude 独立验收后快进推 staging。
  • 提交信息与 PROGRESS 引用本任务书。

BUG 编号

开工时核对 docs/BUG_HISTORY.md 最大号(写作时 BUG-1079)。过场被兑换弹窗挡住记为 BUG-1080;实现中再发现的既有缺陷从 BUG-1081 起编。