6.6 KiB
6.6 KiB
P6 hosted 作品投稿 · 设计 spec(方案 A:自托管内嵌)
日期:2026-09-30 | 决策人:owner(选 A)| 前置勘查:控制者本会话完成,证据均带 file:line。
0. 一句话
作者把游戏部署在别处(itch.io / 自建站),投稿填一个 https URL,平台站内用沙箱 iframe 播放;打通的是「入口」,播放机件早已全存在。
1. 现状勘查(grounded)
后端(crearte-server)hosted 半边已通:
works表(migration 0002)含runtime IN ('external','virtual','hosted')、hosted_url(CHECK https)、play_origin、fallback IN ('external','hosted','none')、entry。- 校验已认:
service/validate.go:133-141——hosted合法 runtime,且要求HostedURL匹配payloadHTTPSPattern。 - 落库映射不缺:
service/import.go:39 PayloadToWork已映射PlayOrigin/HostedURL/Fallback/Entry(此前「Approve 落库缺口」系误报——grep 打在 content.go 而映射在 import.go);repository/work.go:73/96/109Create/Upsert/UpdateMetadata 全列含hosted_url。 - 提交创建
content.go:327-358:virtual 必须有 bundle;hosted 无 bundle 要求但也未拒绝携带 bundle;metadata_change 对 runtime 无不可变性校验(作者可借元数据变更改 runtime)。 PlaySubdomain(workID)(namespace.go:16)确定性派生 sha256 前 8 字节 hex,对全部 runtime 生效;hosted 不需要平台签发子域(播放源即 hostedUrl),playOrigin 可显式覆盖(CONTRIBUTING:42 已写明)。
前端(crearte)播放端全存在、入口焊死:
GameView.vue:30:playable = virtual || hosted;runtime/host/GameHost.vue:98-101:iframe:src="iframeSrc"、:sandbox、:allow、referrerpolicy="no-referrer"、降级外链按钮(:108degradedToExternal)。runtime/host/useGameFrame.ts:58/159/162:hosted 目标直接置ready(不等 agent:boot),3 秒未收到agent:boot仅 console warn(桥不存在属预期)。types.ts:16:GameRuntimeMode三值齐全;但SubmitFormView.vue:33表单 runtime 仅external|virtual,:144-147对 hosted 作品预填直接拒绝(防元数据变更静默降级——该风险根源在后端缺 D-B,见下)。- 内容层提交 payload 的 FE 类型同样只两档(SubmitFormView:144 注释)。
deploy / CSP:
sw/csp.ts:29、serve-runtime.mjs:41的frame-src 'none'是运行时子域页面自身的 CSP(管 play 页里再嵌 iframe),不是主站宿主页面——hosted iframe 挂在主站页面,不受此约束。- deploy 仓 grep 无主站 CSP frame-src 下发(nginx 模板未含);验收腿 T5 实核生产响应头。
结论:方案 A 的真实工作量 = 后端两处校验收紧 + 前端表单第三档 + 预填解锁 + 测试与验收,播放链路零改动。
2. 决策(D-A…D-F)
- D-A hostedUrl 政策:必须 https(已有校验复用);任意域名、不做白名单——第三方禁嵌(X-Frame-Options/CSP frame-ancestors)平台不可预判,由播放端既有 fallback 链兜底(降级外链)。iframe 安全参数沿用现状(sandbox 旗标集 + no-referrer + 外链
rel=noopener noreferrer),不扩权。 - D-B runtime 不可变(根治,后端):
metadata_change提交的payload.runtime(经RuntimeOrDefault)与作品现存 runtime 不一致 →ValidationError{Fields:["runtime: immutable on metadata_change"]}。此后前端:144-147拒绝分支删除(表单能表达 hosted,预填安全)。收紧只作用于新提交:审批路径不复跑 ValidateSubmission(Approve 只做 ParseEnvelope+落库,content.go:596+),存量 draft 无 retroactive 失效。 - D-C hosted 语义纯度:
runtime=hosted且带bundle_upload_id→ValidationError(hosted 不吃平台文件);features允许提交但桥不存在(armBridgeWarn 已容忍);entry/playSubdomain对 hosted 无意义,存而不使用,不报错。cover 上传与 runtime 无关,照常允许。 - D-D 审核面最小改动:AdminView 待审卡片对 hosted 提交展示
hostedUrl文本行(不内嵌播放预览——审核者安全 + 最小变更)。 - D-E fallback:复用现有列;表单在 hosted 档下提供 fallback 选择(
external|hosted|none),默认引导external(需 url 字段配合,表单校验:fallback=external 时 url 必填)。 - D-F deploy 仅核证:实探主站 CSP 头;无 frame 限制则零改动记录在案,有则补模板并复验(T5)。
3. 范围
server(→0.13.0)
- D-B:metadata_change runtime 不可变校验 + 单测。
- D-C:hosted 禁 bundle 校验 + 单测。
- 集成(
-count=1空库腿):hosted new_work 全路径 create→approve→works.hosted_url/play_origin/fallback落库读回;GET 公开作品含runtime=hosted+hostedUrl。 - CHANGELOG 中英成对。
crearte(→0.19.0)
- FE 内容层提交 payload 类型三档 +
hostedUrl/fallback字段。 - SubmitFormView:第三 radio「自托管内嵌」+ hostedUrl 输入 + fallback 选择;校验(https、hosted 必填 hostedUrl、fallback=external 时 url 必填);预填解除
:144-147拒绝、正确回填 hosted 字段。 - vitest:表单 hosted 分支(提交形状 / 校验失败面 / 预填回填)。
- e2e:admin-flow 夹具补 hosted 投稿流(表单提交形状断言);主套件全绿。
- CHANGELOG 中英成对。
deploy:仅 T5 核证,预期零改动。
不做(明确排除):URL 抓取/健康探测、域名信任列表、hosted 的版本更新(new_version 仍 virtual-only)、转码代理、离线缓存、播放端任何改动。
4. 红线
- 不动
full-loop.spec.ts/playwright.stack.config.ts/ 运行时三件套(sw/agent/bootstrap)。 - 不动生产凭据与
.env;deploy 改动(如有)只出模板级。 - server 迁移零新增(hosted 列 0002 起就在)。
- 前端不新增后端端点依赖;管理面仅 D-D 一行展示。
5. 验收(控制者本机,同 P7 六腿口径)
- server 容器四连(gofmt/vet/build/test)RC=0。
- fresh 空库
-count=1集成全过、零 FAIL 零静默 skip。 - curl 冒烟真栈:hosted 草稿→submit→approve→DB 行
hosted_url非空、GET /api/games/<id>含 runtime/hostedUrl;负面:runtime 篡改元数据变更 400、hosted 带 bundle 400。 - FE:vitest 全量 / typecheck / admin-flow / 主套件 RC=0。
- 真栈 full-loop 若分支后端被触及照跑 1 passed。
- 双路独立审查(spec→代码逐条)PASS 后合主,wrapper 记账(ROADMAP P6 行、CHANGELOG 0.2.7、spec/plan 索引)。