diff --git a/docs/README.md b/docs/README.md index 49066b0..83b8160 100644 --- a/docs/README.md +++ b/docs/README.md @@ -7,7 +7,7 @@ |------|------| | [实现说明.md](实现说明.md) | **实现总览**:架构、五个接口怎么实现、base64→URL 映射、错误码、部署/环境要点。先读这份。 | | [接口文档.md](接口文档.md) | 对外 API 契约(字段 / 错误码)。字段命名的唯一权威。 | -| [42/52/72 网关鉴权](接口42-52-72网关鉴权.md) | 登录、过期、刷新、令牌签名配置与测试页。 | +| [客户端网关鉴权](接口42-52-72网关鉴权.md) | 登录、过期、刷新、令牌签名配置与测试页。 | | [接口52网关接入.md](接口52网关接入.md) | 52号请求透传、图片URL与空字段约定、公开测试页及网关部署步骤。 | | [旷视具体需求.md](旷视具体需求.md) | 原始需求。 | | [../OFFLINE_ASSETS.md](../OFFLINE_ASSETS.md) | worker 离线模型权重/字体清单(内网部署)。 | diff --git a/docs/接口42-52-72网关鉴权.md b/docs/接口42-52-72网关鉴权.md index bde9a68..a23a66e 100644 --- a/docs/接口42-52-72网关鉴权.md +++ b/docs/接口42-52-72网关鉴权.md @@ -1,13 +1,15 @@ -# 22、42、52、72 号接口网关鉴权 +# 客户端网关鉴权 | 接口 | 方法与路径 | 执行位置 | |---|---|---| +| 11 | `POST /api/v1/face/measure-11` | 网关转发至 worker 的接口 1,沿用四庭七眼测量及标注图 | | 22 | `POST /api/v1/hair/grow-22` | 网关转发至 Worker,发际线预览和生发图转成 URL | | 42 | `POST /api/v1/face/features-42` | 网关按接口 52 的规则扶正裁切,再用接口 4 的分析结果和 Ft/Zy 判定生成特征 | | 52 | `POST /api/v1/hairline/generate-52` | 网关转发至 worker | +| 61 | `POST /api/v1/face/measure-61` | 网关转发至 worker 的接口 6,沿用去顶庭的测量及标注图 | | 72 | `POST /api/v1/face/b-end-points` | 网关转发至 worker,底图与标注层转成 URL | -客户端调用这些接口时,在原请求上添加 `Authorization: Bearer `。缺少、过期、签名无效或误用刷新令牌时,返回 HTTP 401 和业务码 `1009`。网关验证通过后,22、52、72 通过现有 Worker 池转发,内部转发头由网关提供。 +客户端调用这些接口时,在原请求上添加 `Authorization: Bearer `。缺少、过期、签名无效或误用刷新令牌时,返回 HTTP 401 和业务码 `1009`。网关验证通过后,11、22、52、61、72 通过现有 worker 池转发。11 和 61 沿用 1 和 6 的图片参数、响应字段及标注图 URL,Bearer 令牌只在网关校验。 ## 登录 @@ -35,4 +37,4 @@ 部署接口 22 前,确认 Worker 池的主备节点均包含接口 22(例如 `ubuntu3090_debug` 上的 `b84a506` 或后续版本)。Worker 启用 IP 白名单时,配置网关直连地址。 -网关测试页为 `/static/test_interface22.html`、`/static/test_interface42.html`、`/static/test_interface52.html` 和 `/static/test_interface72.html`。页面可登录、手动刷新令牌并提交图片,访问令牌到期时自动刷新。「测试鉴权」调用 `GET /api/v1/auth/check`,验证无令牌请求返回 HTTP 401、有效令牌请求返回 HTTP 200。 +网关测试页为 `/static/test_interface11_auth.html`、`/static/test_interface22.html`、`/static/test_interface42.html`、`/static/test_interface52.html`、`/static/test_interface61.html` 和 `/static/test_interface72.html`。页面可登录、手动刷新令牌并提交图片,访问令牌到期时自动刷新。「测试鉴权」调用 `GET /api/v1/auth/check`,验证无令牌请求返回 HTTP 401、有效令牌请求返回 HTTP 200。 diff --git a/docs/接口文档.md b/docs/接口文档.md index 79a6909..e2dd0df 100644 --- a/docs/接口文档.md +++ b/docs/接口文档.md @@ -15,7 +15,9 @@ | 接口 | 方法 | 路径 | |------|------|------| | 1 四庭七眼测量 | POST | `/api/v1/face/measure` | +| 11 四庭七眼测量(鉴权) | POST | `/api/v1/face/measure-11` | | 6 四庭七眼测量 v2 | POST | `/api/v1/face/measure-v2` | +| 61 四庭七眼测量 v2(鉴权) | POST | `/api/v1/face/measure-61` | | 2 C 端生发 | POST | `/api/v1/hair/grow` | | 22 生发密度与发际线高度 | POST | `/api/v1/hair/grow-22` | | 3 B 端生发 | POST | `/api/v1/hair/grow-b` | @@ -25,6 +27,8 @@ | 5 发际线 PNG 生成 | POST | `/api/v1/hairline/generate` | | 52 发际线新规则 | POST | `/api/v1/hairline/generate-52` | +接口 11 和 61 分别沿用接口 1 和 6 的图片参数及响应结构,需在请求头添加 `Authorization: Bearer `。网关验证令牌后转发给 GPU worker。参见[登录与刷新令牌](接口42-52-72网关鉴权.md)。 + --- ## 通用约定 @@ -90,7 +94,7 @@ | 1006 | 文件超出大小限制 | 单文件超过 1 MB | | 1007 | 图片参数错误 | file / url / base64 未传,或同时传了多个(三者严格互斥) | | 1008 | 图片格式不支持 | 非 JPG / PNG | -| 1009 | 未授权 | worker内部`X-Internal-Token`错误;网关42/52/72号客户端 Bearer 令牌缺失、无效或过期。HTTP401 | +| 1009 | 未授权 | HTTP 401。网关受保护接口的 Bearer 令牌缺失、无效或过期。旧版 worker 的内部 `X-Internal-Token` 校验失败也使用此错误码。 | --- diff --git a/gateway/app.py b/gateway/app.py index 27d875f..b0ecdd7 100644 --- a/gateway/app.py +++ b/gateway/app.py @@ -249,6 +249,7 @@ async def index(): "integration_guide": "/static/integration.html", "test_pages": { "if1_measure": "/static/test_interface1.html", + "if11_measure": "/static/test_interface11_auth.html", "if2_hair_grow": "/static/test_interface2.html", "if22_hair_grow": "/static/test_interface22.html", "if3_hair_grow_b": "/static/test_interface3.html", @@ -258,6 +259,7 @@ async def index(): "if5_hairline": "/static/test_interface5.html", "if52_hairline": "/static/test_interface52.html", "if6_measure_v2": "/static/test_interface6.html", + "if61_measure_v2": "/static/test_interface61.html", "if7_hair_grow_v2": "/static/test_interface7.html", "if9_head_mask": "/static/test_interface9.html", "if10_head_band": "/static/test_interface10.html", @@ -570,12 +572,28 @@ async def face_measure(request: Request): return await _proxy(request, "/api/v1/face/measure") +@app.post("/api/v1/face/measure-11", summary="接口11:四庭七眼测量标注(鉴权)", tags=["人脸分析"]) +async def face_measure_11(request: Request, auth_error=Depends(client_auth_error)): + """通过 Bearer 鉴权后,使用接口 1 的图片参数和响应格式。""" + if auth_error is not None: + return auth_error + return await face_measure(request) + + @app.post("/api/v1/face/measure-v2", tags=["人脸分析"]) async def face_measure_v2(request: Request): """接口6:四庭七眼测量标注 v2(去顶庭 + 去头部端线)""" return await _proxy(request, "/api/v1/face/measure-v2") +@app.post("/api/v1/face/measure-61", summary="接口61:四庭七眼测量标注 v2(鉴权)", tags=["人脸分析"]) +async def face_measure_61(request: Request, auth_error=Depends(client_auth_error)): + """通过 Bearer 鉴权后,使用接口 6 的图片参数和响应格式。""" + if auth_error is not None: + return auth_error + return await face_measure_v2(request) + + @app.post("/api/v1/hair/grow", tags=["生发"]) async def hair_grow(request: Request): """接口2:C端生发""" diff --git a/gateway/logging_middleware.py b/gateway/logging_middleware.py index 62b9bf1..95d52f1 100644 --- a/gateway/logging_middleware.py +++ b/gateway/logging_middleware.py @@ -436,6 +436,8 @@ PATH_INTERFACE_MAP: Dict[str, Dict[str, Any]] = { "/api/v1/hair/grow-v2": {"num": 7, "name": "C端生发 v2"}, "/api/v1/head/mask": {"num": 9, "name": "头发遮罩生成"}, "/api/v1/head/band": {"num": 10, "name": "头部外缘膨胀带遮罩"}, + "/api/v1/face/measure-11": {"num": 11, "name": "四庭七眼测量标注(鉴权)"}, + "/api/v1/face/measure-61": {"num": 61, "name": "四庭七眼测量标注 v2(鉴权)"}, } # 会按「勾选发型数量」串行多次调用 ComfyUI 的接口(耗时 ≈ 固定开销 + 单次换发型耗时 × 发型数) diff --git a/static/face_measure_auth_test.css b/static/face_measure_auth_test.css new file mode 100644 index 0000000..2e357f2 --- /dev/null +++ b/static/face_measure_auth_test.css @@ -0,0 +1,63 @@ +* { box-sizing: border-box; margin: 0; padding: 0; } +body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: #f5f5f5; color: #333; } +.container { max-width: 1100px; margin: 0 auto; padding: 24px; } +h1 { font-size: 22px; margin-bottom: 8px; } +.subtitle { color: #888; font-size: 13px; margin-bottom: 24px; } + +/* 上传区 */ +.upload-card { background: #fff; border-radius: 12px; padding: 24px; box-shadow: 0 1px 4px rgba(0,0,0,.06); margin-bottom: 24px; } +.upload-row { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; } +.file-input { flex: 1; min-width: 200px; } +.file-input input[type=file] { width: 100%; padding: 8px; border: 2px dashed #ddd; border-radius: 8px; cursor: pointer; } +.btn { padding: 10px 28px; border: none; border-radius: 8px; font-size: 15px; cursor: pointer; font-weight: 600; transition: .2s; } +.btn-primary { background: #2563eb; color: #fff; } +.btn-primary:hover { background: #1d4ed8; } +.btn-primary:disabled { background: #93c5fd; cursor: not-allowed; } +.btn-sm { padding: 6px 14px; font-size: 13px; } +.btn-outline { background: #fff; border: 1px solid #d1d5db; color: #374151; } +.btn-outline:hover { background: #f9fafb; } +.upload-hint { font-size: 12px; color: #9ca3af; margin-top: 8px; } + +/* 状态提示 */ +.status { padding: 10px 16px; border-radius: 8px; font-size: 14px; margin-bottom: 16px; display: none; } +.status.info { background: #dbeafe; color: #1e40af; display: block; } +.status.error { background: #fee2e2; color: #991b1b; display: block; } +.status.success { background: #d1fae5; color: #065f46; display: block; } + +/* 结果双栏 */ +.results { display: flex; gap: 24px; } +.results > div { flex: 1; min-width: 0; background: #fff; border-radius: 12px; box-shadow: 0 1px 4px rgba(0,0,0,.06); overflow: hidden; } +.results .panel-header { font-weight: 700; font-size: 14px; padding: 14px 18px; border-bottom: 1px solid #f0f0f0; background: #fafafa; display: flex; justify-content: space-between; align-items: center; } + +/* JSON 面板 */ +.json-panel { max-height: 600px; overflow: auto; } +.json-content { padding: 16px 18px; font-family: "SF Mono", "Fira Code", monospace; font-size: 12.5px; line-height: 1.6; white-space: pre-wrap; word-break: break-all; } + +/* 图片面板 — 叠加显示 */ +.img-panel { display: flex; align-items: center; justify-content: center; min-height: 300px; background: #222; position: relative; } +.img-stack:not([hidden]) { max-width: 100%; position: relative; display: inline-block; line-height: 0; } +.img-stack img:not([hidden]) { display: block; max-width: min(100%, 520px); max-height: 620px; object-fit: contain; } +.img-stack .layer-anno { position: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: contain; } +.img-stack .layer-base[hidden] + .layer-anno { position: static; width: auto; height: auto; } +.img-panel .placeholder { color: #9ca3af; font-size: 14px; } + +/* 叠加控制 */ +.anno-controls { display: flex; gap: 12px; align-items: center; font-size: 12px; color: #9ca3af; } +.anno-controls label { display: flex; align-items: center; gap: 4px; cursor: pointer; } +.anno-controls input[type=range] { width: 60px; } + +/* 指标卡片 */ +.metrics { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 12px; margin-top: 16px; } +.metric { background: #f9fafb; border-radius: 8px; padding: 12px 14px; } +.metric .label { font-size: 11px; color: #9ca3af; text-transform: uppercase; letter-spacing: .5px; } +.metric .value { font-size: 18px; font-weight: 700; color: #111827; margin-top: 2px; } +.metric .unit { font-size: 12px; color: #6b7280; font-weight: 400; } + +@media (max-width: 768px) { .results { flex-direction: column; } } +.hidden { display: none !important; } +.auth-fields { display: flex; flex-wrap: wrap; gap: 12px; align-items: end; } +.auth-fields label { display: grid; gap: 4px; font-size: 13px; } +.auth-fields input { border: 1px solid #d1d5db; border-radius: 8px; padding: 8px 10px; } +.auth-fields button:disabled { opacity: .5; cursor: not-allowed; } +.diff-list { color: #6b7280; font-size: 13px; line-height: 1.7; margin: -14px 0 24px 18px; } +#statusBar { margin: 12px 0 0; } diff --git a/static/face_measure_auth_test.js b/static/face_measure_auth_test.js new file mode 100644 index 0000000..b348b3c --- /dev/null +++ b/static/face_measure_auth_test.js @@ -0,0 +1,156 @@ +(() => { + const $ = (id) => document.getElementById(id); + const form = $("measure-test"); + const { endpoint, totalHeightLabel } = form.dataset; + const fileInput = $("imageFile"); + const submitButton = $("submitBtn"); + const clearButton = $("clearBtn"); + const copyButton = $("copyBtn"); + const metrics = $("metricsBar"); + const results = $("resultsArea"); + const jsonContent = $("jsonContent"); + const placeholder = $("imagePlaceholder"); + const imageStack = $("imageStack"); + const baseImage = $("baseImage"); + const annotationImage = $("annotationImage"); + let originalImageUrl; + + function setStatus(text, type = "") { + const status = $("statusBar"); + status.textContent = text; + status.className = type ? "status " + type : "status"; + } + + function resetResults() { + results.classList.add("hidden"); + metrics.classList.add("hidden"); + jsonContent.textContent = ""; + imageStack.hidden = true; + placeholder.hidden = false; + placeholder.textContent = "提交后显示叠加效果"; + baseImage.removeAttribute("src"); + annotationImage.removeAttribute("src"); + if (originalImageUrl) { + URL.revokeObjectURL(originalImageUrl); + originalImageUrl = undefined; + } + } + + function updateOverlay() { + baseImage.hidden = !$("showBase").checked; + annotationImage.hidden = !$("showAnno").checked; + annotationImage.style.opacity = $("annoOpacity").value / 100; + } + + function renderMetrics(data) { + const courts = data.four_courts || {}; + const eyes = data.seven_eyes || {}; + const items = [[totalHeightLabel, data.face_total_height_cm]]; + if (form.hasAttribute("data-include-top-court")) items.push(["顶庭", courts.top_court_cm]); + items.push( + ["上庭", courts.upper_court_cm], + ["中庭", courts.middle_court_cm], + ["下庭", courts.lower_court_cm], + ["单眼宽度", eyes.eye_width_cm], + ["脸宽", eyes.face_width_cm], + ["两眼间距", eyes.inter_eye_distance_cm], + ); + + metrics.replaceChildren(); + for (const [label, value] of items) { + const card = document.createElement("div"); + card.className = "metric"; + card.innerHTML = '
cm
'; + card.querySelector(".label").textContent = label; + card.querySelector(".value span").textContent = value ?? "—"; + metrics.append(card); + } + metrics.classList.remove("hidden"); + } + + form.addEventListener("submit", async (event) => { + event.preventDefault(); + const selected = fileInput.files[0]; + if (!selected) { + setStatus("请先选择一张图片", "error"); + return; + } + + fileInput.disabled = submitButton.disabled = clearButton.disabled = true; + submitButton.textContent = "⏳ 请求中..."; + resetResults(); + setStatus("正在请求 " + endpoint, "info"); + + try { + const file = await window.downscaleImageFile(selected); + const body = new FormData(); + body.append("image_file", file); + const start = performance.now(); + const response = await window.hairClientFetch(endpoint, { method: "POST", body }); + if (!response.headers.get("Content-Type")?.includes("application/json")) { + throw new Error("服务返回 HTTP " + response.status + ",响应格式异常"); + } + const result = await response.json(); + const elapsed = ((performance.now() - start) / 1000).toFixed(2); + jsonContent.textContent = JSON.stringify(result, null, 2); + results.classList.remove("hidden"); + + if (!response.ok || result.code !== 0) { + setStatus(`请求失败 (${elapsed}s) · HTTP ${response.status} · code ${result.code} · ${result.message}`, "error"); + placeholder.textContent = "请求失败,无标注图"; + return; + } + + const data = result.data; + const annotationSrc = window.resolveImgSrc(data.annotated_image_url, data.annotated_image_base64, "image/png"); + if (annotationSrc) { + originalImageUrl = URL.createObjectURL(file); + baseImage.src = originalImageUrl; + annotationImage.src = annotationSrc; + updateOverlay(); + imageStack.hidden = false; + placeholder.hidden = true; + } else { + placeholder.textContent = "服务未返回标注图"; + } + renderMetrics(data); + setStatus(`请求成功 (${elapsed}s) · request_id ${result.request_id}`, "success"); + } catch (error) { + setStatus(error.message, "error"); + } finally { + fileInput.disabled = submitButton.disabled = clearButton.disabled = false; + submitButton.textContent = "🚀 提交测试"; + } + }); + + $("showBase").addEventListener("change", updateOverlay); + $("showAnno").addEventListener("change", updateOverlay); + $("annoOpacity").addEventListener("input", updateOverlay); + clearButton.addEventListener("click", () => { + resetResults(); + fileInput.value = ""; + setStatus(""); + }); + + copyButton.addEventListener("click", async () => { + try { + await navigator.clipboard.writeText(jsonContent.textContent); + copyButton.textContent = "✅ 已复制"; + setTimeout(() => { copyButton.textContent = "📋 复制"; }, 1500); + } catch { + setStatus("复制失败,请手动复制 JSON", "error"); + } + }); + + fileInput.addEventListener("change", () => { + if (fileInput.files.length) setStatus("已选择 " + fileInput.files[0].name, "info"); + }); + fileInput.addEventListener("dragover", (event) => event.preventDefault()); + fileInput.addEventListener("drop", (event) => { + event.preventDefault(); + if (event.dataTransfer.files.length) { + fileInput.files = event.dataTransfer.files; + fileInput.dispatchEvent(new Event("change")); + } + }); +})(); diff --git a/static/integration.html b/static/integration.html index 43fad06..02f8f08 100644 --- a/static/integration.html +++ b/static/integration.html @@ -55,8 +55,9 @@
通用约定 - 22/42/52/72 鉴权 + 客户端鉴权 接口1 + 接口11 接口2 接口22 接口3 @@ -65,6 +66,7 @@ 接口5 接口52 接口6 + 接口61 接口72 错误码 在线测试 @@ -79,7 +81,7 @@ 请求方式业务接口使用 POST。验证令牌使用 GET。 编码UTF-8 Content-Type传图使用 multipart/form-data,登录和刷新使用 application/json - 客户端鉴权22、42、52、72 使用 Bearer 访问令牌。 + 客户端鉴权11、22、42、52、61、72 使用 Bearer 访问令牌。 图片参数三选一:image_file(文件上传)/ image_url(URL)/ image_base64(base64 + 前缀) 图片格式JPG / PNG,单人正面照 请求体上限2MB(nginx 层限制)。超限直接返回 HTTP 413,不会进入业务逻辑。建议图片文件 ≤1MB;image_base64 传图会膨胀约 33%,编码前原图建议 ≤1.4MB。手机直拍照片通常 3~8MB,上传前务必先压缩(如最长边缩到 1500px、JPEG 质量 80)。image_url 方式图片在远端,不受此限制 @@ -96,9 +98,10 @@
-

