Files
hair/docs/网关-开发任务书.md
T

218 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 外网网关 — 开发任务书(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`。
- **推荐通用实现**:递归遍历 `data`,凡 key 以 `_base64` 结尾 → 落盘 → 改成同前缀 `_url`
**包括数组元素内部的字段**(接口2/5 的图片字段在数组里)。这样新增字段自动覆盖、无需逐一硬编码。
- `*_base64` 值可能为 **null**(如接口2/3 的生发图,ComfyUI 未起/失败时)→ 该项**保留 null、不落盘、不生成 url**。
**完整字段映射表**worker 内部 `*_base64` → 对外 `*_url`,以 `docs/接口文档.md` 为准):
| 接口 | 路径 | worker 字段(内部) | 对外字段 | 位置 |
|------|------|--------------------|----------|------|
| 1 | `/api/v1/face/measure` | `annotated_image_base64` | `annotated_image_url` | data 顶层 |
| 2 | `/api/v1/hair/grow` | `results[].image_base64` | `results[].image_url` | **数组元素** |
| 2 | 〃 | `results[].grown_image_base64` | `results[].grown_image_url` | **数组元素**(可空) |
| 3 | `/api/v1/hair/grow-b` | `hair_growth_image_base64` | `hair_growth_image_url` | data 顶层(可空) |
| 4 | `/api/v1/face/features` | —(无图片字段,原样透传)| — | — |
| 5 | `/api/v1/hairline/generate` | `hairline_images[].image_base64` | `hairline_images[].image_url` | **数组元素** |
> 接口2/5 **新增了必填 `gender` 入参**、接口3 用 `marked_image_*`+`original_image_*`——这些都是
> **请求 multipart 参数**,网关原样透传即可,无需改造(网关对入参透明)。
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. **接口实现进度****接口 1/2/3/4/5 worker 均已真实实现**。接口 4(用户特征)返回 `features`(JSON 字符串)、
**无图片字段,网关原样透传**;但接口4 worker 会调外网豆包视觉模型(`ark.cn-beijing.volces.com`)——
网关本身不受影响,但要知道该接口耗时含一次远程大模型调用(数秒)。
5. **生发图耗时**:接口 2(一次 N 张 Flux~18s)、接口 3~6s)经 ComfyUI 同步出图,**`request_timeout_seconds` 要调大**(建议 ≥120s),否则网关会先超时换 worker 重试。
---
> **文档版本**: v1.0 **创建日期**: 2026-06-14 配套:系统架构 v1.0
> **关键约束**: 接口文档不变;不跑算法;每 worker 并发=1;无后端→1007base64→落盘→URL