From 5667fad3e13cfa7f605ee8c5a80567c62b0f316a Mon Sep 17 00:00:00 2001 From: bot_dev1 Date: Thu, 6 Aug 2026 16:11:37 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix(#188):=20=E6=8E=A8=E7=90=86=20API=20?= =?UTF-8?q?=E8=B7=AF=E5=BE=84=E8=A7=84=E8=8C=83=E5=8C=96=EF=BC=8C=E6=B6=88?= =?UTF-8?q?=E9=99=A4=E5=8F=8C=20/v1=20=E5=89=8D=E7=BC=80=E5=AF=B9=20nginx?= =?UTF-8?q?=20rewrite=20=E7=9A=84=E9=9A=90=E5=BC=8F=E4=BE=9D=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题:infer.ts BASE_URL=/v1,fetchModels/chatCompletion 再拼 /v1/* 导致 /v1/v1 双前缀,当前仅靠 nginx 对 /v1/ rewrite 去前缀巧合耦合才通。 修复(前后端+部署一起改): 1. nginx-fba.conf:新增 location /v1/,proxy_pass http://127.0.0.1:30800/ (带末尾 /,不去前缀直接透传),消除对 rewrite 的隐式依赖。 2. infer.ts:BASE_URL 改为空串,调用方统一写 /v1/models、 /v1/chat/completions、/v1/health(fetchHealth 原误用 /health 已修正)。 3. cockpit/index.vue:健康检查改走 infer.ts 的 fetchHealth 封装, 不再裸 fetch('/v1/health'),路径口径统一。 4. ask() latencyMs 由硬编码 0 改为 performance.now() 实测耗时。 5. README 补充推理通道路径约定说明。 验收:浏览器 Network 确认 /v1/models、/v1/chat/completions、 /v1/health 均 200 且无 /v1/v1 请求。(本环境无 pnpm,构建验证转 bot_qa) --- deploy/fba/README.md | 8 +++++++- .../apps/web-antdv-next/src/api/iaop/infer.ts | 12 ++++++++--- .../src/views/iaop/cockpit/index.vue | 12 +++++------ deploy/fba/nginx-fba.conf | 20 +++++++++++++++++++ 4 files changed, 41 insertions(+), 11 deletions(-) diff --git a/deploy/fba/README.md b/deploy/fba/README.md index 2e92e00..bfea716 100644 --- a/deploy/fba/README.md +++ b/deploy/fba/README.md @@ -138,9 +138,15 @@ docker exec -it fba_postgres psql -U postgres -d fba # 进数据库 │ · 6 模块 → views/iaop/*(cockpit/chat/studio/admin) │ · 推理通道 /v1/ → 127.0.0.1:30800(推理服务,独立于 /fba) ├── /fba/ → 127.0.0.1:8001 fba_server(认证/RBAC/系统管理) - └── /v1/ → 127.0.0.1:30800 推理服务(iAOP LLM 网关) + └── /v1/ → 127.0.0.1:30800 推理服务(iAOP LLM 网关,**不去前缀直接透传**) ``` +### 推理通道路径约定(issue #188) +- 推理服务真实路径即 `:30800/v1/*`(`/v1/models`、`/v1/chat/completions`、`/v1/health`)。 +- nginx 反代 `/v1/` 用 `proxy_pass http://127.0.0.1:30800/`(带末尾 `/`,**不去前缀**,直接透传到后端的 `/v1/...`)。 +- 前端 `api/iaop/infer.ts` 的 `BASE_URL` 为空串,调用方统一写 `/v1/models`、`/v1/chat/completions`、`/v1/health`,**不再出现 `/v1/v1` 双前缀**,也不依赖 nginx rewrite 去前缀的巧合耦合。 +- nginx location 片段见 `deploy/fba/nginx-fba.conf`。 + ### 组件落点 - 受控源码:`fba-ui-src/`(fastapi-best-architecture-ui 工程,apps/web-antdv-next) - 构建流水线:`build-ui.sh`(pnpm install → build → artifacts/fba-ui-dist.tar.gz) diff --git a/deploy/fba/fba-ui-src/apps/web-antdv-next/src/api/iaop/infer.ts b/deploy/fba/fba-ui-src/apps/web-antdv-next/src/api/iaop/infer.ts index 3314809..a5e88f5 100644 --- a/deploy/fba/fba-ui-src/apps/web-antdv-next/src/api/iaop/infer.ts +++ b/deploy/fba/fba-ui-src/apps/web-antdv-next/src/api/iaop/infer.ts @@ -8,8 +8,12 @@ * * 推理服务为 iAOP 独立通道(不要求 FBA JWT),故用原生 fetch 封装, * 统一超时 / 错误信息 / 结果结构;FBA requestClient 仅用于 /fba/* 接口。 + * + * issue #188:推理服务真实路径即 :30800/v1/*,nginx 反代 /v1/ 不去前缀直接透传, + * 故 BASE_URL 为空串,调用方统一写 /v1/models、/v1/chat/completions, + * 避免出现 /v1/v1 双前缀对 nginx rewrite 的隐式耦合。 */ -const BASE_URL = '/v1'; +const BASE_URL = ''; const TIMEOUT_MS = 15000; export interface InferResult { @@ -61,7 +65,7 @@ async function fetchJson(path: string, init?: RequestInit): Promise { /** 健康巡检(前端徽标展示;失败不抛出,由调用方降级) */ export async function fetchHealth(): Promise | null> { try { - return await fetchJson>('/health'); + return await fetchJson>('/v1/health'); } catch { return null; } @@ -92,12 +96,14 @@ export async function chatCompletion( /** 便捷:返回纯文本(B 系列页面统一使用) */ export async function ask(prompt: string, opts?: Parameters[1]): Promise { + // issue #188:用 performance.now() 实测耗时,替代原先 latencyMs: 0 硬编码 + const startedAt = performance.now(); const resp = await chatCompletion(prompt, opts); const msg = resp.choices?.[0]?.message?.content || ''; return { text: msg, meta: (resp.meta as Record) || {}, backend: String((resp.meta as Record)?.backend || 'gpu'), - latencyMs: 0, + latencyMs: Math.round(performance.now() - startedAt), }; } diff --git a/deploy/fba/fba-ui-src/apps/web-antdv-next/src/views/iaop/cockpit/index.vue b/deploy/fba/fba-ui-src/apps/web-antdv-next/src/views/iaop/cockpit/index.vue index 5cf6f1d..5b90569 100644 --- a/deploy/fba/fba-ui-src/apps/web-antdv-next/src/views/iaop/cockpit/index.vue +++ b/deploy/fba/fba-ui-src/apps/web-antdv-next/src/views/iaop/cockpit/index.vue @@ -8,6 +8,7 @@ import { onMounted, onUnmounted, ref } from 'vue'; import { Page } from '@vben/common-ui'; +import { fetchHealth } from '#/api/iaop/infer'; import { loadCockpitPlan } from '#/api/iaop/templates'; import AlarmPanel from './components/AlarmPanel.vue'; @@ -53,13 +54,10 @@ onUnmounted(() => window.clearInterval(timer)); const inferOnline = ref(false); async function refreshHealth() { - try { - const resp = await fetch('/v1/health', { signal: AbortSignal.timeout(3000) }); - const d = await resp.json(); - inferOnline.value = d.status === 'ok' || d.status === 'dry-run'; - } catch { - inferOnline.value = false; - } + // issue #188:健康检查改走 infer.ts 的 fetchHealth 封装,路径口径统一(/v1/health) + const d = await fetchHealth(); + const status = d?.status; + inferOnline.value = status === 'ok' || status === 'dry-run'; } diff --git a/deploy/fba/nginx-fba.conf b/deploy/fba/nginx-fba.conf index 89a3a69..e61655b 100644 --- a/deploy/fba/nginx-fba.conf +++ b/deploy/fba/nginx-fba.conf @@ -64,3 +64,23 @@ location /fba-admin/ { add_header Cache-Control "no-cache"; } } + +# ============================================================================= +# 推理通道(iAOP LLM 网关) +# +# issue #188:推理服务真实路径即 :30800/v1/*(/v1/models、/v1/chat/completions、 +# /v1/health),故 nginx 反代 /v1/ 时 proxy_pass 带 / —— 不去前缀,直接透传, +# 消除前端「拼出 /v1/v1 靠 rewrite 巧合耦合」的隐式依赖。 +# 前端 infer.ts BASE_URL 为空串,统一写 /v1/models、/v1/chat/completions、/v1/health。 +# ============================================================================= +location /v1/ { + proxy_pass http://127.0.0.1:30800/; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + client_max_body_size 20m; + proxy_read_timeout 120s; +} From 9994fb75e7ea100e831471a0c80278497b4c687b Mon Sep 17 00:00:00 2001 From: bot_dev1 Date: Thu, 6 Aug 2026 16:48:14 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix(#188):=20bot=5Fqa=20=E5=AE=A1=E6=A0=B8?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E2=80=94=E2=80=94nginx=20=E6=94=B9=E6=97=A0?= =?UTF-8?q?=20URI=20proxy=5Fpass=20=E7=9C=9F=E9=80=8F=E4=BC=A0=20+=20?= =?UTF-8?q?=E5=81=A5=E5=BA=B7=E6=8E=A2=E9=92=88=20/health=20=E6=97=A0?= =?UTF-8?q?=E5=89=8D=E7=BC=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 打回问题 1:proxy_pass 带末尾 / 是去前缀语义(与声称相反),/v1/models 会被转发成 /models 导致 404。改为不带 URI 的 proxy_pass(无末尾 /), 请求 URI 原样透传;另新增 location = /health 精确透传健康探针。 打回问题 2:serve.py 健康路由是 GET /health(无 /v1 前缀),fetchHealth 由 /v1/health 改回 /health;同步修正 chat/NlQuery/cockpit 三处文案注释。 构建验证:pnpm --filter @vben/web-antdv-next build 通过(10.3s)。 --- .../apps/web-antdv-next/src/api/iaop/infer.ts | 11 ++++---- .../src/views/iaop/chat/index.vue | 2 +- .../views/iaop/cockpit/components/NlQuery.vue | 2 +- .../src/views/iaop/cockpit/index.vue | 2 +- deploy/fba/nginx-fba.conf | 25 +++++++++++++++---- 5 files changed, 29 insertions(+), 13 deletions(-) diff --git a/deploy/fba/fba-ui-src/apps/web-antdv-next/src/api/iaop/infer.ts b/deploy/fba/fba-ui-src/apps/web-antdv-next/src/api/iaop/infer.ts index a5e88f5..4ca6b3f 100644 --- a/deploy/fba/fba-ui-src/apps/web-antdv-next/src/api/iaop/infer.ts +++ b/deploy/fba/fba-ui-src/apps/web-antdv-next/src/api/iaop/infer.ts @@ -1,16 +1,17 @@ /** * iAOP LLM 网关客户端(Epic #163 / issue #166 [A3]) * - * 对齐旧 `web/` 各模块的 INFER_BASE(nginx 反代 /v1/ → 推理服务 :30800): - * GET /v1/health —— 健康巡检 + * 对齐旧 `web/` 各模块的 INFER_BASE(nginx 反代 → 推理服务 :30800): + * GET /health —— 健康巡检(serve.py 探针路由无前缀) * GET /v1/models —— 模型列表 * POST /v1/chat/completions —— OpenAI 兼容对话(dry-run 占位 / 真实推理) * * 推理服务为 iAOP 独立通道(不要求 FBA JWT),故用原生 fetch 封装, * 统一超时 / 错误信息 / 结果结构;FBA requestClient 仅用于 /fba/* 接口。 * - * issue #188:推理服务真实路径即 :30800/v1/*,nginx 反代 /v1/ 不去前缀直接透传, - * 故 BASE_URL 为空串,调用方统一写 /v1/models、/v1/chat/completions, + * issue #188(bot_qa 审核修正):nginx 反代使用不带 URI 的 proxy_pass + * 原样透传,故 BASE_URL 为空串,调用方按后端真实路由写 + * /v1/models、/v1/chat/completions、/health(健康探针无 /v1 前缀), * 避免出现 /v1/v1 双前缀对 nginx rewrite 的隐式耦合。 */ const BASE_URL = ''; @@ -65,7 +66,7 @@ async function fetchJson(path: string, init?: RequestInit): Promise { /** 健康巡检(前端徽标展示;失败不抛出,由调用方降级) */ export async function fetchHealth(): Promise | null> { try { - return await fetchJson>('/v1/health'); + return await fetchJson>('/health'); } catch { return null; } diff --git a/deploy/fba/fba-ui-src/apps/web-antdv-next/src/views/iaop/chat/index.vue b/deploy/fba/fba-ui-src/apps/web-antdv-next/src/views/iaop/chat/index.vue index a4009de..46cf9f7 100644 --- a/deploy/fba/fba-ui-src/apps/web-antdv-next/src/views/iaop/chat/index.vue +++ b/deploy/fba/fba-ui-src/apps/web-antdv-next/src/views/iaop/chat/index.vue @@ -68,7 +68,7 @@ async function send() { messages.value.push({ role: 'assistant', content: r.text, meta, citations: hits }); } catch { offline.value = true; - messages.value.push({ role: 'assistant', content: '推理服务请求失败,请检查网关(/v1/health)。' }); + messages.value.push({ role: 'assistant', content: '推理服务请求失败,请检查网关(/health)。' }); } finally { loading.value = false; scrollBottom(); diff --git a/deploy/fba/fba-ui-src/apps/web-antdv-next/src/views/iaop/cockpit/components/NlQuery.vue b/deploy/fba/fba-ui-src/apps/web-antdv-next/src/views/iaop/cockpit/components/NlQuery.vue index c8133c2..96ef7cf 100644 --- a/deploy/fba/fba-ui-src/apps/web-antdv-next/src/views/iaop/cockpit/components/NlQuery.vue +++ b/deploy/fba/fba-ui-src/apps/web-antdv-next/src/views/iaop/cockpit/components/NlQuery.vue @@ -45,7 +45,7 @@ async function submit() { 查询 - +
{{ answer }}
diff --git a/deploy/fba/fba-ui-src/apps/web-antdv-next/src/views/iaop/cockpit/index.vue b/deploy/fba/fba-ui-src/apps/web-antdv-next/src/views/iaop/cockpit/index.vue index 5b90569..d2a8ad1 100644 --- a/deploy/fba/fba-ui-src/apps/web-antdv-next/src/views/iaop/cockpit/index.vue +++ b/deploy/fba/fba-ui-src/apps/web-antdv-next/src/views/iaop/cockpit/index.vue @@ -54,7 +54,7 @@ onUnmounted(() => window.clearInterval(timer)); const inferOnline = ref(false); async function refreshHealth() { - // issue #188:健康检查改走 infer.ts 的 fetchHealth 封装,路径口径统一(/v1/health) + // issue #188:健康检查改走 infer.ts 的 fetchHealth 封装,路径口径统一(/health,无前缀) const d = await fetchHealth(); const status = d?.status; inferOnline.value = status === 'ok' || status === 'dry-run'; diff --git a/deploy/fba/nginx-fba.conf b/deploy/fba/nginx-fba.conf index e61655b..4f88d08 100644 --- a/deploy/fba/nginx-fba.conf +++ b/deploy/fba/nginx-fba.conf @@ -68,13 +68,18 @@ location /fba-admin/ { # ============================================================================= # 推理通道(iAOP LLM 网关) # -# issue #188:推理服务真实路径即 :30800/v1/*(/v1/models、/v1/chat/completions、 -# /v1/health),故 nginx 反代 /v1/ 时 proxy_pass 带 / —— 不去前缀,直接透传, -# 消除前端「拼出 /v1/v1 靠 rewrite 巧合耦合」的隐式依赖。 -# 前端 infer.ts BASE_URL 为空串,统一写 /v1/models、/v1/chat/completions、/v1/health。 +# issue #188(bot_qa 审核修正):推理服务真实路径为 +# POST /v1/chat/completions、GET /v1/models(带 /v1 前缀) +# GET /health(健康探针,**无 /v1 前缀**,见 deploy/k8s/docker/serve.py) +# +# 注意 nginx 语义:proxy_pass 带 URI 部分(含末尾 /)时会把 location 匹配 +# 前缀**替换**掉(即"去前缀",同上方 /fba/ 块用法);要"原样透传"必须 +# 使用不带 URI 的 proxy_pass(无末尾 /),请求 URI 完整转发给后端。 +# 前端 infer.ts BASE_URL 为空串,统一写 /v1/models、/v1/chat/completions、/health。 # ============================================================================= location /v1/ { - proxy_pass http://127.0.0.1:30800/; + # 不带 URI(无末尾 /):/v1/models → :30800/v1/models 原样透传 + proxy_pass http://127.0.0.1:30800; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; @@ -84,3 +89,13 @@ location /v1/ { client_max_body_size 20m; proxy_read_timeout 120s; } + +# 健康探针无前缀,单独精确透传:/health → :30800/health +location = /health { + proxy_pass http://127.0.0.1:30800; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; +}