22/42/52/72 客户端鉴权

+

客户端鉴权

-

使用同一个访问令牌调用 22、42、52 和 72。账号为 yonghe,密码由对接人员提供。客户端发送 Authorization: Bearer <access_token>。

+

客户端发送 Authorization: Bearer <access_token>。账号为 yonghe,密码由对接人员提供。以下接口共用同一个访问令牌。

+

接口 11、接口 22、接口 42、接口 52、接口 61、接口 72

1. 登录获取令牌

@@ -187,6 +190,17 @@ const { code, data } = await res.json();
+
+

11. 四庭七眼测量(鉴权)  POST  /api/v1/face/measure-11

+
+

请求使用 Bearer 访问令牌,图片参数和响应字段见接口 1。返回四庭七眼测量数据和透明标注图 URL。

+
curl -sS -X POST "$BASE_URL/api/v1/face/measure-11" \
+  -H "Authorization: Bearer $ACCESS_TOKEN" \
+  -F 'image_file=@photo.jpg'
+

打开接口 11 测试页

+
+
+

2. C端生发  POST  /api/v1/hair/grow

@@ -455,6 +469,17 @@ console.log(features.gene_style); // "自然型"
+
+

61. 四庭七眼测量 v2(鉴权)  POST  /api/v1/face/measure-61

