Files
hair/docs/网关-开发任务书.md
T
xslandClaude Opus 4.8 147fef6ca6 docs(网关): 补全 base64→url 映射表(接口1/2/3/5) + 嵌套数组/超时提醒
接口 1/2/3/5 已真实实现,补齐网关需要的字段映射,供网关开发参考:
- 网关任务书 §6:完整映射表(annotated_image / results[].image / results[].grown_image /
  best_hairline_image / hair_growth_image / hairline_images[].image);推荐"凡 *_base64 递归改写"
  通用实现;可空字段(生发图)保留 null;gender 等入参网关透传无需改造
- 网关任务书 §9:接口4 仍 mock;生发接口(2/3) ComfyUI 同步出图慢,request_timeout 调大≥120s
- 架构 §9:标注两个易漏点(数组内字段需递归、生发图可空)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 00:29:33 +08:00

12 KiB
Raw Blame History

外网网关 — 开发任务书(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. 总体约束

  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):

{
  "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 固定 1shared_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.jsonstatic/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 数)

开发步骤

  1. pool.py
    • 后台 asyncio 任务,每 interval_seconds 给每个 worker 发 GET {worker}/health(带 X-Internal-Tokentimeout_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
# 可用 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 best_hairline_image_base64 best_hairline_image_url data 顶层
    3 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 → 转发 → 改写 → 返回」一条链路。

验证(端到端,最终冒烟)

# 经网关(对外 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/ 可写。

验证

sudo systemctl restart hair-gateway && sudo systemctl status hair-gateway
# 端到端冒烟(同阶段三);journalctl 无 ERROR

完成标准:线上经 HTTPS 走通全链路,标注图可访问,日志无异常。


8. 交付清单(网关侧 DoD

  • gateway/app.pyconfig.pypool.pyforward.pyconfig.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/5 worker 已真实实现(图片字段见上方映射表);接口 4(用户特征)仍为 mock(无图片字段,原样透传)。
  5. 生发图耗时:接口 2(一次 N 张 Flux~18s)、接口 3~6s)经 ComfyUI 同步出图,request_timeout_seconds 要调大(建议 ≥120s),否则网关会先超时换 worker 重试。

文档版本: v1.0 创建日期: 2026-06-14 配套:系统架构 v1.0 关键约束: 接口文档不变;不跑算法;每 worker 并发=1;无后端→1007base64→落盘→URL