Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE
142 lines
13 KiB
Markdown
142 lines
13 KiB
Markdown
# 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 起编。
|