+
+

请求使用 Bearer 访问令牌,图片参数和响应字段见接口 6。返回去顶庭的测量数据和透明标注图 URL。

+
curl -sS -X POST "$BASE_URL/api/v1/face/measure-61" \
+  -H "Authorization: Bearer $ACCESS_TOKEN" \
+  -F 'image_file=@photo.jpg'
+

打开接口 61 测试页

+
+
+

72. B 端五个关键点 POST /api/v1/face/b-end-points

@@ -487,7 +512,7 @@ console.log(features.gene_style); // "自然型" 1007图片参数错误 / 后端不可用参数传错 / 服务繁忙请稍后重试 1008图片格式不支持非 JPG/PNG / base64 解码失败 - 1009未授权HTTP 401。登录凭据错误,或 42/52/72 所需的令牌缺失、无效或过期。按 鉴权流程 处理。 + 1009未授权HTTP 401。登录凭据错误,或所需的访问令牌缺失、无效或过期。按 鉴权流程 处理。

注:1004 仍在使用(接口2/5 的 gender 校验);接口7(grow-v2)已弃用,请改用接口2。

先检查 HTTP 状态和 Content-Type,再解析 JSON。HTTP 200 仍需检查 code,只有 0 表示成功。HTTP 401 的 JSON 用于鉴权提示与刷新。上传超限可能返回 HTTP 413,网关异常可能返回 502/504,这些响应可能是 HTML。

