From e9a7dc8740afe8f373a68c487d9890b98cd53d86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=85=E5=BF=B5?= Date: Sun, 20 Sep 2026 21:41:19 +0800 Subject: [PATCH] docs(plan): p4b review fixes (etag recipe, sandbox origin, narrow screen) Co-Authored-By: Claude Code --- docs/plans/2026-09-20-P4b-悬浮组件与全端语言.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/plans/2026-09-20-P4b-悬浮组件与全端语言.md b/docs/plans/2026-09-20-P4b-悬浮组件与全端语言.md index 94c3ef6..4b906b5 100644 --- a/docs/plans/2026-09-20-P4b-悬浮组件与全端语言.md +++ b/docs/plans/2026-09-20-P4b-悬浮组件与全端语言.md @@ -20,7 +20,7 @@ - [ ] 新增: - **管理端**(tag embed-slots):`GET /admin/embed-slots`(cursor;items: EmbedSlot{id, publicToken, name, enabled, albumId?, albumTitle?, configVersion, settings Json{position left|right, size sm|md, intervalMs 3000-30000, lang auto|zh-CN|en, collapsedDefault bool}, updatedAt});`POST /admin/embed-slots` {name, settings?} → 201;`PATCH /admin/embed-slots/{id}` {name?, enabled?, settings?, albumId?|null 绑定/解绑} → 200(albumId 变更 → configVersion+1;绑定校验相册管理权:操作者须为该相册 owner/admin 或 slot 授权成员——**简化决策:P4b 权限=仅公司 admin 可管理展示位**(需求"管理员统一管理展示位,创建者只操作被授权展示位"——被授权语义复杂度高,登记偏差:embed_slot_members 表建好但 P4b 不出授权 UI/API,P5/后续按需补));`DELETE` 不做(软性 enabled=false 即下线) - - **公开端**(ov):`GET /public/embed-slots/{token}?lang=` → 200 {slotToken, configVersion, album: {publicToken, title(resolved), defaultLocale}|null, photos: [{photoId, thumb{url,expiresAt}, width, height}](近期已发布 ≤20,按 publishedAt DESC)};404 NOT_FOUND(不存在/停用/未绑定相册→同样 404 防探测,description 说明);**ETag/If-None-Match 支持**(configVersion+照片 max entityVersion 为 ETag;304 支持——省流量,A23) + - **公开端**(ov):`GET /public/embed-slots/{token}?lang=` → 200 {slotToken, configVersion, album: {publicToken, title(resolved), defaultLocale}|null, photos: [{photoId, thumb{url,expiresAt}, width, height}](近期已发布 ≤20,按 publishedAt DESC)};404 NOT_FOUND(不存在/停用/未绑定相册→同样 404 防探测,description 说明);**ETag/If-None-Match 支持**(ETag = `"{configVersion}-{photoCount}-{maxPublishedAtMs}-{maxPhotoId}"`——评审修正:含 photoCount 与 publishedAt,新照片发布必变;纯 max entityVersion 会因新照片小版本号而 304 陈旧);304 支持——省流量,A23 - [ ] constants: EMBED_* 轮询参数(EMBED_POLL_INTERVAL_MS 默认 30000 env 可调) - [ ] gen + lint + 全回归绿;⏸ 快速确认点 - [ ] Commit `feat(contracts): embed slots admin and public endpoints` @@ -37,9 +37,10 @@ ## Task 3: embed-loader(packages/embed-loader) - [ ] `packages/embed-loader/src/loader.js`(手写 ~200 行,无构建或极简 esbuild 打包——决策:esbuild 已在依赖树(vite),加 devDep esbuild 打包+minify,产物 `dist/v1/loader.js` 提交入库): - - 扫描 `script[src*="/embed/v1/loader.js"]` 自身 data-*(data-slot/data-lang/data-position/data-interval 覆盖)→ 防重复(window.__photoliveEmbed loaded 标记 + 同 slot 去重)→ 每槽创建固定定位 iframe(right/left, 尺寸 per settings.size 映射 px,z-index 2147483000,box-shadow 圆角,sandbox 属性 allow-same-origin? NO——iframe 加载项目域需 scripts+popups:sandbox="allow-scripts allow-popups allow-popups-to-escape-sandbox",src= `{EMBED_ORIGIN}/embed/{slot}?lang=...&pos=...&interval=...`——EMBED_ORIGIN 从 loader.js 自身 src 推导(document.currentScript.origin),支持 CDN 部署) + - 扫描 `script[src*="/embed/v1/loader.js"]` 自身 data-*(data-slot/data-lang/data-position/data-interval 覆盖)→ 防重复(window.__photoliveEmbed loaded 标记 + 同 slot 去重)→ 每槽创建固定定位 iframe(right/left, 尺寸 per settings.size 映射 px,z-index 2147483000,box-shadow 圆角,sandbox=`"allow-scripts allow-popups allow-popups-to-escape-sandbox allow-same-origin"`——**评审修正:必须含 allow-same-origin**,否则 iframe 源为 opaque "null",loader 的 origin 相等白名单会拒掉自己 iframe 的 resize/collapse 消息且 iframe 内 API 跨源需 ACAO *;嵌入 iframe 对宿主天然跨源,allow-same-origin 仅使其保有项目域源,白名单按「消息 origin === iframe contentWindow 的源」严格校验,src= `{EMBED_ORIGIN}/embed/{slot}?lang=...&pos=...&interval=...`——EMBED_ORIGIN 从 loader.js 自身 src 推导(document.currentScript.origin),支持 CDN 部署) + - **窄屏规则**(评审缺口:需求 4.8 移动端可用状态):viewport <480px → 强制 collapsedDefault=true + 尺寸降为 sm;Playwright 移动视口用例覆盖 - postMessage 白名单:监听 message(origin 必须等于 iframe origin)仅接受 {photoliveEmbed: true, type: resize|collapse|expand, ...}→ 调整 iframe 高度/收起态(收起=高度 0+展示小把手按钮 div——把手也在 loader 侧,点击 expand);其余消息忽略。iframe 内不发其他命令 - - 收起记忆 sessionStorage per slot(会话内不反复弹出,需求 9) + - 收起记忆 sessionStorage per slot(会话内不反复弹出,需求 9);初始轮询失效上限(连续失败 N=5 次退避至 5×interval,参数 env 可调——评审建议) - 不污染宿主:零全局(仅一个标记属性)、无样式注入到宿主元素(把手 div inline style)、动态卸载 API 可选(P4b 略) - [ ] 单测(node 环境模拟 DOM 太薄——用 Playwright 宿主页测试替代,包内 vitest 仅测 src 解析/参数合并纯函数) - [ ] viewer 静态服务:apps/viewer/public/embed/v1/loader.js(构建时从 packages/embed-loader/dist 拷贝——vite publicDir 静态放 public/ 即可,脚本 build 步骤 copy+校验 hash;dev 直接可访问) @@ -76,7 +77,7 @@ ## Task 7: 回归门 + P4b 验收记录 - [ ] 全仓回归绿 + mobile 不降级;记录数字 -- [ ] `docs/plans/progress/P4b-验收记录.md`(A11/A22/A23 组件/A12+T15 证据、偏差表[embed_slot_members 建而未用/权限简化 admin-only/ETag 简化实现/esbuild devDep]、P5 移交[真实域名/CDN 预热/展示位授权 UI 按需]) +- [ ] `docs/plans/progress/P4b-验收记录.md`(A11/A22(**简化级:展示位权限 admin-only**,镜像 P2 A25 部分模式)/A23 组件/A12+T15 证据、偏差表[embed_slot_members 建而未用/权限简化 admin-only/ETag 四元组实现/esbuild devDep]、P5 移交[真实域名/CDN 预热/展示位授权 UI 按需/**embed 页 frame-ancestors CSP 配置**/初始轮询失效上限参数化]) - [ ] 总纲 P4b 进度注记(P4b 完成 → **P1-P4b 全部完成,仅余 P5 真实环境接入**);`git tag p4b`;Commit `docs: p4b acceptance record` ---