收尾两批并行开发的文档回写与任务包

- Sprint 执行计划 v1.1:§1.1 补第一批交付实据;T9 SPIKE 结论回写 Sprint 1 表
  (T-P0-16 通过 2.8ms;T-P0-12 有条件;T-P0-10 原型 6/6 + 真实照片重验前置);
  Sprint 2 表标注 T-P2-01~06 交付;新增 §9 两批交付记录
- 开发任务拆解 v1.5:交付回写 + 新风险登记(RTMPose 域差异、BiRefNet 异步化)
- 新增 开发任务包/:三批多 agent 并行的协作规程与 14 份任务契约
- 新增 前置开通清单-云资源与资质.md
- 清理已用毕的评审报告(文档评审 2026-08-13、项目整体评审 2026-08-14),
  README 中相应引用改为留痕表述
This commit is contained in:
xsl
2026-08-14 21:07:20 +08:00
parent 2934954eb2
commit e511b6a66f
22 changed files with 1407 additions and 461 deletions
+104
View File
@@ -0,0 +1,104 @@
# T5 · 图片直传预签名 URL
> 对应任务:`T-P1-27` | 需求:`FR-SYS-03`、架构 §9.3 | 分支:`task/t5-upload-presign` | worktree:`/home/xsl/meida_backend-t5`
> 先读《README.md(协作规程)》,遵守全部纪律。
## 1. 目标
实现 `POST /uploads/presign`:为客户端签发 COS 临时桶的 PUT 预签名 URL,**图片不经应用服务器**(FR-SYS-03 验收原文)。本任务只做「签发」,不做上传回调/校验图片内容(那是报告链路 T-P3 的事)。
## 2. 已就绪的契约(协调提交已提供)
### 依赖(协调提交已 `uv add`,禁改 pyproject/uv.lock)
`cos-python-sdk-v5`(腾讯云 COS 官方 SDK,模块名 `qcloud_cos`)。mypy 对它的缺失 stub 已在协调提交里配好 override。真实调用只需要 `CosS3Config` 与 `CosS3Client.get_presigned_url` / `get_presigned_download_url` 所在的 `CosS3Client`。
### 配置(`config.py` 已加字段,`.env.example` 已示例)
```python
cos_region: str = "ap-shanghai" # env: COS_REGION
cos_secret_id: str = "" # env: COS_SECRET_ID
cos_secret_key: str = "" # env: COS_SECRET_KEY
cos_bucket_temp: str = "meida-temp" # env: COS_BUCKET_TEMP(临时桶,2h 生命周期 CR-02)
cos_bucket_user: str = "meida-user" # env: COS_BUCKET_USER
cos_bucket_kb: str = "meida-kb" # env: COS_BUCKET_KB
cos_bucket_report: str = "meida-report" # env: COS_BUCKET_REPORT
cos_presign_expire_seconds: int = 600 # env: COS_PRESIGN_EXPIRE_SECONDS
upload_max_bytes: int = 20_971_520 # env: UPLOAD_MAX_BYTES(20MB)
upload_allowed_types: str = "image/jpeg,image/png,image/webp" # env: UPLOAD_ALLOWED_TYPES
upload_scenes: dict[str, dict[str, Any]] = Field(..., alias="UPLOAD_SCENES_JSON")
# 默认:{"report_photo": {"bucket": "temp"}, "avatar": {"bucket": "temp"}}
```
(字段名/别名以 config.py 协调提交的实际代码为准——**开工先读 config.py 核对**,文档若有出入以代码为准。)
### 错误码(`errors.py` 已加)
- `UPLOAD_REJECTED = 40010`:400「不支持的图片类型或超出大小限制」,hint「请上传 20MB 以内的 JPG/PNG/WebP 图片」。
### 骨架(协调提交已建)
- `packages/storage/__init__.py`、`README.md`、`service.py`(空骨架);
- `apps/api/routers/uploads.py`(空骨架 router,prefix `/uploads`,已注册 main.py);
- `apps/api/deps.py` 已有 `StorageServiceDep`(**deps.py 禁改**;若构造参数不满足,在路由函数内用 `SettingsDep` 自建 service)。
### 分层位置
`packages.storage` 在 import-linter"共享能力层"。只准 import `packages.shared`/`packages.domain` 与 `qcloud_cos`/stdlib。
## 3. 实现要点
允许改动:`packages/storage/**`、`apps/api/routers/uploads.py`、测试文件。
1. **`packages/storage/schemas.py`**:
- `PresignRequest(scene: str, content_type: str, size_bytes: int)`(size ≥1);
- `PresignResponse(upload_url: str, object_key: str, expires_in: int, max_bytes: int, content_type: str)`。
2. **`packages/storage/service.py`**:
- 定义 `Protocol CosSigner`:`def presign_put(bucket: str, key: str, expires: int, content_type: str) -> str`(**把 SDK 隔离在 Protocol 后面**,单测用 FakeSigner,绝不连网);
- `QcloudCosSigner`:包 `CosS3Client`(构造参数 region/secret_id/secret_key),`presign_put` 调 `client.get_presigned_url(Method.PUT, bucket, key, Expire=expires, Headers={"Content-Type": content_type})`(`from qcloud_cos import CosConfig, CosS3Client`;`Method` 从 `qcloud_cos.method` import——以实际 SDK 签名为准,若 Headers 参数名不同按 SDK 文档调整,**以能签出带 content-type 约束的 PUT URL 为准**);
- `StorageService(settings, signer: CosSigner | None = None)`:signer 为 None 时用 `QcloudCosSigner` 惰性构造(**只有真实请求才建 client**;测试必传 FakeSigner);
- `def build_presign(user_id: int, request: PresignRequest) -> PresignResponse`:
- scene 不在 `settings.upload_scenes` → `AppError(UPLOAD_REJECTED)`(message 写"不支持的上传场景");
- content_type ∉ `settings.upload_allowed_types` 解析集合 → `UPLOAD_REJECTED`;
- size_bytes > `upload_max_bytes` → `UPLOAD_REJECTED`;
- 扩展名映射 `{image/jpeg: jpg, image/png: png, image/webp: webp}`(其它类型已被上一条拦下);
- object_key = `f"{scene}/{user_id}/{datetime:%Y%m%d}/{uuid4().hex}.{ext}"`(**这个 key 布局写死**:按场景分目录、按用户隔离、按日分组、uuid 防猜——注释说明这是 COS 侧做前缀审计与生命周期的基础);
- bucket = scenes 配置里的 `bucket`("temp"/"user"/"kb"/"report" → 对应 `settings.cos_bucket_*` 四字段映射);
- 调 signer 得 URL,返回 `PresignResponse(expires_in=settings.cos_presign_expire_seconds, max_bytes=settings.upload_max_bytes, ...)`。
3. **`apps/api/routers/uploads.py`**:
- `POST /presign`,`user_id: CurrentUserId` + settings;`ok(PresignResponse(...))`;
- 模块 docstring 写明:真实联调需在 `.env` 配 COS 四件套;本地单测不依赖任何真实 COS。
## 4. 测试要求
单测 `tests/unit/test_upload_presign.py`(全部用 FakeSigner,断言 URL/key/参数,**零网络**):
- 合法请求 → key 布局正确(scene 前缀、user_id 段、日期段、扩展名)、expires_in/max_bytes 回显;
- 非法 content_type(如 `application/pdf`)→ 40010;
- 超大小 → 40010;
- 未知 scene → 40010;
- bucket 映射:`{"bucket":"temp"}` → 使用 `cos_bucket_temp` 配置值(FakeSigner 捕获 bucket 参数断言);
- `upload_allowed_types` 字符串解析(空串/带空格容错)。
不需要集成测试(无 DB 依赖)。
## 5. 禁改清单(通用禁改之外)
`packages/llm/**`、`packages/billing/**`、`packages/identity/**`、`packages/analytics/**`、`apps/api/routers/{agent,auth,quota,tasks,events}.py`、`apps/api/deps.py`、`tests/integration/**`。
## 6. 验收标准(FR-SYS-03)
1. `POST /api/v1/uploads/presign`(含 `/v1` 别名)可用,认证必需;
2. 类型/大小/场景三重服务端校验,错误码统一 40010;
3. key 布局可审计(scene/user/日期/uuid);
4. 单测全绿且无任何真实网络调用(FakeSigner 注入);
5. 全部检查绿(mypy 范围同 T2 列表)。
## 7. 提交信息
```
feat(storage): COS presigned PUT URL endpoint for direct upload
POST /uploads/presign:场景/类型/大小三重校验 + 可审计 object_key 布局 +
SDK 隔离在 CosSigner Protocol 后(FR-SYS-03,T-P1-27)。
偏差说明:<如有写在这里,无则写"无">
```