@@ -501,6 +526,7 @@ console.log(features.gene_style); // "自然型" + @@ -510,6 +536,7 @@ console.log(features.gene_style); // "自然型" + diff --git a/static/test_interface11_auth.html b/static/test_interface11_auth.html new file mode 100644 index 0000000..3c71b1e --- /dev/null +++ b/static/test_interface11_auth.html @@ -0,0 +1,78 @@ + + + + + +接口 11 — 四庭七眼测量鉴权测试 + + + + + + +
+

接口 11 — 四庭七眼测量鉴权测试

+

POST /api/v1/face/measure-11  |  上传正面照 → 原图+标注叠加显示

+ +
+
+ + + + + +
+

请使用对接账号登录后提交图片。

+ + + +
+
+
+ +
+ + +
+
+ 支持 JPG / PNG 正面照  |  也可拖拽图片到文件选择框 +
+
+ + + + + + + +
+ + diff --git a/static/test_interface61.html b/static/test_interface61.html new file mode 100644 index 0000000..be7d2ac --- /dev/null +++ b/static/test_interface61.html @@ -0,0 +1,83 @@ + + + + + +接口 61 — 四庭七眼测量 v2 鉴权测试 + + + + + + +
+

接口 61 — 四庭七眼测量 v2 鉴权测试

