12 KiB
外网网关 — 开发任务书(AI Agent 执行版)
在 外网机(
hair.xiangsilian.com) 上开发。本任务书自包含,只负责网关这一层。 配套:系统架构-网关与高性能后端.md(两端共享的契约,先读)。 worker(GPU 机)侧算法由另一份任务书负责:接口1-四庭七眼测量-开发任务书.md,本机不涉及。
执行者:AI coding agent。严格按阶段顺序执行,每阶段跑完「验证方法」通过后再进入下一阶段。
0. 网关是什么 / 不是什么
- 是:一个无状态的薄反向代理。对外保持 HTTPS 接口与接口文档完全不变;对内把请求转发给高性能 worker 池,做健康检查、空闲派发、鉴权、把 worker 返回的 base64 标注图落盘成对外 URL。
- 不是:不跑任何算法(无 MediaPipe / torch / BiSeNet);不解析业务入参;不持有业务状态。所有业务逻辑和错误码(1001–1008)都来自 worker,网关原样透传。
网关唯一自行产生的错误:无可用 worker / 全忙排队超时 → code: 1007(复用系统错误,不新增错误码)。
1. 总体约束
- 对外契约零变化:客户端看到的 URL、请求方式(multipart/form-data,三选一图片)、响应结构
{code,message,request_id,data}、字段名、错误码,全部与docs/接口文档.md一致。客户端无感知拆分。 - 代理全部 5 个接口:
/api/v1/face/measure、/hair/grow、/hair/grow-b、/face/features、/hairline/generate。worker 跑完整 app,网关统一代理。 - 技术栈:Python + FastAPI +
httpx(异步转发)+ uvicorn。不引入 torch/mediapipe/opencv 等重依赖,网关保持轻量。 - 无状态:除"健康池 + worker 忙闲状态"这点运行时状态外,不持久化业务数据。
- 密码、worker 列表等走配置文件,不硬编码、不入 git。
2. 配置文件
gateway/config.json(运维维护,入 .gitignore;仓库只放 gateway/config.example.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. 阶段一:骨架 + 配置加载
开发步骤
- 新建目录:
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) config.py:加载config.json,缺字段给默认值,启动时校验workers非空、密码非占位值(占位值打 WARNING)。app.py:建 FastAPI 应用,挂载/static,加/gateway-health(网关自身健康,区别于 worker 的/health)。- 更新根
.gitignore:忽略gateway/config.json、static/annotations/*(保留.gitkeep)。
验证
./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 数)
开发步骤
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)。
- 后台 asyncio 任务,每
- 派发
acquire_worker():从在线池里挑一个空闲 worker 占用;全忙则await等待,最多queue_wait_seconds,超时抛NoWorkerAvailable;在线池为空也抛该异常。用完release。 - 路由层捕获
NoWorkerAvailable→ 返回{code:1007, message:"后端服务暂不可用,请稍后重试", ...}。
验证
- 配 2 个假 worker(本地起两个返回
/health200 + 简单 echo 的 stub):并发打 3 个请求 → 前 2 个并行、第 3 个排队后成功。 - 停掉 1 个 stub →
interval×unhealthy_threshold内自动下线,请求只走存活的。 - 全停 → 请求返回
code==1007。
# 可用 python 起两个 stub:每个监听不同端口,/health 返回200,业务接口 sleep 1s 再回
完成标准:健康检查上下线正确;并发=在线 worker 数;全忙排队、池空→1007。
5. 阶段三:鉴权头 + 转发 + 标注图改写
开发步骤
-
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)。
- 透传 method、multipart/form body、查询参数;附加
-
base64 → URL 改写:拿到 worker 的 JSON 响应后,对约定的图片字段:
- worker 用
*_base64承载 PNG(如接口1annotated_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/measureannotated_image_base64annotated_image_urldata 顶层 2 /api/v1/hair/growresults[].image_base64results[].image_url数组元素 2 〃 results[].grown_image_base64results[].grown_image_url数组元素(可空) 3 /api/v1/hair/grow-bhair_growth_image_base64hair_growth_image_urldata 顶层(可空) 4 /api/v1/face/features—(无图片字段,原样透传) — — 5 /api/v1/hairline/generatehairline_images[].image_base64hairline_images[].image_url数组元素 接口2/5 新增了必填
gender入参、接口3 用marked_image_*+original_image_*——这些都是 请求 multipart 参数,网关原样透传即可,无需改造(网关对入参透明)。 - worker 用
-
5 个接口路由统一走「选 worker → 转发 → 改写 → 返回」一条链路。
验证(端到端,最终冒烟)
# 经网关(对外 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. 阶段四:静态托管 + 清理
开发步骤
- 网关
app.mount("/static", StaticFiles(directory="static"), ...)托管落盘的标注图。 - 加定期清理:
static/annotations/会持续增长,按时间(如保留 N 天)或容量清理。可用进程内定时任务或外部 cron(任选,记录方案)。
验证
- 生成的图能经
https://hair.xiangsilian.com/static/annotations/<uuid>.png访问(200)。 - 清理任务按规则删除过期文件,不误删近期文件。
完成标准:静态图可公网访问;清理任务可控。
7. 阶段五:部署
开发步骤
- nginx:443 → 网关进程(uvicorn)。沿用现有
nginx/hair.conf风格。 - systemd 管理网关进程(可继续用
hair.service,或新建hair-gateway.service)。 config.json就位(worker 列表 + 密码);static/annotations/可写。
验证
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. 风险与注意
- 联调依赖 worker:阶段四之前可用本地 stub worker(返回
/health200 + 假的 base64 图)独立开发;worker 真机就绪后再换真实地址端到端联调。 - 接口文档是字段唯一权威:图片字段映射表、对外字段名都以
docs/接口文档.md为准,冲突时以文档为准并在 PR 说明指出。 - 安全(先跑通后处理,已知项):
:28187当前 HTTP 明文 + 公网可达,密码明文传输;后续建议加 TLS / 内网 / IP 白名单(见架构文档 §13/§14)。本阶段不阻塞。 - 接口实现进度:接口 1/2/3/4/5 worker 均已真实实现。接口 4(用户特征)返回
features(JSON 字符串)、 无图片字段,网关原样透传;但接口4 worker 会调外网豆包视觉模型(ark.cn-beijing.volces.com)—— 网关本身不受影响,但要知道该接口耗时含一次远程大模型调用(数秒)。 - 生发图耗时:接口 2(一次 N 张 Flux,~18s)、接口 3(~6s)经 ComfyUI 同步出图,
request_timeout_seconds要调大(建议 ≥120s),否则网关会先超时换 worker 重试。
文档版本: v1.0 | 创建日期: 2026-06-14 | 配套:系统架构 v1.0 关键约束: 接口文档不变;不跑算法;每 worker 并发=1;无后端→1007;base64→落盘→URL