security: 清除全部公开测试页硬编码的 worker 内部密码
- 16 个测试页移除 X-Internal-Token 硬编码(网关不校验该头,删除零影响,已线上验证) - 11/12 系列保留 token 输入框仅清空默认值,直连 worker 调试改为手输 - 接入文档 integration.html:补请求体 2MB 上限与 413 说明、HTTP 层错误提示、接口4 7 字段、补全 9/10/99 测试页入口
This commit is contained in:
+10
-4
@@ -72,6 +72,7 @@
|
||||
<tr><td>Content-Type</td><td><code>multipart/form-data</code></td></tr>
|
||||
<tr><td>图片参数</td><td>三选一:<code>image_file</code>(文件上传)/ <code>image_url</code>(URL)/ <code>image_base64</code>(base64 + 前缀)</td></tr>
|
||||
<tr><td>图片格式</td><td>JPG / PNG,单人正面照</td></tr>
|
||||
<tr><td>请求体上限</td><td><strong>2MB</strong>(nginx 层限制)。超限直接返回 HTTP <strong>413</strong>,不会进入业务逻辑。建议图片文件 ≤1MB;<code>image_base64</code> 传图会膨胀约 33%,编码前原图建议 ≤1.4MB。手机直拍照片通常 3~8MB,<strong>上传前务必先压缩</strong>(如最长边缩到 1500px、JPEG 质量 80)。<code>image_url</code> 方式图片在远端,不受此限制</td></tr>
|
||||
</table>
|
||||
|
||||
<p style="margin-top:14px;font-size:13px"><strong>统一响应结构</strong>:</p>
|
||||
@@ -190,22 +191,23 @@ const { code, data } = await res.json();
|
||||
<div class="card" id="if4">
|
||||
<h2>4. 用户特征分析 <span class="badge post">POST</span> <code>/api/v1/face/features</code></h2>
|
||||
<div class="card-body">
|
||||
<p class="desc">上传照片 → 火山方舟豆包视觉模型分析 → 返回固定 6 项面部特征(脸型/眉形/面部年龄/动静类型/性别/基因风格)。</p>
|
||||
<p class="desc">上传照片 → 火山方舟豆包视觉模型分析 → 返回固定 7 项面部特征(脸型/眉形/面部年龄/动静类型/性别/基因风格/轮廓类型)。</p>
|
||||
|
||||
<p><strong>入参</strong>:image_file / image_url / image_base64 三选一。无其他参数。</p>
|
||||
|
||||
<p style="margin-top:12px"><strong>data 字段</strong></p>
|
||||
<table>
|
||||
<tr><th>字段</th><th>类型</th><th>说明</th></tr>
|
||||
<tr><td><code>features</code></td><td>string</td><td><strong>JSON 字符串</strong>(不是对象!客户端需 <code>JSON.parse()</code>)。解析后得到<strong>固定 6 个英文字段</strong></td></tr>
|
||||
<tr><td><code>features</code></td><td>string</td><td><strong>JSON 字符串</strong>(不是对象!客户端需 <code>JSON.parse()</code>)。解析后得到<strong>固定 7 个英文字段</strong></td></tr>
|
||||
</table>
|
||||
|
||||
<p style="margin-top:8px"><strong>features 字段</strong>(固定返回 6 个):</p>
|
||||
<p style="margin-top:8px"><strong>features 字段</strong>(固定返回 7 个):</p>
|
||||
<table>
|
||||
<tr><th>字段</th><th>说明</th><th>字段</th><th>说明</th></tr>
|
||||
<tr><td>face_shape</td><td>脸型</td><td>eyebrow_shape</td><td>眉形</td></tr>
|
||||
<tr><td>facial_age</td><td>面部年龄区间</td><td>gender</td><td>性别</td></tr>
|
||||
<tr><td>dynamic_static_type</td><td>动静类型</td><td>gene_style</td><td>基因风格</td></tr>
|
||||
<tr><td>contour_type</td><td>轮廓类型(轮廓偏曲/轮廓适中/轮廓偏直)</td><td>—</td><td>—</td></tr>
|
||||
</table>
|
||||
|
||||
<pre>const res = await fetch('https://hair.xiangsilian.com/api/v1/face/features', {
|
||||
@@ -309,6 +311,7 @@ console.log(features.gene_style); // "自然型"</pre>
|
||||
<tr><td>1009</td><td>未授权</td><td>缺少或错误的 <code>X-Internal-Token</code>(<code>/api/*</code> 路径鉴权)</td></tr>
|
||||
</table>
|
||||
<p style="font-size:12px;color:#94a3b8;margin-top:8px">注:1004 仍在使用(接口2/5 的 gender 校验);接口7(grow-v2)已弃用,请改用接口2。</p>
|
||||
<p style="font-size:12px;color:#dc2626;margin-top:8px">⚠ <strong>HTTP 层错误不是业务 JSON</strong>:业务错误(code≠0)一律返回 HTTP 200;但请求体超 2MB 时 nginx 直接返回 <strong>413</strong>(HTML 错误页)、网关不可用时返回 502/504。客户端务必先判断 HTTP 状态码,非 200 时不要 <code>res.json()</code>,否则会报 <code>Unexpected token '<'</code>(把 HTML 当 JSON 解析)。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -321,9 +324,12 @@ console.log(features.gene_style); // "自然型"</pre>
|
||||
<tr><td>1. 四庭七眼</td><td><a href="/static/test_interface1.html" class="link">/static/test_interface1.html</a></td><td>上传照片 → 原图+标注叠加,底图/标注开关,指标卡片</td></tr>
|
||||
<tr><td>2. C端生发</td><td><a href="/static/test_interface2.html" class="link">/static/test_interface2.html</a></td><td>上传+性别 → 方案一覧(原图/叠加/生发),双图对比</td></tr>
|
||||
<tr><td>3. B端生发</td><td><a href="/static/test_interface3.html" class="link">/static/test_interface3.html</a></td><td>划线图上传 → 生发效果图</td></tr>
|
||||
<tr><td>4. 用户特征</td><td><a href="/static/test_interface4.html" class="link">/static/test_interface4.html</a></td><td>上传照片 → 6项面部特征 + 原始JSON</td></tr>
|
||||
<tr><td>4. 用户特征</td><td><a href="/static/test_interface4.html" class="link">/static/test_interface4.html</a></td><td>上传照片 → 7项面部特征 + 原始JSON</td></tr>
|
||||
<tr><td>5. 发际线PNG</td><td><a href="/static/test_interface5.html" class="link">/static/test_interface5.html</a></td><td>上传+性别 → 发际线方案+中心点坐标</td></tr>
|
||||
<tr><td>6. 四庭七眼 v2</td><td><a href="/static/test_interface6.html" class="link">/static/test_interface6.html</a></td><td>同接口1,去顶庭 · 竖线发际线→下巴 · 无头部端线</td></tr>
|
||||
<tr><td>9. 头发遮罩</td><td><a href="/static/test_interface9.html" class="link">/static/test_interface9.html</a></td><td>上传照片 → 头发遮罩 + 分步可视化</td></tr>
|
||||
<tr><td>10. 外缘膨胀带</td><td><a href="/static/test_interface10.html" class="link">/static/test_interface10.html</a></td><td>上传照片 → 头部外缘带状遮罩 + 分步可视化</td></tr>
|
||||
<tr><td>99. 人脸特殊点</td><td><a href="/static/test_interface99.html" class="link">/static/test_interface99.html</a></td><td>上传照片 → 60 关键点 + 5 发际线点坐标(<strong>测试大图请先压缩到 2MB 内</strong>,见通用约定)</td></tr>
|
||||
</table>
|
||||
<p style="font-size:12px;color:#94a3b8;margin-top:12px">
|
||||
完整 API 文档:<a href="/docs" class="link">/docs</a>(Swagger UI)
|
||||
|
||||
@@ -130,7 +130,6 @@
|
||||
|
||||
<script>
|
||||
const API_BASE = window.location.origin;
|
||||
const TOKEN = 'dev-shared-secret-2026';
|
||||
|
||||
const FEAT_LABELS = {
|
||||
face_height: '脸高 (px)',
|
||||
@@ -182,7 +181,6 @@ async function submitTest() {
|
||||
try {
|
||||
const r = await fetch(API_BASE + '/api/v1/debug/face-shape', {
|
||||
method: 'POST',
|
||||
headers: { 'X-Internal-Token': TOKEN },
|
||||
body: fd,
|
||||
});
|
||||
const json = await r.json();
|
||||
|
||||
@@ -150,7 +150,7 @@ async function submitTest() {
|
||||
form.append('image_file', file);
|
||||
|
||||
try {
|
||||
const resp = await fetch(API_BASE + '/api/v1/face/measure', { method: 'POST', headers: { 'X-Internal-Token': 'dev-shared-secret-2026' }, body: form });
|
||||
const resp = await fetch(API_BASE + '/api/v1/face/measure', { method: 'POST', body: form });
|
||||
const json = await resp.json();
|
||||
const _elapsed = ((performance.now() - _reqStart) / 1000).toFixed(2);
|
||||
|
||||
|
||||
@@ -214,7 +214,7 @@ async function submitTest() {
|
||||
form.append('dilate_cm', $('dilateCm').value || '2.0');
|
||||
|
||||
try {
|
||||
const resp = await fetch(API_BASE + ENDPOINT, { method: 'POST', headers: { 'X-Internal-Token': 'dev-shared-secret-2026' }, body: form });
|
||||
const resp = await fetch(API_BASE + ENDPOINT, { method: 'POST', body: form });
|
||||
const json = await resp.json();
|
||||
const dt = ((performance.now() - t0) / 1000).toFixed(2);
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
</div>
|
||||
<div class="upload-row" style="margin-top:10px">
|
||||
<label style="font-size:13px; font-weight:600; white-space:nowrap">X-Internal-Token</label>
|
||||
<input type="text" id="token" placeholder="直连 worker 需填;走网关可留空" value="dev-shared-secret-2026"
|
||||
<input type="text" id="token" placeholder="直连 worker 需填;走网关可留空" value=""
|
||||
style="flex:1; min-width:200px; padding:8px; border:1px solid #ddd; border-radius:8px">
|
||||
</div>
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
</div>
|
||||
<div class="upload-row" style="margin-top:10px">
|
||||
<label style="font-size:13px;font-weight:600;white-space:nowrap">X-Internal-Token</label>
|
||||
<input type="text" id="token" value="dev-shared-secret-2026" style="flex:1;min-width:200px;padding:8px;border:1px solid #ddd;border-radius:8px">
|
||||
<input type="text" id="token" value="" style="flex:1;min-width:200px;padding:8px;border:1px solid #ddd;border-radius:8px">
|
||||
</div>
|
||||
<div class="params">
|
||||
<div class="pf">
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
</div>
|
||||
<div class="upload-row" style="margin-top:10px">
|
||||
<label style="font-size:13px;font-weight:600;white-space:nowrap">X-Internal-Token</label>
|
||||
<input type="text" id="token" value="dev-shared-secret-2026" style="flex:1;min-width:200px;padding:8px;border:1px solid #ddd;border-radius:8px">
|
||||
<input type="text" id="token" value="" style="flex:1;min-width:200px;padding:8px;border:1px solid #ddd;border-radius:8px">
|
||||
</div>
|
||||
<div class="params">
|
||||
<div class="pf">
|
||||
|
||||
@@ -92,7 +92,6 @@
|
||||
<script>
|
||||
const API_BASE = window.location.origin;
|
||||
const ENDPOINT = '/api/v1/hairline/grow_v2_final';
|
||||
const TOKEN = 'dev-shared-secret-2026';
|
||||
|
||||
// 缓存最近一次后端返回的 final(data URI)和纯红遮罩(data URI),供重绘使用
|
||||
let _finalDataUri = '';
|
||||
@@ -145,7 +144,6 @@ async function submitTest() {
|
||||
try {
|
||||
const resp = await fetch(API_BASE + ENDPOINT, {
|
||||
method: 'POST',
|
||||
headers: { 'X-Internal-Token': TOKEN },
|
||||
body: form,
|
||||
});
|
||||
const json = await resp.json();
|
||||
|
||||
@@ -85,7 +85,6 @@
|
||||
<script>
|
||||
const API_BASE = window.location.origin;
|
||||
const ENDPOINT = '/api/v1/hairline/grow_v2_final_v2';
|
||||
const TOKEN = 'dev-shared-secret-2026';
|
||||
|
||||
function $(id) { return document.getElementById(id); }
|
||||
function setStatus(text, type) { const b = $('statusBar'); b.textContent = text; b.className = 'status ' + type; }
|
||||
@@ -118,7 +117,6 @@ async function submitTest() {
|
||||
try {
|
||||
const resp = await fetch(API_BASE + ENDPOINT, {
|
||||
method: 'POST',
|
||||
headers: { 'X-Internal-Token': TOKEN },
|
||||
body: form,
|
||||
});
|
||||
const json = await resp.json();
|
||||
|
||||
@@ -157,7 +157,7 @@ async function submitTest() {
|
||||
fd.append('prompt', $('promptInput').value);
|
||||
const _reqStart = performance.now();
|
||||
try {
|
||||
const r = await fetch(API_BASE + '/api/v1/hair/grow', { method:'POST', headers:{ 'X-Internal-Token': 'dev-shared-secret-2026' }, body:fd });
|
||||
const r = await fetch(API_BASE + '/api/v1/hair/grow', { method:'POST', body:fd });
|
||||
const json = await r.json();
|
||||
const _elapsed = ((performance.now() - _reqStart) / 1000).toFixed(2);
|
||||
$('jsonContent').textContent = JSON.stringify(json, null, 2);
|
||||
|
||||
@@ -162,7 +162,7 @@ async function submitTest() {
|
||||
|
||||
const _reqStart = performance.now();
|
||||
try {
|
||||
const r = await fetch(API_BASE + '/api/v1/hair/grow-b', { method:'POST', headers: { 'X-Internal-Token': 'dev-shared-secret-2026' }, body:fd });
|
||||
const r = await fetch(API_BASE + '/api/v1/hair/grow-b', { method:'POST', body:fd });
|
||||
const json = await r.json();
|
||||
const _elapsed = ((performance.now() - _reqStart) / 1000).toFixed(2);
|
||||
$('jsonContent').textContent = JSON.stringify(json, null, 2);
|
||||
|
||||
@@ -220,7 +220,7 @@ async function submitTest() {
|
||||
const fd = new FormData(); fd.append('image_file', f); fd.append('gender', $('gender').value); fd.append('hair_style', checked.join(',')); fd.append('generate_grow_image', $('genGrowImg').checked ? 'true' : 'false');
|
||||
const _reqStart = performance.now();
|
||||
try {
|
||||
const r = await fetch(API_BASE + '/api/v1/hairline/generate', { method:'POST', headers: { 'X-Internal-Token': 'dev-shared-secret-2026' }, body:fd });
|
||||
const r = await fetch(API_BASE + '/api/v1/hairline/generate', { method:'POST', body:fd });
|
||||
const json = await r.json();
|
||||
const _elapsed = ((performance.now() - _reqStart) / 1000).toFixed(2);
|
||||
$('jsonContent').textContent = JSON.stringify(json, null, 2);
|
||||
|
||||
@@ -152,7 +152,7 @@ async function submitTest() {
|
||||
form.append('image_file', file);
|
||||
|
||||
try {
|
||||
const resp = await fetch(API_BASE + '/api/v1/face/measure-v2', { method: 'POST', headers: { 'X-Internal-Token': 'dev-shared-secret-2026' }, body: form });
|
||||
const resp = await fetch(API_BASE + '/api/v1/face/measure-v2', { method: 'POST', body: form });
|
||||
const json = await resp.json();
|
||||
const _elapsed = ((performance.now() - _reqStart) / 1000).toFixed(2);
|
||||
|
||||
|
||||
@@ -154,7 +154,7 @@ async function submitTest() {
|
||||
fd.append('prompt', $('promptInput').value);
|
||||
const _reqStart = performance.now();
|
||||
try {
|
||||
const r = await fetch(API_BASE + '/api/v1/hair/grow-v2', { method:'POST', headers: { 'X-Internal-Token': 'dev-shared-secret-2026' }, body:fd });
|
||||
const r = await fetch(API_BASE + '/api/v1/hair/grow-v2', { method:'POST', body:fd });
|
||||
const json = await r.json();
|
||||
const _elapsed = ((performance.now() - _reqStart) / 1000).toFixed(2);
|
||||
$('jsonContent').textContent = JSON.stringify(json, null, 2);
|
||||
|
||||
@@ -210,7 +210,7 @@ async function submitTest() {
|
||||
form.append('erode_cm', $('erodeCm').value || '1.2');
|
||||
|
||||
try {
|
||||
const resp = await fetch(API_BASE + ENDPOINT, { method: 'POST', headers: { 'X-Internal-Token': 'dev-shared-secret-2026' }, body: form });
|
||||
const resp = await fetch(API_BASE + ENDPOINT, { method: 'POST', body: form });
|
||||
const json = await resp.json();
|
||||
const dt = ((performance.now() - t0) / 1000).toFixed(2);
|
||||
|
||||
|
||||
@@ -129,7 +129,7 @@ async function submitTest() {
|
||||
form.append('image_file', file);
|
||||
|
||||
try {
|
||||
const resp = await fetch(API_BASE + ENDPOINT, { method: 'POST', headers: { 'X-Internal-Token': 'dev-shared-secret-2026' }, body: form });
|
||||
const resp = await fetch(API_BASE + ENDPOINT, { method: 'POST', body: form });
|
||||
const json = await resp.json();
|
||||
const dt = ((performance.now() - t0) / 1000).toFixed(2);
|
||||
lastJson = json;
|
||||
|
||||
Reference in New Issue
Block a user