218 lines
12 KiB
Markdown
218 lines
12 KiB
Markdown
# 外网网关 — 开发任务书(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 返回的 1001–1008 业务响应**原样透传**给客户端(不要改 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 → 401(worker 侧行为);经网关正常。
|
||
|
||
**完成标准**: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. nginx:443 → 网关进程(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;无后端→1007;base64→落盘→URL
|