后台模板上传(后端):新增批量导入接口与存储/领域支持

- shared-contracts 新增导入 manifest、逐条与结果 DTO(camelCase,拒绝未知字段)
- module-assets 新增 prepare_template_import:新 ID 默认上架,已存在条目就地更新并保留上架状态与未知扩展字段;同一 ID 同版本字节不同时按 CLI 同一句文案拒绝,字节一致时按内容复用既有对象
- platform-oss 为 application/zip 放宽单对象上限到 64 MiB(图片与元数据仍 5 MiB),测试替身同步该口径
- api-server 新增 POST /admin/api/agc-templates/import:multipart(manifest + zip_N/cover_N),逐项校验归档安全性、entry 存在性与封面(按字节嗅探格式,仅 PNG/JPEG/WebP)后才取发布锁
- 锁内流程:CAS 校验 revision → 内容寻址写入 zip/封面/元数据并回读 → 一次提交清单 → 释放锁;断连由独立任务持有,不确定结果保留锁并返回 503
- 路由挂 require_admin_auth 与 200 MiB body 上限,页签权限矩阵与路由契约测试同步
- 新增用例:清单与文件字段校验、归档越界与符号链接拒绝、内容寻址键与封面嗅探、zip 内容类型存储上限
- 新增「后台模板上传」里程碑与实施计划文档
- 验证:cargo test(module-assets 11 项、platform-oss 12 项、api-server 定向 4 项)、cargo fmt --check、check:doc-index、check:encoding、git diff --check
This commit is contained in:
kdletters
2026-09-21 15:18:55 +08:00
parent 33036b65fc
commit 4863c8c066
8 changed files with 1281 additions and 6 deletions
@@ -0,0 +1,40 @@
# 后台模板上传实施计划
| 字段 | 值 |
| --- | --- |
| Version | 1.0 |
| Status | in-progress |
| Date | 2026-09-21 |
| Milestone Spec | `docs/project-memory/plans/【里程碑】后台模板上传-2026-09-21.md` |
## 修改边界与顺序
1. **领域规则(`module-assets/template_library.rs`**:新增「导入准备」函数——按 ID 合并清单条目(新增默认上架、已存在就地更新并保留 `enabled` 与未知字段),校验 ID / 版本 / entry / runtime / 显示字段上限,产出待提交的清单与 `template.json` 字节;不接触存储。
2. **存储(复用 `platform-oss/template_library.rs`**:只使用既有 `begin_publish` / `read_index` / `put_immutable` / `commit_index` / `finish`;如缺少「读单个对象作元数据基线」的能力,再补最小只读方法,不改锁与提交语义。
3. **契约(`shared-contracts/admin.rs` + `apps/admin-web/src/api/adminApiTypes.ts`**:新增导入请求(manifest)与导入结果 DTO;错误体沿用现有 `AppError` + 逐项原因结构。
4. **接口(`api-server/admin_templates.rs` + `modules/admin.rs`**:新增 multipart handler,按「解析 manifest → 校验每个 ZIP / 封面 → 取锁 → CAS → 写内容寻址对象并回读 → 提交清单 → 释放锁」顺序实现;路由套 `require_admin_auth` 与 256 MiB body 上限,并同步路由契约测试与页签权限矩阵测试。
5. **后台页面(`AdminAgcTemplatesPage.tsx` + `adminApiClient.ts`**:新增「上传模板」入口与弹窗(多选 ZIP、逐行元数据、批量提交、逐行错误、写入确认),沿用既有 `useAdminWriteConfirm` 与刷新语义。
6. **文档**:主规范新增「后台模板上传」章节;决策记录补一条;主规范中「本轮只允许…不上传 ZIP」改为指向新章节。
## 验证命令
```bash
cargo test --locked -p module-assets --manifest-path server-rs/Cargo.toml -- template_library
cargo test --locked -p api-server --manifest-path server-rs/Cargo.toml -- agc_template
cargo fmt --all --manifest-path server-rs/Cargo.toml -- --check
npm run admin-web:typecheck
npx vitest run apps/admin-web/src/pages/AdminAgcTemplatesPage.test.tsx apps/admin-web/src/api/adminApiClient.test.ts
npm run check:encoding
npm run check:doc-index
git diff --check
```
接口 smoke`npm run dev:api-server`(本地 8082+ `npm run dev:admin-web`,先用未授权/无页签权限请求确认失败关闭,再以隔离存储替身或用户显式确认的 dev bucket 做一次真实导入。
## 时间盒与风险
- **风险:真实发布不可逆**。导入会写公共 bucket 且不提供删除,因此默认只在本地用替身验证;对 dev bucket 的写验证必须由用户显式确认,测试用模板需可在事后下架。
- **风险:ZIP 原字节发布与 CLI 确定性打包不一致**。同一模板可能被两条路径写成不同字节;由「同 ID 同版本字节必须一致」的门禁兜住,导入失败时提示改用 CLI 或递增版本。
- **风险:大文件内存**。ZIP(≤64 MiB)与封面在内存中校验,单批上限 20;需要更大模板时走 CLI。
- **风险:批量部分写入**。所有对象在清单提交前写入且不删除;中途失败时清单不变、已写对象成为未被引用的历史对象,由后续同键复用。
- **回退**:下线路由与页面入口即可停止使用;已发布内容按既有 CLI / 后台下架流程处理,历史对象保留。
@@ -0,0 +1,53 @@
# 后台模板上传
| 字段 | 值 |
| --- | --- |
| Version | 1.0 |
| Status | proposed(用户 2026-09-21 提出“后台再加个模板上传功能,最好支持批量上传”;边界见下,评审通过后开工) |
| Date | 2026-09-21 |
| Parent Spec | `docs/technical/【技术方案】AGC模板库与模板建项-2026-09-17.md` |
## 目标与范围
管理员在后台直接上传模板并发布到公共模板库,不必走本地 CLI;一批可以带多个模板(批量上传),一批只做一次发布锁和一次清单提交。
- 新增 `POST /admin/api/agc-templates/import``multipart/form-data`):`manifest` 文本字段 + `zip_<index>` / `cover_<index>` 文件字段,字段下标与 manifest 条目顺序一一对应。
- manifest`{ expectedRevision, templates: [{ id, title, summary, tags, runtime, engine, engineVersion, entry, templateVersion }] }``expectedRevision` 是当前清单字节 SHA-256,用于锁内 CAS。
- 限制:单批最多 20 个模板;单个 ZIP ≤ 64 MiB;单张封面 ≤ 5 MiB;请求体 ≤ 200 MiB`id` / `templateVersion` / `entry` 走既有标识符与相对路径白名单;`runtime` 仅接受 `html / unity / godot / cocos`。存储层为 `application/zip` 单独放宽单对象上限到 64 MiB(图片与元数据仍是 5 MiB)。
- 语义:一批**全有或全无**——任一模板校验失败都在写入前整批拒绝并逐项给出原因;通过后在同一把发布锁内写入全部内容寻址对象、逐个回读校验,最后提交一次清单。
- 新 ID 默认上架;已存在 ID 的导入就地更新该条目(保留 `enabled` 状态、所属分组与未知扩展字段,包含下架条目);同一 ID 同一 `templateVersion` 但 ZIP 字节不同时拒绝,提示递增 `templateVersion`
- ZIP 由服务端校验后按原字节发布(不重新打包、不解压落盘):合法 zip、无符号链接、无绝对路径 / `..` / 盘符条目、必须包含声明的 `entry`、至少一个文件,条目数与解压后体积受上限保护。
- 封面**每个模板必填**(与 CLI 源布局 `v1/<id>/cover.*` 一致),仅接受真实 PNG / JPEG / WebP,上限与编辑路径相同(5 MiB、单边 4096、1600 万像素);上传按字节嗅探格式,不信任 multipart 声明的 content-type。SVG 仍只可能来自 CLI 历史发布,编辑与上传都不产生新的 SVG 封面。
- 后台「模板管理」页新增上传入口:可多选 ZIP、逐行编辑元数据(含可选封面)、批量提交、逐行显示校验错误;沿用现有写入确认、防重复提交与刷新语义。
## 不做
- 不做模板删除、版本回滚、下架条目清理、历史对象回收。
- 不改 ZIP 内容、不做服务端重新打包(CLI 仍是确定性打包与 `--only` 定向发布的入口)。
- 不做审核流、不做从 URL 拉取、不做 CLI 源布局目录(`v1/<id>/{meta.json,project/**,cover.*}`)的自动打包上传。
- 不新增 SpacetimeDB 表或 schema。
## 合同与依赖
- 存储与锁:复用 `platform-oss``TemplateLibraryStore` / `TemplatePublishSession``begin_publish` / `read_index` / `put_immutable` / `commit_index` / `finish`)与既有版本控制前置检查;不新增第二套发布协议。
- 领域规则:`module-assets/template_library.rs` 承担清单合并与字段校验(保留未知字段、下架状态、两组间 ID 唯一),沿用既有标识符 / 相对路径 / 摘要规则。
- DTO`shared-contracts/admin.rs` 新增导入结果类型;前端类型在 `apps/admin-web/src/api/adminApiTypes.ts`
- 鉴权:沿用后台认证与 `agc-templates` 页签权限;路由挂载在 `modules/admin.rs`
## 验收标准
1. 权限与路由:未授权读取/写入均失败关闭;无 `agc-templates` 页签权限不可导入;路由契约测试覆盖新路径与方法。
2. 校验失败关闭:manifest 非法、ID/版本/entry 越界、runtime 不在白名单、ZIP 非法或含越界/符号链接条目、缺少 entry、封面超限或非真实图片,全部在**任何写入之前**整批拒绝,响应逐项给出模板 ID 与原因。
3. 批量原子性:一批 3 个模板(含 1 个新 ID、1 个新版本、1 个下架 ID)在锁内一次提交;中途任一环节失败时清单与历史对象不变,返回可诊断错误。
4. 版本一致性:同 ID 同版本、ZIP 字节不同必须拒绝并要求递增版本;同 ID 同版本、字节完全一致视为幂等复用,不报错。
5. CAS 与锁:`expectedRevision` 过期返回 409;锁被占用返回 409;清单写入结果不明时保留锁并返回 503,不自动重试。
6. 保留语义:既有条目的展示字段按上传内容更新,`enabled` 分组、其它条目、未知扩展字段与历史对象不变;下架条目仍留在 `inactiveTemplates`
7. 内容寻址与回读:ZIP / 封面 / template.json 以自身字节摘要寻址,写入后逐项回读校验,清单最后提交且返回最新快照。
8. 界面:桌面与窄屏都能完成多选 ZIP、逐行元数据编辑、批量提交与错误展示;保存中防重复提交,409 引导刷新后重试。
9. 检查:`cargo test`module-assets / platform-oss / api-server 定向)、`npm run admin-web:typecheck`、后台页面 vitest、`npm run check:encoding``npm run check:doc-index``cargo fmt --check``git diff --check` 全部通过。
## 依赖
- 已交付的后台模板管理链路(`GET/PUT /admin/api/agc-templates`、页面、锁与内容寻址发布协议)。
- 模板库 OSS 写凭据(`GENARRATIVE_AGC_TEMPLATE_LIBRARY_OSS_ACCESS_KEY_ID/SECRET` 或成套 `ALIYUN_OSS_*`);无凭据时导入返回 503 且页面明确不可用。
- 真实 dev bucket 的写入验证需要用户显式确认(见实施计划「验证」)。