Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf68a32d9f | ||
|
|
2e789d4efa |
+6
-1
@@ -59,7 +59,12 @@ NotoSansCJKsc-Regular.otf https://github.com/notofonts/noto-cjk/raw/main/Sans/OT
|
||||
|
||||
## 还差什么(pip 依赖)
|
||||
|
||||
模型已就位,但**内网机还需要 Python 依赖的离线 wheel 包**(`mediapipe`/`opencv-python`/`torch` CPU 版等),否则 `pip install` 在内网无法联网安装。这部分**与目标机的操作系统和 Python 版本强相关**,需确认后单独打包:见仓库提交说明或联系下载方补充 `wheels/` 目录。
|
||||
模型已就位,但**内网机还需要 Python 依赖的离线 wheel 包**,否则 `pip install` 在内网无法联网安装。这部分**与目标机的操作系统、Python 版本、CUDA 版本强相关**,需确认后单独打包:
|
||||
|
||||
- **worker(GPU 机)**:`mediapipe` / `opencv-python` / `numpy<2` / `Pillow` / **`torch`+`torchvision` 的 CUDA 版**(按 GPU 的 CUDA 版本选 cu118/cu121 等)+ FastAPI/uvicorn 全家桶。
|
||||
- **网关机**:很轻,只需 FastAPI/uvicorn/httpx 等代理依赖,**不需要 torch/mediapipe**。
|
||||
|
||||
> 架构已拆分(见 `docs/系统架构-网关与高性能后端.md`):算法依赖只装在 worker,网关保持轻量。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
# 文档索引(按开发机器划分)
|
||||
|
||||
系统拆分为两台机器开发:**外网网关** 和 **高性能 worker(GPU)**。下面标清每台机器该读哪些文档。
|
||||
|
||||
## 🌐 两端共享(都要读)
|
||||
|
||||
| 文档 | 作用 |
|
||||
|------|------|
|
||||
| [系统架构-网关与高性能后端.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 独立开发。
|
||||
+58
-39
@@ -1,8 +1,16 @@
|
||||
# 接口 1:四庭七眼测量 — 开发任务书(AI Agent 执行版)
|
||||
# 接口 1:四庭七眼测量 — 开发任务书(worker 侧 · AI Agent 执行版)
|
||||
|
||||
> **在高性能 worker(GPU 机)上开发。** 本任务书只负责 worker 侧算法逻辑。
|
||||
> 配套技术方案:[`接口1-四庭七眼测量-技术实现方案.md`](接口1-四庭七眼测量-技术实现方案.md)
|
||||
> 系统架构(两端共享契约,先读):[`系统架构-网关与高性能后端.md`](系统架构-网关与高性能后端.md)
|
||||
> **网关侧在另一台机器开发,有独立任务书**:[`网关-开发任务书.md`](网关-开发任务书.md),本机不涉及。
|
||||
> 执行者:AI coding agent。请**严格按阶段顺序**执行,每个阶段完成后运行该阶段的「验证方法」,**通过后再进入下一阶段**。
|
||||
|
||||
> 📌 **本机职责**:worker 跑完整 `app.py` + `face_analysis`,用 GPU。与单机版的两处关键差异:
|
||||
> - ① torch 用 **GPU(CUDA)** 版;
|
||||
> - ② 阶段八 handler **返回 `annotated_image_base64`,不保存到 static、不拼 URL**(落盘改 URL 由网关做)。
|
||||
> - 接口文档(对外契约)**完全不变**。
|
||||
|
||||
---
|
||||
|
||||
## 0. 背景与目标
|
||||
@@ -219,38 +227,40 @@ print('numpy', numpy.__version__); print('mediapipe', mediapipe.__version__)"
|
||||
## 9. 阶段八:接入 app.py
|
||||
|
||||
**开发步骤**
|
||||
1. 在 `app.py` 替换 `/api/v1/face/measure` 的 Mock 实现:
|
||||
1. 在 `app.py`(**worker 侧**)替换 `/api/v1/face/measure` 的 Mock 实现:
|
||||
- 解析三选一图片输入(沿用现有 URL/base64/file 处理;URL 需下载,base64 需去前缀解码)。
|
||||
- 校验:大小 ≤1MB(1006)、可解码(1008)、分辨率用**短边/长边**判断(1002,技术方案 §8.3 修订版)。**门槛做成可配置**:读环境变量 `MIN_SHORT_SIDE`(默认 1080)、`MIN_LONG_SIDE`(默认 1920),不要硬编码(见 §14 分辨率门槛说明)。
|
||||
- 校验:大小 ≤1MB(1006)、可解码(1008)、分辨率用**短边/长边**判断(1002,技术方案 §8.3 修订版)。**门槛做成可配置**:读环境变量 `MIN_SHORT_SIDE`(默认 600)、`MIN_LONG_SIDE`(默认 800),不要硬编码(见 §14)。
|
||||
- `detector.detect` → None 则 1001。
|
||||
- `check_frontal_face` → False 则 1003。
|
||||
- `hair_segmenter` 取 mask(失败传 None,由 measure 内部兜底)。
|
||||
- `measure_face` → `create_annotated_image` → 保存到 `static/annotations/{uuid}.png` → 拼出 URL(用 `SAMPLE_IMAGE_URL` 同源的 base,即 `https://hair.xiangsilian.com/static/annotations/{uuid}.png`)。
|
||||
- `return ok(result.to_response())`,data 内含 `annotated_image_url`。
|
||||
2. 模型单例在模块加载时初始化(detector、segmenter),避免每请求重建。
|
||||
3. 异常兜底:未预期异常返回 `err(1007, ...)`(按文档错误码定义对齐)。
|
||||
- `measure_face` → `create_annotated_image`。
|
||||
- **⚠️ 拆分架构:返回 base64,不落盘不拼 URL**。`data["annotated_image_base64"] = base64(png)`,`return ok(data)`。落盘成 `annotated_image_url` 由网关完成(见架构文档 §9)。**worker 不写 static、不拼 hair.xiangsilian.com URL**。
|
||||
2. 模型单例在模块加载时初始化(detector、segmenter),避免每请求重建;BiSeNet `.to('cuda' if available)`。
|
||||
3. **鉴权中间件**:worker 增加校验 `X-Internal-Token` 的依赖/中间件,密码来自 worker 配置文件 `accept_passwords` 列表,不匹配返回 HTTP 401(架构文档 §7)。`/health` 不校验(供网关探测)。
|
||||
4. 异常兜底:未预期异常返回 `err(1007, ...)`。
|
||||
|
||||
**交付物**:更新后的 `app.py`
|
||||
|
||||
**验证方法**(本地起服务)
|
||||
> 默认门槛已是 600/800,`frontal.jpg` 直接放行,无需绕过校验。
|
||||
**验证方法**(本地起 worker)
|
||||
> 默认门槛已是 600/800,`frontal.jpg` 直接放行。worker 已加鉴权,需带 `X-Internal-Token` 头(值取 worker 配置的密码;本地测试可设一个测试密码)。
|
||||
```bash
|
||||
./venv/bin/uvicorn app:app --host 127.0.0.1 --port 8000 &
|
||||
F=http://127.0.0.1:8000/api/v1/face/measure
|
||||
# 0) 正常图 → code==0,data 含 four_courts/seven_eyes/annotated_image_url/hairline_source/head_pose
|
||||
curl -s -X POST $F -F image_file=@tests/fixtures/frontal.jpg | python -m json.tool
|
||||
# 1002) 低分辨率
|
||||
curl -s -X POST $F -F image_file=@tests/fixtures/lowres.png
|
||||
# 1001) 非人脸风景
|
||||
curl -s -X POST $F -F image_file=@tests/fixtures/landscape.jpg
|
||||
# 1008) 损坏文件
|
||||
curl -s -X POST $F -F image_file=@tests/fixtures/corrupt.bin
|
||||
# 1006) 超大图(>1MB,临时生成不入库)
|
||||
H="X-Internal-Token: testpass" # 与本地 worker 配置一致
|
||||
# 0) 正常图 → code==0,data 含 four_courts/seven_eyes/annotated_image_base64/hairline_source/head_pose
|
||||
curl -s -H "$H" -X POST $F -F image_file=@tests/fixtures/frontal.jpg | python -m json.tool
|
||||
# 1002/1001/1008/1006 同样带 -H "$H"
|
||||
curl -s -H "$H" -X POST $F -F image_file=@tests/fixtures/lowres.png # 1002
|
||||
curl -s -H "$H" -X POST $F -F image_file=@tests/fixtures/landscape.jpg # 1001
|
||||
curl -s -H "$H" -X POST $F -F image_file=@tests/fixtures/corrupt.bin # 1008
|
||||
head -c 1100000 /dev/urandom > /tmp/oversize.bin
|
||||
curl -s -X POST $F -F image_file=@/tmp/oversize.bin
|
||||
curl -s -H "$H" -X POST $F -F image_file=@/tmp/oversize.bin # 1006
|
||||
# 鉴权) 不带 token → HTTP 401
|
||||
curl -s -o /dev/null -w "%{http_code}\n" -X POST $F -F image_file=@tests/fixtures/frontal.jpg # 401
|
||||
# 1003) 侧脸:仓库无素材,用 mock 大 yaw 在单测中覆盖
|
||||
```
|
||||
- 访问返回的 `annotated_image_url` 对应的本地文件存在。
|
||||
- 正常用例 data 含 `annotated_image_base64`(**不是** URL——落盘改 URL 由网关做,见网关任务书);base64 解码后是合法 PNG。
|
||||
- 不带 token 返回 401;`/health` 不需要 token。
|
||||
- `/docs` Swagger 正常加载,该接口 schema 未破坏。
|
||||
|
||||
**完成标准**:0/1002/1001/1008/1006 五类用例返回正确 code(有现成夹具);1003 用 mock 覆盖;正常用例 data 结构与文档一致。
|
||||
@@ -280,37 +290,46 @@ curl -s -X POST $F -F image_file=@/tmp/oversize.bin
|
||||
|
||||
---
|
||||
|
||||
## 11. 阶段十:部署与冒烟
|
||||
## 11. 阶段十:worker 部署与冒烟(GPU 机 :28187)
|
||||
|
||||
**开发步骤**
|
||||
1. 确认 `hair.service`(systemd)无需改动即可加载新依赖;若新增 torch 导致启动变慢,记录冷启动耗时。
|
||||
2. 部署脚本补一步 `scripts/download_weights.sh`(生产机拉权重)。
|
||||
3. 重启服务,跑线上冒烟。
|
||||
1. 在 GPU 机部署完整 `app.py` + `face_analysis` + 模型权重(见 `OFFLINE_ASSETS.md`);torch 用 CUDA 版。
|
||||
2. resnet18 骨干放入 torch 缓存(`~/.cache/torch/hub/checkpoints/`),避免联网下载。
|
||||
3. worker 配置文件写好 `accept_passwords`;uvicorn 监听 `0.0.0.0:28187`;防火墙只放行网关 IP。
|
||||
4. systemd 管理 worker 进程;确认 `/health` 模型就绪后才返回 200。
|
||||
|
||||
**交付物**:更新的部署说明(写进 `CLAUDE.md` 或 `docs/`)
|
||||
**交付物**:worker 部署说明 + systemd unit + worker 配置文件示例
|
||||
|
||||
**验证方法**
|
||||
**验证方法**(在网关机或被放行的机器上)
|
||||
```bash
|
||||
sudo systemctl restart hair && sudo systemctl status hair
|
||||
curl -s -X POST https://hair.xiangsilian.com/api/v1/face/measure \
|
||||
TOK="X-Internal-Token: <worker配置的密码>"
|
||||
# 0) 正常请求(直连 worker)→ code==0,data 含 annotated_image_base64
|
||||
curl -s -H "$TOK" -X POST http://<worker>:28187/api/v1/face/measure \
|
||||
-F image_file=@tests/fixtures/frontal.jpg | python -m json.tool
|
||||
# 期望 code==0;annotated_image_url 可公网访问(curl -I 返回 200)
|
||||
journalctl -u hair -n 50 # 无 ERROR/Traceback
|
||||
# 鉴权) 不带 token → 401
|
||||
curl -s -o /dev/null -w "%{http_code}\n" -X POST http://<worker>:28187/api/v1/face/measure -F image_file=@tests/fixtures/frontal.jpg
|
||||
# 就绪) /health → 200
|
||||
curl -s http://<worker>:28187/health
|
||||
nvidia-smi # 确认推理时 GPU 被占用
|
||||
```
|
||||
|
||||
**完成标准**:线上接口返回真实数据,标注图可访问,日志无异常。
|
||||
**完成标准**:worker 直连返回真实数据(base64 图)、鉴权生效、`/health` 就绪、GPU 在用。
|
||||
|
||||
> 端到端(经网关的 HTTPS 冒烟)见 [`网关-开发任务书.md`](网关-开发任务书.md) 的验证。
|
||||
|
||||
---
|
||||
|
||||
## 12. 总交付清单(Definition of Done)
|
||||
## 12. 总交付清单(worker 侧 Definition of Done)
|
||||
|
||||
- [ ] `requirements.txt` / `.gitignore` / `scripts/download_weights.sh`
|
||||
- [ ] `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)
|
||||
- [ ] `tests/`:fixtures + 单元 + 集成 + 数值回归,`pytest` 全绿
|
||||
- [ ] 线上冒烟通过,标注图可公网访问
|
||||
- [ ] 文档:实测基线数值表 + 部署说明更新
|
||||
- [ ] 返回 data 含新增字段 `hairline_source`、`head_pose`,其余字段与 `docs/接口文档.md` 对齐
|
||||
- [ ] `app.py` 中 `/api/v1/face/measure` 真实实现(移除该接口 Mock),**返回 `annotated_image_base64`**
|
||||
- [ ] worker 鉴权中间件(`X-Internal-Token`)+ 配置文件 + `/health` 就绪态
|
||||
- [ ] `tests/`:fixtures + 单元 + 集成 + 数值回归 + §15 三层精度,`pytest` 全绿
|
||||
- [ ] worker(GPU 机 :28187)部署冒烟通过,GPU 在用
|
||||
- [ ] worker 返回 data 含 `annotated_image_base64`、`hairline_source`、`head_pose`,业务字段与 `docs/接口文档.md` 对齐
|
||||
|
||||
> 网关侧的交付清单在 [`网关-开发任务书.md`](网关-开发任务书.md)(另一台机器开发),本任务书不含。
|
||||
|
||||
---
|
||||
|
||||
@@ -476,4 +495,4 @@ def test_landmark_overlay():
|
||||
|
||||
---
|
||||
|
||||
> **任务书版本**: v1.3 | **创建日期**: 2026-06-13(v1.3:新增 §15 三层精度验证策略 + 合成真值生成器)| 配套技术方案 v2.0
|
||||
> **任务书版本**: v1.5 | **创建日期**: 2026-06-13(v1.5:拆出网关任务书到独立文档,本书聚焦 worker 侧)| 配套技术方案 v2.0 / 系统架构 v1.0 / 网关任务书 v1.0
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
|
||||
> 基于 MediaPipe Face Mesh(468 关键点)测量「眉心以下」+ 人脸解析分割(BiSeNet)获取「真实发际线/头顶」+ 人脸比例先验作为兜底
|
||||
|
||||
> 📌 **运行位置**:本文档描述的全部算法逻辑运行在 **高性能 worker(GPU 机)** 上,不在外网网关。系统已拆分为「外网网关 + worker」两层,详见 [`系统架构-网关与高性能后端.md`](系统架构-网关与高性能后端.md)。相对单机版有两处差异:
|
||||
> 1. **GPU 加速**:BiSeNet 改用 CUDA 推理(torch GPU 版),MediaPipe 仍 CPU。
|
||||
> 2. **标注图返回 base64**:worker **不落盘、不拼 URL**,把标注 PNG 以 `annotated_image_base64` 返回;落盘成 `annotated_image_url` 由网关完成(见架构文档 §9)。本文后续 §6/§8 的"保存到 static + 返回 URL"仅适用于单机版,拆分后改为返回 base64。
|
||||
> 3. **资源宽裕**:32G + GPU,无需单机版的 2核4G 并发限制;worker 自身并发=1 由网关保证。
|
||||
|
||||
---
|
||||
|
||||
## 1. 模型选型
|
||||
@@ -726,9 +731,13 @@ async def face_measure(image_file: UploadFile = File(...)):
|
||||
annotated = create_annotated_image(image, result)
|
||||
buf = BytesIO()
|
||||
annotated.save(buf, format="PNG")
|
||||
# ... 保存并返回 URL
|
||||
|
||||
return ok(result.to_response())
|
||||
# 6. 拆分架构下:返回 base64,由网关落盘改写成 annotated_image_url(见架构文档 §9)
|
||||
import base64
|
||||
data = result.to_response()
|
||||
data["annotated_image_base64"] = base64.b64encode(buf.getvalue()).decode()
|
||||
return ok(data)
|
||||
# —— 单机版(非拆分)才在此保存到 static/ 并返回 annotated_image_url ——
|
||||
```
|
||||
|
||||
---
|
||||
@@ -810,13 +819,14 @@ Pillow==11.0.0 # 标注图生成(PNG 透明图层)
|
||||
numpy==1.26.4 # ⚠️ 必须 <2,否则 mediapipe 0.10.x import 崩溃
|
||||
|
||||
# 方案 B:头发分割(BiSeNet face-parsing)
|
||||
torch==2.2.2 # CPU 版即可:pip install torch --index-url https://download.pytorch.org/whl/cpu
|
||||
# 拆分架构:worker 有 GPU → 用 CUDA 版 torch(按 worker 的 CUDA 版本选 whl)
|
||||
torch==2.2.2 # GPU(CUDA)版,例如 cu121:--index-url https://download.pytorch.org/whl/cu121
|
||||
torchvision==0.17.2
|
||||
```
|
||||
|
||||
> ⚠️ **numpy 锁版本**:mediapipe 0.10.x 对 numpy 2.x 支持不稳定,务必锁 `numpy<2`(已验证 1.26.4 可用)。先用此组合跑通,再考虑升级。
|
||||
>
|
||||
> ⚠️ **torch 体积**:CPU 版 torch ~200MB,是本接口最大的依赖。若服务器资源紧张或不想引入 torch,可改用 SegFormer-b0(onnxruntime 推理,体积更小),或先只上线方案 A、把方案 B 作为第二期。
|
||||
> ⚠️ **torch GPU/CPU**:拆分架构下 worker 有独立 GPU,用 **CUDA 版 torch**(按 worker 实际 CUDA 版本选对应 whl index,如 cu118/cu121),BiSeNet 推理走 GPU。单机/无 GPU 环境回退 CPU 版(`--index-url .../whl/cpu`,~200MB)。BiSeNet 加载时把 `.to('cuda' if torch.cuda.is_available() else 'cpu')`。
|
||||
|
||||
> MediaPipe 0.10.x 的经典 Solutions API (`mp.solutions.face_mesh`) 仍稳定可用。如需迁移到 Tasks API,后续可平滑升级。
|
||||
|
||||
|
||||
@@ -0,0 +1,259 @@
|
||||
# 系统架构:外网网关 + 高性能后端(worker)
|
||||
|
||||
> 本文档描述「旷视五接口」的部署架构拆分。**接口文档(对外契约)完全不变**——客户端看到的 URL、请求/响应结构、错误码 1001–1008 全部保持原样。本文只改变内部如何处理这些请求。
|
||||
|
||||
---
|
||||
|
||||
## 1. 背景与目标
|
||||
|
||||
外网服务器(`hair.xiangsilian.com`)性能不足以跑 MediaPipe + BiSeNet + 标注图生成等重逻辑。因此拆分为两层:
|
||||
|
||||
- **外网网关(gateway)**:保持 HTTPS 对外接口不变,**自身不跑算法**,只做反向代理、健康检查、负载分发、鉴权、标注图托管。资源占用极小,可继续跑在现有外网机。
|
||||
- **高性能后端(worker)**:独立机器,**GPU + 32G 内存**,跑真正的算法逻辑。通过 `http://hair.xiangsilian.com:28187` 这类 `host:port` 暴露。
|
||||
|
||||
**核心诉求**:
|
||||
1. 对外接口与文档零变化。
|
||||
2. 网关可配置**多个 worker URL**,周期探测可用性,只把请求发给健康的 worker。
|
||||
3. **每个 worker 并发 = 1**(一次处理一个请求);多 worker 即可并发,**总并发 = 健康 worker 数**。
|
||||
|
||||
---
|
||||
|
||||
## 2. 拓扑总览
|
||||
|
||||
```
|
||||
HTTPS (对外,接口文档不变)
|
||||
┌────────┐ :443 ┌───────────────────────────┐
|
||||
│ 客户端 │ ───────▶ │ 外网网关 gateway │
|
||||
└────────┘ │ hair.xiangsilian.com │
|
||||
│ - 反向代理 5 个接口 │
|
||||
│ - 健康检查 worker 池 │
|
||||
│ - 空闲 worker 派发(并发=worker数)│
|
||||
│ - 共享密码鉴权 │
|
||||
│ - 标注图 base64→落盘→URL │
|
||||
│ - 无可用后端→1007 │
|
||||
└───────┬───────────┬─────────┘
|
||||
HTTP + 密码头 │ │
|
||||
┌───────────────────────┘ └─────────────┐
|
||||
▼ ▼
|
||||
┌──────────────────┐ ┌──────────────────┐
|
||||
│ worker #1 │ ...(可配置多个)... │ worker #N │
|
||||
│ :28187 GPU/32G │ │ :xxxxx GPU/32G │
|
||||
│ 跑完整 app.py │ │ 跑完整 app.py │
|
||||
│ + face_analysis │ │ + face_analysis │
|
||||
│ 并发=1 │ │ 并发=1 │
|
||||
└──────────────────┘ └──────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 职责划分
|
||||
|
||||
| 能力 | 网关 gateway | worker |
|
||||
|------|:---:|:---:|
|
||||
| 对外 HTTPS、接口契约 | ✅ | ✗ |
|
||||
| 反向代理 5 个接口 | ✅ | ✗ |
|
||||
| 算法(MediaPipe/BiSeNet/测量/标注图) | ✗ | ✅ |
|
||||
| 入参校验(大小/格式/分辨率/人脸) | ✗(透传) | ✅ |
|
||||
| worker 健康检查 + 池管理 | ✅ | 提供 `/health` |
|
||||
| 负载分发(挑空闲 worker) | ✅ | ✗ |
|
||||
| 鉴权(共享密码) | 发送密码 | 校验密码 |
|
||||
| 标注 PNG 落盘 + 对外 URL | ✅ | 返回 base64 |
|
||||
| `/static/*` 静态托管 | ✅ | ✗ |
|
||||
| GPU | 不需要 | ✅ |
|
||||
|
||||
> **原则**:所有业务逻辑只在 worker 实现一份(worker 跑的就是完整 `app.py` + `face_analysis`)。网关是无状态的薄层,除了"健康池 + worker 忙闲状态"外不持有业务状态。
|
||||
|
||||
---
|
||||
|
||||
## 4. 网关配置文件
|
||||
|
||||
后端 URL 列表、共享密码、各项参数集中在一个配置文件,运维手动维护(密码定期轮换)。
|
||||
|
||||
`gateway/config.json`(示例):
|
||||
```json
|
||||
{
|
||||
"workers": [
|
||||
"http://hair.xiangsilian.com:28187",
|
||||
"http://10.0.0.12:28187"
|
||||
],
|
||||
"shared_password": "REPLACE_ME_ROTATE_PERIODICALLY",
|
||||
"accept_passwords": ["REPLACE_ME_ROTATE_PERIODICALLY"],
|
||||
"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
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
字段说明:
|
||||
- `workers`:worker 基址列表,手动增删。**增加一个就多一路并发**。
|
||||
- `shared_password`:网关调用 worker 时发送的密码。
|
||||
- `accept_passwords`:worker 端用(见 §7)——允许的密码列表,**轮换期可同时放新旧两个**,实现不停机改密码。网关侧也可放在 worker 的独立配置里。
|
||||
- `health_check`:探测周期、超时、连续失败几次判定下线、连续成功几次判定上线。
|
||||
- `dispatch.per_worker_concurrency`:**固定为 1**(当前约束)。
|
||||
- `dispatch.queue_wait_seconds`:所有 worker 都忙时请求最多排队多久,超时返回 1007。
|
||||
- `request_timeout_seconds`:单次转发到 worker 的超时。
|
||||
- `retry_on_failure` / `max_retries`:worker 转发失败时是否换一个 worker 重试。
|
||||
|
||||
> 配置变更后网关需 reload(可做成监听文件变更热加载,或重启网关进程)。
|
||||
|
||||
---
|
||||
|
||||
## 5. 健康检查
|
||||
|
||||
网关后台**周期轮询**每个 worker 的 `/health`(已存在于 `app.py`,排除在 OpenAPI 之外):
|
||||
|
||||
- 间隔 `interval_seconds`,每次超时 `timeout_seconds`。
|
||||
- 连续失败 `unhealthy_threshold` 次 → 标记**下线**,停止派发。
|
||||
- 重新连续成功 `healthy_threshold` 次 → 标记**上线**,恢复派发。
|
||||
- 健康池 = 当前在线的 worker 集合。
|
||||
|
||||
**主动检查 + 被动剔除**双保险:除周期探测外,转发请求时若 worker 连接失败/超时,立即把它标记为不健康并(按配置)换一个 worker 重试。
|
||||
|
||||
> worker 的 `/health` 建议返回 **模型就绪状态**:只有 MediaPipe / BiSeNet 权重都加载完成才返回 200,否则返回 503——避免请求被派发到尚未热好的 worker。
|
||||
|
||||
---
|
||||
|
||||
## 6. 负载分发与并发
|
||||
|
||||
**每个 worker 并发 = 1**,所以分发逻辑很简单:
|
||||
|
||||
1. 网关维护每个健康 worker 的**忙/闲**状态。
|
||||
2. 新请求到来:从健康池里选**一个空闲 worker**,标记为忙,转发;收到响应(或失败)后标记为闲。
|
||||
3. 若当前**无空闲 worker**(全忙):请求进入**队列等待**,直到有 worker 空闲或等待超过 `queue_wait_seconds`(超时返回 1007)。
|
||||
4. 若**健康池为空**(无可用后端):直接返回 1007。
|
||||
|
||||
- **总并发能力 = 健康 worker 数量**。加机器即扩并发。
|
||||
- 选空闲 worker 的策略:任意(如先到先得 / 轮转空闲),并发=1 下无需最少连接数算法。
|
||||
|
||||
---
|
||||
|
||||
## 7. 鉴权(共享密码)
|
||||
|
||||
`:28187` 在公网可直接访问,必须鉴权防止他人直接打 worker。
|
||||
|
||||
- **网关 → worker**:每个转发请求带密码头,如 `X-Internal-Token: <shared_password>`。
|
||||
- **worker 校验**:worker 侧配置文件持有 `accept_passwords` 列表,收到请求校验 `X-Internal-Token` 是否在列表内;不匹配返回 HTTP 401,**不进入业务逻辑**。
|
||||
- **轮换**:运维改密码时,先把新密码加进 worker 的 `accept_passwords`(此时新旧都接受)→ 再把网关 `shared_password` 切到新值 → 确认无旧密码流量后从 worker 移除旧密码。全程不停机。
|
||||
- 密码**仅存配置文件**,不写日志、不进 git(配置文件加入 `.gitignore`,仓库只放 `config.example.json`)。
|
||||
|
||||
> 建议叠加防火墙:worker 防火墙只放行网关来源 IP(纵深防御)。密码是应用层兜底。
|
||||
|
||||
---
|
||||
|
||||
## 8. 单次请求处理流程
|
||||
|
||||
```
|
||||
客户端 ──(HTTPS, multipart/json, 接口文档原样)──▶ 网关
|
||||
│
|
||||
├─ 1. 选一个空闲健康 worker(无则排队/1007)
|
||||
├─ 2. 原样转发请求体 + 加 X-Internal-Token 头
|
||||
│ ──(HTTP)──▶ worker
|
||||
│ ├─ 校验密码(失败→401,网关视为该 worker 异常)
|
||||
│ ├─ 跑完整业务(校验/MediaPipe/BiSeNet/测量/标注图)
|
||||
│ └─ 返回标准信封;图片字段以 base64 形式(见 §9)
|
||||
├─ 3. 收到 worker 响应,标记该 worker 空闲
|
||||
├─ 4. 若响应含 base64 图片 → 落盘到网关 /static/annotations/{uuid}.png
|
||||
│ → 把字段改写成对外 URL(见 §9)
|
||||
└─ 5. 把最终(符合接口文档的)响应返回客户端
|
||||
```
|
||||
|
||||
- 网关**不解析也不校验**业务入参,原样透传(worker 负责全部校验与错误码)。worker 返回的 1001–1008 由网关**透传**给客户端。
|
||||
- 网关只在「无后端/排队超时」时**自行**返回 1007。
|
||||
|
||||
---
|
||||
|
||||
## 9. 标注图处理(base64 → 落盘 → URL 改写)★关键
|
||||
|
||||
接口文档里多个接口返回图片 URL(接口1 标注图、接口3 标记图、接口5 发际线图)。拆分后:
|
||||
|
||||
1. **worker 不落盘、不拼 URL**,而是把生成的 PNG 以 **base64** 放进响应的约定字段返回给网关。
|
||||
- 约定:worker 用 `*_base64` 字段承载图片,例如接口1 返回 `annotated_image_base64` 而非 `annotated_image_url`。
|
||||
2. **网关收到后**:对每个 `*_base64` 字段——
|
||||
- 解码 → 保存到网关本地 `static/annotations/{uuid}.png`;
|
||||
- 删除该 base64 字段,新增对应的 `*_url` 字段,值为 `https://hair.xiangsilian.com/static/annotations/{uuid}.png`;
|
||||
3. 客户端最终看到的字段名/URL **与接口文档完全一致**(如 `annotated_image_url`)。
|
||||
|
||||
> 网关持有一份「接口 → 图片字段」映射表(接口1: `annotated_image` ↔ 接口5: `hairline_image` 等),按表把内部 `*_base64` 转成对外 `*_url`。映射表以接口文档为准。
|
||||
>
|
||||
> 代价:内部 HTTP 多传一份 base64(图片放大约 1.33×)。worker↔网关若跨网络,单图 ~100KB–1MB 量级,可接受。
|
||||
>
|
||||
> 静态文件清理:网关 `static/annotations/` 会持续增长,需加定期清理(按时间或容量,定时任务),与拆分前同样的问题,由网关侧负责。
|
||||
|
||||
---
|
||||
|
||||
## 10. 错误处理
|
||||
|
||||
| 场景 | 返回 | 由谁 |
|
||||
|------|------|------|
|
||||
| 业务错误(无人脸/分辨率/非正面/超大/格式…) | 1001–1008(原样) | worker 产生,网关透传 |
|
||||
| **无可用 worker / 全忙排队超时** | **1007 系统错误** | 网关 |
|
||||
| worker 转发失败(连接/超时/5xx/401) | 按配置换 worker 重试;重试耗尽 → 1007 | 网关 |
|
||||
|
||||
- 复用 **1007**(系统错误)表示「基础设施层不可用」,**不新增错误码**,接口文档不动。
|
||||
- 局限:调用方无法从错误码区分"算法失败"与"后端全挂"(都是 1007)。如需区分,可在 `message` 文案上体现(如"后端服务暂不可用,请稍后重试"),但 `code` 保持 1007。
|
||||
|
||||
---
|
||||
|
||||
## 11. worker 侧相对单机方案的变化
|
||||
|
||||
worker 跑的就是「接口1 技术实现方案」里描述的完整逻辑,但有几处因拆分/硬件而变:
|
||||
|
||||
1. **GPU 加速**:worker 有独立 GPU,BiSeNet 改用 **CUDA** 推理(torch GPU 版),比 CPU 快很多;MediaPipe 仍 CPU(Python solutions API 仅 CPU)。
|
||||
2. **不落盘、返回 base64**:标注图相关接口的 handler 改为返回 `*_base64`,不再保存到本地 `/static`、不拼 URL(改由网关做,见 §9)。
|
||||
3. **`/health` 反映就绪**:模型加载完才返回 200。
|
||||
4. **鉴权中间件**:worker 增加一个校验 `X-Internal-Token` 的中间件/依赖(见 §7)。
|
||||
5. **资源宽裕**:32G 内存 + GPU,无需单机方案里 2核4G 的并发限制与降级开关;但 worker 自身仍是**并发=1**(由网关保证,不向 worker 发并发请求;worker 可不做内部并发控制,但建议 uvicorn 单 worker 进程以省显存)。
|
||||
|
||||
> 单机方案文档(接口1 技术实现方案)描述的「方案A/B、虹膜标定、标注图、误差验证」全部不变,只是运行位置从外网机挪到 GPU worker,并启用 GPU。
|
||||
|
||||
---
|
||||
|
||||
## 12. 部署
|
||||
|
||||
### 网关(外网机,hair.xiangsilian.com)
|
||||
- 新增 gateway 应用(轻量 FastAPI/asgi 代理)。
|
||||
- nginx 把 443 → 网关进程;网关再转发到 worker 池。
|
||||
- 托管 `/static/*`(标注图落盘目录)。
|
||||
- 配置文件 `gateway/config.json`(含 worker 列表 + 密码)。
|
||||
- systemd 管理网关进程。
|
||||
|
||||
### worker(GPU 机,:28187)
|
||||
- 部署完整 `app.py` + `face_analysis` + 模型权重(见 `OFFLINE_ASSETS.md`)。
|
||||
- torch 用 **GPU 版**(CUDA),其余依赖同单机方案。
|
||||
- uvicorn 监听 `0.0.0.0:28187`(仅经防火墙放行网关)。
|
||||
- 配置文件持有 `accept_passwords`。
|
||||
- systemd 管理 worker 进程;`/health` 供网关探测。
|
||||
|
||||
---
|
||||
|
||||
## 13. 安全注意
|
||||
|
||||
- `:28187` **公网可达 + HTTP 明文**:密码头会明文走公网。**强烈建议**给 worker 也套一层 TLS(worker 前置 nginx 终止 HTTPS,或网关↔worker 走内网/VPN/IP 白名单),否则密码可被中间人嗅探。
|
||||
- 若短期内只能 HTTP,务必靠**防火墙 IP 白名单**把 worker 限制为只接受网关来源,密码作为应用层兜底。
|
||||
- 密码不入 git、不写日志。
|
||||
- 网关对转发的请求体大小设上限(如 ≤2MB),防止被超大 body 拖垮。
|
||||
|
||||
---
|
||||
|
||||
## 14. 待确认 / 后续
|
||||
|
||||
1. **worker↔网关传输是否加 TLS**:当前 §13 标为强烈建议。若运维能给 worker 配 HTTPS 或限定内网,安全性更好。请确认部署条件。
|
||||
2. **worker host:port 形态**:`hair.xiangsilian.com:28187` 是端口转发到 GPU 机,还是 GPU 机直接持有该域名?影响防火墙与 TLS 方案。
|
||||
3. **多 worker 的物理分布**:是否都在同一内网?若跨公网,base64 图片传输与密码明文风险都需重新评估。
|
||||
4. **静态图清理策略**:网关 `static/annotations/` 的保留时长 / 清理触发条件。
|
||||
|
||||
---
|
||||
|
||||
> **文档版本**: v1.0 | **创建日期**: 2026-06-14 | 配套:接口1 技术方案 v2.0 / 开发任务书 / OFFLINE_ASSETS.md
|
||||
> **关键约束**: 接口文档不变;每 worker 并发=1;无后端→1007;标注图 base64→网关落盘→URL
|
||||
@@ -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 返回的 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`。
|
||||
- 维护一张「接口路径 → 图片字段名」映射表(接口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 → 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. **图片字段未实现的接口**:接口 2/4/5 worker 暂为 mock,网关先按"无图片字段或透传"处理,待 worker 实现对应图片后再补映射。
|
||||
|
||||
---
|
||||
|
||||
> **文档版本**: v1.0 | **创建日期**: 2026-06-14 | 配套:系统架构 v1.0
|
||||
> **关键约束**: 接口文档不变;不跑算法;每 worker 并发=1;无后端→1007;base64→落盘→URL
|
||||
Reference in New Issue
Block a user