feat: 头像上传(uuid重命名/魔数校验/2MB/配置版本管线)与三级解析链

- POST /api/platform/tenants/{slug}/brand/avatar(require_tenant):
  multipart 收文件,魔数判定类型(\x89PNG/\xff\xd8\xff/RIFF....WEBP,
  优先于扩展名——改后缀骗不过,存盘扩展名取自内容)、≤2MB(413,
  upload.size 预检免拷贝)、服务端 uuid4 重命名(原始文件名不参与
  路径),落 data/uploads/<slug>/(目录懒建,gitignore 已忽略)
- 走 Task 6 配置版本管线(复用 config_routes 内部 helper):读当前
  快照 → brand 组只替换 avatar 字段 → validate_group →
  write_config_version(comment=头像上传) → registry_reload 热生效
- /api/public/avatar 三级解析链:uploads → seed 资产 → 静态兜底;
  Phase 1 文件名穿越防线(拒 / \ . ..)扩到 uploads 段
- registry.uploads_root:自 data_dir.parent 派生(生产=<repo>/data/
  uploads;测试注入 tmp data_dir 时自动落 tmp 内,隔离免污染)
- 新依赖 python-multipart(multipart 表单解析)

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-10-13 10:35:41 +08:00
parent 7502176190
commit a43c654337
6 changed files with 382 additions and 17 deletions
+8 -2
View File
@@ -90,8 +90,8 @@ class TenantRuntime:
加载期就绪:config(TenantConfig:data=快照⊕出厂默认,含 sites)/
db_path(该租户 SQLite 文件路径)/seed_dir(文件源=租户目录;DB 源
None——avatar 解析链对其回退静态兜底)/compiled_wall(墙编译产物)/
prompts(PromptProvider.from_config(snapshot["prompts"]),两源统一
None——avatar 解析链的 seed 资产段对其跳过)/compiled_wall(墙编译
产物)/prompts(PromptProvider.from_config(snapshot["prompts"]),两源统一
入口——$DEFLECTION_POOL 话术池注入的单一消费口)。
懒建:engine/session_factory(Registry 首次 resolve,ensure_schema 已跑)。
manager(Task 5b)由装配线另行构造注入,不在本类持有。
@@ -123,6 +123,12 @@ class TenantRegistry:
self._by_slug: dict[str, TenantRuntime] = {}
self._domain_index: dict[str, tuple[str, str]] = {}
self._locks: dict[str, asyncio.Lock] = {}
# 头像上传根目录(Task 7):生产 data_dir = <repo>/tenants,与
# <repo>/data/ 是仓库根兄弟目录(spec 布局,.gitignore 已忽略 data/)
# → 取 data_dir.parent 派生;测试注入 tmp data_dir 时上传自动落
# tmp 内(DB 源夹具 data_dir=tmp/tenants → tmp/data/uploads),
# 隔离免污染仓库目录。布局若变,只改这一处。
self.uploads_root = self._data_dir.parent / "data" / "uploads"
# ---- 加载 -----------------------------------------------------------