+

POST /api/v1/face/measure-61  |  去顶庭 · 竖线发际线→下巴 · 无头部端线

+
    +
  • 去顶庭:不画头顶横线,不返回顶庭数据;face_total_height_cm = 上+中+下庭
  • +
  • 竖线范围:发际线→下巴尖(接口1 为头顶→下巴尖)
  • +
  • 不画头部端线:仅七眼 6 点 5 段标尺(接口1 为 8 点 7 段)
  • +
+ +
+
+ + + + + +
+

请使用对接账号登录后提交图片。

+ + + +
+
+
+ +
+ + +
+
+ 支持 JPG / PNG 正面照  |  也可拖拽图片到文件选择框 +
+
+ + + + + + + +
+ + diff --git a/tests/test_gateway_client_auth.py b/tests/test_gateway_client_auth.py index 717c207..df94ced 100644 --- a/tests/test_gateway_client_auth.py +++ b/tests/test_gateway_client_auth.py @@ -14,7 +14,11 @@ LOGIN = "/api/v1/auth/login" REFRESH = "/api/v1/auth/refresh" FEATURES_42 = "/api/v1/face/features-42" CHECK = "/api/v1/auth/check" -PROTECTED = [FEATURES_42, "/api/v1/hairline/generate-52", "/api/v1/face/b-end-points", "/api/v1/hair/grow-22"] +MEASURE_ROUTES = { + "/api/v1/face/measure-11": "/api/v1/face/measure", + "/api/v1/face/measure-61": "/api/v1/face/measure-v2", +} +PROTECTED = [*MEASURE_ROUTES, FEATURES_42, "/api/v1/hairline/generate-52", "/api/v1/face/b-end-points", "/api/v1/hair/grow-22"] @pytest.fixture @@ -39,7 +43,8 @@ def test_protected_routes_reject_missing_static_and_refresh_tokens(client, monke monkeypatch.setattr(gateway, "_proxy", proxy) monkeypatch.setattr(gateway, "_face_features_response", features) refresh_token = login(client)["refresh_token"] - for headers in ({}, {"X-API-Token": "yonghe1999"}, {"Authorization": "Bearer " + refresh_token}): + for headers in ({}, {"X-API-Token": "yonghe1999"}, {"Authorization": "Bearer invalid"}, + {"Authorization": "Bearer " + refresh_token}): result = client.post(path, headers=headers) assert result.status_code == 401 assert result.json()["code"] == 1009 @@ -47,6 +52,20 @@ def test_protected_routes_reject_missing_static_and_refresh_tokens(client, monke features.assert_not_awaited() +@pytest.mark.parametrize("path,legacy_path", MEASURE_ROUTES.items()) +def test_measure_alias_uses_legacy_path_and_preserves_legacy_access(client, monkeypatch, path, legacy_path): + proxy = AsyncMock(return_value={"code": 0, "data": {"annotated_image_url": "/static/annotations/face.png"}}) + monkeypatch.setattr(gateway, "_proxy", proxy) + headers = {"Authorization": "Bearer " + login(client)["access_token"]} + photo = {"image_file": ("face.png", b"raw", "image/png")} + + authenticated = client.post(path, headers=headers, files=photo) + legacy = client.post(legacy_path, files=photo) + assert authenticated.status_code == legacy.status_code == 200 + assert authenticated.json() == legacy.json() == proxy.return_value + assert [call.args[1] for call in proxy.await_args_list] == [legacy_path, legacy_path] + + def test_login_expiry_tampering_and_refresh(client, monkeypatch): clock = SimpleNamespace(now=1_000_000) monkeypatch.setattr(client_auth, "time", SimpleNamespace(time=lambda: clock.now))
接口测试页功能
1. 四庭七眼/static/test_interface1.html上传照片 → 原图+标注叠加,底图/标注开关,指标卡片
11. 四庭七眼(鉴权)/static/test_interface11_auth.html登录、测试鉴权、刷新令牌、四庭七眼测量
2. C端生发/static/test_interface2.html上传+性别 → 方案一覧(原图/叠加/生发),双图对比
22. 生发密度与发际线高度/static/test_interface22.html登录后生成,支持密度多选和发际线高度预览
3. B端生发/static/test_interface3.html划线图上传 → 生发效果图
52. 发际线新规则/static/test_interface52.html登录、测试鉴权、刷新令牌、发际线预览与生发
72. B 端五个关键点/static/test_interface72.html登录、测试鉴权、刷新令牌、关键点和虚线标注
6. 四庭七眼 v2/static/test_interface6.html同接口1,去顶庭 · 竖线发际线→下巴 · 无头部端线
61. 四庭七眼 v2(鉴权)/static/test_interface61.html登录、测试鉴权、刷新令牌、三庭测量
9. 头发遮罩/static/test_interface9.html上传照片 → 头发遮罩 + 分步可视化
10. 外缘膨胀带/static/test_interface10.html上传照片 → 头部外缘带状遮罩 + 分步可视化
99. 人脸特殊点/static/test_interface99.html上传照片 → 60 关键点 + 5 发际线点坐标(测试大图请先压缩到 2MB 内,见通用约定)