# 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/109` Create/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"`、降级外链按钮(`:108` `degradedToExternal`)。 - `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)** 1. D-B:metadata_change runtime 不可变校验 + 单测。 2. D-C:hosted 禁 bundle 校验 + 单测。 3. 集成(`-count=1` 空库腿):hosted new_work 全路径 create→approve→`works.hosted_url/play_origin/fallback` 落库读回;GET 公开作品含 `runtime=hosted` + `hostedUrl`。 4. CHANGELOG 中英成对。 **crearte(→0.19.0)** 1. FE 内容层提交 payload 类型三档 + `hostedUrl`/`fallback` 字段。 2. SubmitFormView:第三 radio「自托管内嵌」+ hostedUrl 输入 + fallback 选择;校验(https、hosted 必填 hostedUrl、fallback=external 时 url 必填);预填解除 `:144-147` 拒绝、正确回填 hosted 字段。 3. vitest:表单 hosted 分支(提交形状 / 校验失败面 / 预填回填)。 4. e2e:admin-flow 夹具补 hosted 投稿流(表单提交形状断言);主套件全绿。 5. 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 六腿口径) 1. server 容器四连(gofmt/vet/build/test)RC=0。 2. fresh 空库 `-count=1` 集成全过、零 FAIL 零静默 skip。 3. curl 冒烟真栈:hosted 草稿→submit→approve→DB 行 `hosted_url` 非空、`GET /api/games/` 含 runtime/hostedUrl;负面:runtime 篡改元数据变更 400、hosted 带 bundle 400。 4. FE:vitest 全量 / typecheck / admin-flow / 主套件 RC=0。 5. 真栈 full-loop 若分支后端被触及照跑 1 passed。 6. 双路独立审查(spec→代码逐条)PASS 后合主,wrapper 记账(ROADMAP P6 行、CHANGELOG 0.2.7、spec/plan 索引)。