docs: 拆分网关与worker文档,按机器分别开发

- 新增 网关-开发任务书.md: 独立自包含的网关侧任务书(骨架/健康检查派发/
  鉴权转发/base64改URL/静态托管/部署/DoD),在外网机开发
- worker任务书 v1.5: 移除§16网关章节,顶部标注本机职责,DoD只留worker侧,
  指向独立网关任务书
- 新增 docs/README.md: 文档索引,标清worker机/网关机/共享分别读哪些
This commit is contained in:
xsl
2026-06-14 14:29:48 +08:00
parent 2e789d4efa
commit bf68a32d9f
3 changed files with 243 additions and 63 deletions
+33
View File
@@ -0,0 +1,33 @@
# 文档索引(按开发机器划分)
系统拆分为两台机器开发:**外网网关** 和 **高性能 workerGPU**。下面标清每台机器该读哪些文档。
## 🌐 两端共享(都要读)
| 文档 | 作用 |
|------|------|
| [系统架构-网关与高性能后端.md](系统架构-网关与高性能后端.md) | 两层拆分的总设计与契约:拓扑、职责、配置、鉴权、base64→URL、错误处理。**两端的接口约定,先读。** |
| [接口文档.md](接口文档.md) | 对外 API 契约(字段/错误码)。**拆分后保持不变**,是字段命名的唯一权威。 |
| [旷视具体需求.md](旷视具体需求.md) | 原始需求 |
## 🖥️ 高性能 worker 机(GPU
跑完整 `app.py` + `face_analysis`,做真正的算法。
| 文档 | 作用 |
|------|------|
| [接口1-四庭七眼测量-技术实现方案.md](接口1-四庭七眼测量-技术实现方案.md) | 四庭七眼算法方案(MediaPipe / BiSeNet / 标定 / 标注图 / 误差) |
| [接口1-四庭七眼测量-开发任务书.md](接口1-四庭七眼测量-开发任务书.md) | worker 侧开发任务书(阶段一~十 + 精度验证),AI agent 执行 |
| [../OFFLINE_ASSETS.md](../OFFLINE_ASSETS.md) | 离线模型权重/字体清单(内网部署)。worker 需要这些权重 |
## 🚪 外网网关机
薄反向代理,不跑算法。
| 文档 | 作用 |
|------|------|
| [网关-开发任务书.md](网关-开发任务书.md) | 网关侧开发任务书(健康检查 / 派发 / 鉴权 / base64→URL / 部署),AI agent 执行 |
---
> 开发顺序建议:worker 先跑通(本机即可开发验证)→ worker 部署到 GPU 机 → 网关接入联调。网关在 worker 就绪前可用本地 stub worker 独立开发。
@@ -1,13 +1,14 @@
# 接口 1:四庭七眼测量 — 开发任务书(AI Agent 执行版)
# 接口 1:四庭七眼测量 — 开发任务书(worker 侧 · AI Agent 执行版)
> **在高性能 worker(GPU 机)上开发。** 本任务书只负责 worker 侧算法逻辑。
> 配套技术方案:[`接口1-四庭七眼测量-技术实现方案.md`](接口1-四庭七眼测量-技术实现方案.md)
> 系统架构:[`系统架构-网关与高性能后端.md`](系统架构-网关与高性能后端.md)
> 系统架构(两端共享契约,先读):[`系统架构-网关与高性能后端.md`](系统架构-网关与高性能后端.md)
> **网关侧在另一台机器开发,有独立任务书**:[`网关-开发任务书.md`](网关-开发任务书.md),本机不涉及。
> 执行者:AI coding agent。请**严格按阶段顺序**执行,每个阶段完成后运行该阶段的「验证方法」,**通过后再进入下一阶段**。
> 📌 **架构拆分(务必先读架构文档)**:系统分为「外网网关」+「高性能 worker(GPU)」两层。
> - **本任务书的阶段一~十(§2–§11)都在 worker 上实现**——worker 跑完整 `app.py` + `face_analysis`,用 GPU。
> - **网关是独立工作流,见 §16**(薄代理:健康检查 / 派发 / 鉴权 / 标注图落盘改 URL)。
> - 与单机版的两处关键差异:① torch 用 **GPU(CUDA)** 版;② 阶段八 handler **返回 `annotated_image_base64` 而非保存到 static + URL**(落盘改 URL 由网关做)。
> 📌 **本机职责**worker 跑完整 `app.py` + `face_analysis`,用 GPU。与单机版的两处关键差异:
> - ① torch 用 **GPU(CUDA)** 版;
> - ② 阶段八 handler **返回 `annotated_image_base64`,不保存到 static、不拼 URL**落盘改 URL 由网关做)。
> - 接口文档(对外契约)**完全不变**。
---
@@ -258,7 +259,7 @@ curl -s -H "$H" -X POST $F -F image_file=@/tmp/oversize.bin # 1006
curl -s -o /dev/null -w "%{http_code}\n" -X POST $F -F image_file=@tests/fixtures/frontal.jpg # 401
# 1003) 侧脸:仓库无素材,用 mock 大 yaw 在单测中覆盖
```
- 正常用例 data 含 `annotated_image_base64`**不是** URL——落盘改 URL 由网关做,见 §16);base64 解码后是合法 PNG。
- 正常用例 data 含 `annotated_image_base64`**不是** URL——落盘改 URL 由网关做,见网关任务书);base64 解码后是合法 PNG。
- 不带 token 返回 401`/health` 不需要 token。
- `/docs` Swagger 正常加载,该接口 schema 未破坏。
@@ -314,13 +315,12 @@ nvidia-smi # 确认推理时 GPU 被占用
**完成标准**:worker 直连返回真实数据(base64 图)、鉴权生效、`/health` 就绪、GPU 在用。
> 端到端(经网关的 HTTPS 冒烟)见 §16 网关工作流的验证。
> 端到端(经网关的 HTTPS 冒烟)见 [`网关-开发任务书.md`](网关-开发任务书.md) 的验证。
---
## 12. 总交付清单(Definition of Done
## 12. 总交付清单(worker 侧 Definition of Done
**worker 侧(算法)**
- [ ] `requirements.txt`torch CUDA 版)/ `.gitignore` / `scripts/download_weights.sh`
- [ ] `face_analysis/``detector.py``pose.py``calibration.py``hair_segmenter.py``measure.py``annotation.py``face_mesh_landmarks.py``fonts/``weights/`
- [ ] `app.py``/api/v1/face/measure` 真实实现(移除该接口 Mock),**返回 `annotated_image_base64`**
@@ -329,12 +329,7 @@ nvidia-smi # 确认推理时 GPU 被占用
- [ ] workerGPU 机 :28187)部署冒烟通过,GPU 在用
- [ ] worker 返回 data 含 `annotated_image_base64``hairline_source``head_pose`,业务字段与 `docs/接口文档.md` 对齐
**网关侧(见 §16**
- [ ] 网关代理 5 个接口,对外契约/字段/URL 与接口文档**完全一致**
- [ ] worker 池健康检查 + 空闲派发(并发=worker 数)+ 全忙排队/无后端→1007
- [ ] 共享密码鉴权(配置文件,可轮换)
- [ ] base64 → 落盘 `static/annotations/` → 改写 `annotated_image_url`
- [ ] 端到端 HTTPS 冒烟:`https://hair.xiangsilian.com/...` 返回真实数据、`annotated_image_url` 可公网访问
> 网关侧的交付清单在 [`网关-开发任务书.md`](网关-开发任务书.md)(另一台机器开发),本任务书不含。
---
@@ -500,50 +495,4 @@ def test_landmark_overlay():
---
## 16. 网关工作流(独立于上面 worker 阶段)
> 完整设计见 [`系统架构-网关与高性能后端.md`](系统架构-网关与高性能后端.md)。网关是**新建的薄代理应用**,与 worker 算法解耦,可并行开发。worker 至少完成阶段八(能直连返回 base64)后即可联调。
### 16.1 网关骨架与配置
**开发步骤**
1. 新建 `gateway/`(独立 FastAPI/asgi 应用):`gateway/app.py``gateway/config.example.json``gateway/pool.py`(健康池+派发)、`gateway/forward.py`(转发+图片改写)。
2. 加载 `config.json`(架构文档 §4 schema):worker 列表、密码、健康检查/派发参数。`config.json``.gitignore`,仓库只放 `config.example.json`
**验证**:启动网关,加载配置无误,打印解析出的 worker 列表与参数。
### 16.2 健康检查 + 派发(并发=worker 数)
**开发步骤**
1. 后台任务周期轮询每个 worker `/health`(带 token),按 `unhealthy/healthy_threshold` 维护在线池(架构 §5)。
2. 每个 worker 维护忙/闲状态;新请求选一个**空闲健康** worker 派发,并发=1/worker;全忙则排队至 `queue_wait_seconds`,超时→1007;池空→1007(架构 §6/§10)。
**验证**
- 配 2 个 worker,并发打 3 个请求:前 2 个并行、第 3 个排队;返回均正常。
- 停掉 1 个 worker:健康检查在 `interval×threshold` 内将其下线,请求自动只走存活的。
- 全停:请求返回 `code==1007`
### 16.3 鉴权 + 转发 + 标注图改写
**开发步骤**
1. 转发:原样透传客户端请求体,附加 `X-Internal-Token`(架构 §7/§8)。worker 的 10011008 原样透传给客户端。
2. **图片改写**worker 响应里的 `*_base64` 字段 → 解码落盘 `static/annotations/{uuid}.png` → 删除 base64、写入对应 `*_url`=`https://hair.xiangsilian.com/static/...`(架构 §9)。映射表以接口文档为准(接口1:`annotated_image`)。
3. 网关托管 `/static/*`;加标注图定期清理任务。
4. 转发失败按 `max_retries` 换 worker 重试;耗尽→1007。
**验证(端到端,最终冒烟)**
```bash
# 经网关 HTTPS(对外,无需 token——token 是网关→worker 内部的)
curl -s -X POST https://hair.xiangsilian.com/api/v1/face/measure \
-F image_file=@tests/fixtures/frontal.jpg | python -m json.tool
# 期望:与接口文档完全一致——data 含 annotated_image_url(不是 base64
curl -sI https://hair.xiangsilian.com/static/annotations/<uuid>.png # 200
```
- **对外响应字段/URL 与接口文档逐一一致**(客户端无感知拆分)。
- 直接打 worker 不带 token → 401;经网关正常。
**网关完成标准**:5 接口代理正常;多 worker 并发与排队符合预期;后端全挂→1007;标注图经网关落盘且公网可访问;**对外契约零变化**。
---
> **任务书版本**: v1.4 **创建日期**: 2026-06-13v1.4:适配网关+worker 拆分架构——worker 返 base64/GPU/鉴权,新增 §16 网关工作流)| 配套技术方案 v2.0 / 系统架构 v1.0
> **任务书版本**: v1.5 **创建日期**: 2026-06-13(v1.5:拆出网关任务书到独立文档,本书聚焦 worker 侧)| 配套技术方案 v2.0 / 系统架构 v1.0 / 网关任务书 v1.0
+198
View File
@@ -0,0 +1,198 @@
# 外网网关 — 开发任务书(AI Agent 执行版)
> 在 **外网机(`hair.xiangsilian.com`** 上开发。本任务书自包含,只负责**网关**这一层。
> 配套:[`系统架构-网关与高性能后端.md`](系统架构-网关与高性能后端.md)(两端共享的契约,**先读**)。
> worker(GPU 机)侧算法由另一份任务书负责:`接口1-四庭七眼测量-开发任务书.md`,本机不涉及。
> 执行者:AI coding agent。**严格按阶段顺序**执行,每阶段跑完「验证方法」通过后再进入下一阶段。
---
## 0. 网关是什么 / 不是什么
- **是**:一个无状态的**薄反向代理**。对外保持 HTTPS 接口与接口文档**完全不变**;对内把请求转发给高性能 worker 池,做健康检查、空闲派发、鉴权、把 worker 返回的 base64 标注图落盘成对外 URL。
- **不是**:不跑任何算法(无 MediaPipe / torch / BiSeNet);不解析业务入参;不持有业务状态。所有业务逻辑和错误码(1001–1008)都来自 worker,网关原样透传。
**网关唯一自行产生的错误**:无可用 worker / 全忙排队超时 → `code: 1007`(复用系统错误,不新增错误码)。
---
## 1. 总体约束
1. **对外契约零变化**:客户端看到的 URL、请求方式(multipart/form-data,三选一图片)、响应结构 `{code,message,request_id,data}`、字段名、错误码,全部与 `docs/接口文档.md` 一致。客户端**无感知**拆分。
2. **代理全部 5 个接口**`/api/v1/face/measure``/hair/grow``/hair/grow-b``/face/features``/hairline/generate`。worker 跑完整 app,网关统一代理。
3. **技术栈**Python + FastAPI + `httpx`(异步转发)+ uvicorn。**不引入** torch/mediapipe/opencv 等重依赖,网关保持轻量。
4. **无状态**:除"健康池 + worker 忙闲状态"这点运行时状态外,不持久化业务数据。
5. 密码、worker 列表等走**配置文件**,不硬编码、不入 git。
---
## 2. 配置文件
`gateway/config.json`(运维维护,**入 `.gitignore`**;仓库只放 `gateway/config.example.json`):
```json
{
"workers": [
"http://hair.xiangsilian.com:28187",
"http://10.0.0.12:28187"
],
"shared_password": "REPLACE_ME_ROTATE_PERIODICALLY",
"public_base_url": "https://hair.xiangsilian.com",
"static_dir": "static/annotations",
"health_check": {
"path": "/health",
"interval_seconds": 8,
"timeout_seconds": 3,
"unhealthy_threshold": 2,
"healthy_threshold": 1
},
"dispatch": {
"per_worker_concurrency": 1,
"queue_wait_seconds": 30,
"request_timeout_seconds": 60,
"retry_on_failure": true,
"max_retries": 1
}
}
```
字段含义见架构文档 §4。要点:`workers` 手动增删(**加一个就多一路并发**);`per_worker_concurrency` 固定 1`shared_password` 网关调用 worker 时通过 `X-Internal-Token` 头发送。
---
## 3. 阶段一:骨架 + 配置加载
**开发步骤**
1. 新建目录:
```
gateway/
├── app.py # FastAPI 应用 + 5 接口代理路由
├── config.py # 读取/校验 config.json
├── config.example.json
├── pool.py # 健康池 + worker 忙闲状态 + 派发
├── forward.py # httpx 转发 + base64→URL 改写
└── __init__.py
static/annotations/ # 标注图落盘目录(.gitkeep
```
2. `config.py`:加载 `config.json`,缺字段给默认值,启动时校验 `workers` 非空、密码非占位值(占位值打 WARNING)。
3. `app.py`:建 FastAPI 应用,挂载 `/static`,加 `/gateway-health`(网关自身健康,区别于 worker 的 `/health`)。
4. 更新根 `.gitignore`:忽略 `gateway/config.json`、`static/annotations/*`(保留 `.gitkeep`)。
**验证**
```bash
./venv/bin/uvicorn gateway.app:app --host 127.0.0.1 --port 8080 &
curl -s http://127.0.0.1:8080/gateway-health # 200
```
启动日志打印解析出的 worker 列表与派发参数。
**完成标准**:网关起得来,配置正确加载,无 worker 时也不崩。
---
## 4. 阶段二:健康检查 + 空闲派发(并发=worker 数)
**开发步骤**
1. `pool.py`
- 后台 asyncio 任务,每 `interval_seconds` 给每个 worker 发 `GET {worker}/health`(带 `X-Internal-Token``timeout_seconds` 超时)。
- 连续失败 `unhealthy_threshold` 次 → 下线;重新连续成功 `healthy_threshold` 次 → 上线。维护在线集合。
- 每个 worker 一个 `asyncio.Lock`(或容量=1 的信号量)表示忙闲(`per_worker_concurrency=1`)。
2. 派发 `acquire_worker()`:从在线池里挑一个**空闲** worker 占用;全忙则 `await` 等待,最多 `queue_wait_seconds`,超时抛 `NoWorkerAvailable`;在线池为空也抛该异常。用完 `release`。
3. 路由层捕获 `NoWorkerAvailable` → 返回 `{code:1007, message:"后端服务暂不可用,请稍后重试", ...}`。
**验证**
- 配 2 个**假 worker**(本地起两个返回 `/health` 200 + 简单 echo 的 stub):并发打 3 个请求 → 前 2 个并行、第 3 个排队后成功。
- 停掉 1 个 stub → `interval×unhealthy_threshold` 内自动下线,请求只走存活的。
- 全停 → 请求返回 `code==1007`。
```bash
# 可用 python 起两个 stub:每个监听不同端口,/health 返回200,业务接口 sleep 1s 再回
```
**完成标准**:健康检查上下线正确;并发=在线 worker 数;全忙排队、池空→1007。
---
## 5. 阶段三:鉴权头 + 转发 + 标注图改写
**开发步骤**
1. `forward.py`:用 `httpx.AsyncClient` 把客户端请求**原样转发**到选中的 worker 对应路径:
- 透传 method、multipart/form body、查询参数;附加 `X-Internal-Token: <shared_password>` 头。
- `request_timeout_seconds` 超时;连接失败/超时/5xx/401 视为该 worker 异常 → 标记不健康,按 `max_retries` 换 worker 重试;耗尽 → 1007。
- worker 返回的 10011008 业务响应**原样透传**给客户端(不要改 code)。
2. **base64 → URL 改写**:拿到 worker 的 JSON 响应后,对约定的图片字段:
- worker 用 `*_base64` 承载 PNG(如接口1 `annotated_image_base64`)。
- 网关:解码 → 存 `static/annotations/{uuid}.png` → **删除 `*_base64`,新增 `*_url`** = `{public_base_url}/static/annotations/{uuid}.png`。
- 维护一张「接口路径 → 图片字段名」映射表(接口1:`annotated_image`;接口3/5 待其实现后补;**以接口文档字段名为准**)。
3. 5 个接口路由统一走「选 worker → 转发 → 改写 → 返回」一条链路。
**验证(端到端,最终冒烟)**
```bash
# 经网关(对外 HTTPS;客户端不需要 token——token 是网关→worker 内部的)
curl -s -X POST https://hair.xiangsilian.com/api/v1/face/measure \
-F image_file=@<一张人像> | python -m json.tool
# 期望:与接口文档完全一致——data 含 annotated_image_url(不是 base64
curl -sI https://hair.xiangsilian.com/static/annotations/<uuid>.png # 200
```
- 对外响应字段/URL 与 `docs/接口文档.md` **逐一一致**。
- 响应里**不应**出现 `*_base64`(已被网关消化)。
- 直接打 worker 不带 token → 401worker 侧行为);经网关正常。
**完成标准**:5 接口代理通;base64 落盘改 URL 正确;对外契约零变化;失败重试与 1007 生效。
---
## 6. 阶段四:静态托管 + 清理
**开发步骤**
1. 网关 `app.mount("/static", StaticFiles(directory="static"), ...)` 托管落盘的标注图。
2. 加**定期清理**`static/annotations/` 会持续增长,按时间(如保留 N 天)或容量清理。可用进程内定时任务或外部 cron(任选,记录方案)。
**验证**
- 生成的图能经 `https://hair.xiangsilian.com/static/annotations/<uuid>.png` 访问(200)。
- 清理任务按规则删除过期文件,不误删近期文件。
**完成标准**:静态图可公网访问;清理任务可控。
---
## 7. 阶段五:部署
**开发步骤**
1. nginx443 → 网关进程(uvicorn)。沿用现有 `nginx/hair.conf` 风格。
2. systemd 管理网关进程(可继续用 `hair.service`,或新建 `hair-gateway.service`)。
3. `config.json` 就位(worker 列表 + 密码);`static/annotations/` 可写。
**验证**
```bash
sudo systemctl restart hair-gateway && sudo systemctl status hair-gateway
# 端到端冒烟(同阶段三);journalctl 无 ERROR
```
**完成标准**:线上经 HTTPS 走通全链路,标注图可访问,日志无异常。
---
## 8. 交付清单(网关侧 DoD)
- [ ] `gateway/``app.py`、`config.py`、`pool.py`、`forward.py`、`config.example.json`
- [ ] 代理 5 个接口,对外契约/字段/URL 与接口文档**完全一致**
- [ ] worker 池健康检查(上下线)+ 空闲派发(并发=在线 worker 数)+ 全忙排队/无后端→1007
- [ ] 共享密码鉴权(`X-Internal-Token`,配置文件,可轮换)
- [ ] base64 → 落盘 `static/annotations/` → 改写 `*_url`,响应无 `*_base64` 残留
- [ ] 静态托管 + 定期清理
- [ ] 端到端 HTTPS 冒烟通过,`annotated_image_url` 公网可访问
- [ ] `config.json` 入 `.gitignore`,仓库只留 `config.example.json`
---
## 9. 风险与注意
1. **联调依赖 worker**:阶段四之前可用**本地 stub worker**(返回 `/health` 200 + 假的 base64 图)独立开发;worker 真机就绪后再换真实地址端到端联调。
2. **接口文档是字段唯一权威**:图片字段映射表、对外字段名都以 `docs/接口文档.md` 为准,冲突时以文档为准并在 PR 说明指出。
3. **安全(先跑通后处理,已知项)**:`:28187` 当前 HTTP 明文 + 公网可达,密码明文传输;后续建议加 TLS / 内网 / IP 白名单(见架构文档 §13/§14)。本阶段不阻塞。
4. **图片字段未实现的接口**:接口 2/4/5 worker 暂为 mock,网关先按"无图片字段或透传"处理,待 worker 实现对应图片后再补映射。
---
> **文档版本**: v1.0 **创建日期**: 2026-06-14 配套:系统架构 v1.0
> **关键约束**: 接口文档不变;不跑算法;每 worker 并发=1;无后端→1007base64→落盘→URL