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

142 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 起编。