feat(api): translate HTTPException detail to Chinese for admin routes

Add a global detail translator for common 4xx messages (Server not found,
auth headers, settings not found, etc.) while keeping /api/agent/* in English.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Nexus Agent
2026-06-09 08:42:43 +08:00
parent 213aad97a4
commit 3ba78cd10a
4 changed files with 163 additions and 2 deletions
@@ -0,0 +1,31 @@
# 2026-06-09 — HTTP API detail 英文改中文(全局层)
## 变更摘要
管理端 API 的 `HTTPException.detail` 经全局 handler 译为中文;`/api/agent/*` 保持英文(Agent 协议兼容)。
## 动机
Backlog「~75 条 API detail 英文」:422 已由 `validation_errors_zh.py` 覆盖,但 400/401/404 等仍返回 `Server not found` 等英文字面量。
## 涉及文件
- `server/utils/http_errors_zh.py`(新)— 精确映射 + 正则(如 immutable setting、command exit
- `server/main.py``StarletteHTTPException` handler 调用 `translate_http_detail`
- `tests/test_http_errors_zh.py`
## 迁移 / 重启
需重启 API / 重建镜像。
## 验证
```bash
pytest tests/test_http_errors_zh.py tests/test_validation_errors_zh.py -q
# 示例:GET /api/servers/999999 → detail「服务器不存在」
```
## 后续
- 新增英文字面量时补 `_DETAIL_EXACT` 或改源码直接写中文
- `str(exc)` 透传类错误依赖下层异常已是中文
+6 -2
View File
@@ -688,9 +688,13 @@ async def custom_http_exception_handler(request: Request, exc: StarletteHTTPExce
"""Convert 404 JSON to blank HTML — don't leak backend (FastAPI) info."""
if exc.status_code == 404:
return HTMLResponse(status_code=404)
# Let other HTTP exceptions return normally (auth errors, validation errors, etc.)
from fastapi.responses import JSONResponse
return JSONResponse(status_code=exc.status_code, content={"detail": exc.detail})
from server.utils.http_errors_zh import translate_http_detail
detail = exc.detail
if not request.url.path.startswith("/api/agent/"):
detail = translate_http_detail(detail)
return JSONResponse(status_code=exc.status_code, content={"detail": detail})
# ── Validation errors: agent paths at debug to reduce noise from legacy agents ──
+90
View File
@@ -0,0 +1,90 @@
"""Translate HTTPException detail strings to Chinese for admin-facing API responses."""
from __future__ import annotations
import re
from typing import Any
# Exact English literals from server/api (and shared services)
_DETAIL_EXACT: dict[str, str] = {
"Not found": "不存在",
"Server not found": "服务器不存在",
"Platform not found": "平台不存在",
"Node not found": "节点不存在",
"Admin not found": "管理员不存在",
"Setting not found": "设置项不存在",
"API_KEY not found": "API_KEY 不存在",
"Schedule not found": "调度任务不存在",
"Preset not found": "凭据预设不存在",
"SSH key preset not found": "SSH 密钥预设不存在",
"telegram_bot_token not found": "telegram_bot_token 不存在",
"Retry job not found": "重试任务不存在",
"Credential not found": "凭据不存在",
"Execution not found": "执行记录不存在",
"Script not found": "脚本不存在",
"Login failed": "登录失败",
"Missing refresh token": "缺少刷新令牌",
"Invalid refresh token": "刷新令牌无效",
"Missing Authorization header": "缺少 Authorization 请求头",
"Invalid or expired token": "令牌无效或已过期",
"Can only setup TOTP for yourself": "只能为自己设置 TOTP",
"Can only enable TOTP for yourself": "只能为自己启用 TOTP",
"Can only disable TOTP for yourself": "只能为自己禁用 TOTP",
"Setup failed": "设置失败",
"Enable failed": "启用失败",
"Disable failed": "禁用失败",
"Server has no domain configured": "服务器未配置域名",
"Missing API key": "缺少 API Key",
"Invalid API key for this server": "该服务器的 API Key 无效",
"Invalid or expired job": "任务无效或已过期",
"new_path required for rename": "重命名需要指定 new_path",
"Invalid operation": "无效操作",
"server_id required": "server_id 必填",
"Too many script callback requests": "脚本回调请求过于频繁",
}
_DETAIL_PATTERNS: list[tuple[re.Pattern[str], str]] = [
(
re.compile(r"^Command failed \(exit (\d+)\)$"),
r"命令失败(退出码 \1",
),
(
re.compile(
r"^Setting '([^']+)' is immutable and cannot be modified via API$"
),
r"设置项「\1」不可通过 API 修改",
),
]
def _looks_chinese(text: str) -> bool:
return any("\u4e00" <= ch <= "\u9fff" for ch in text)
def translate_http_detail_str(msg: str) -> str:
"""Return Chinese detail when a known translation exists."""
if not msg or _looks_chinese(msg):
return msg
if msg in _DETAIL_EXACT:
return _DETAIL_EXACT[msg]
for pattern, repl in _DETAIL_PATTERNS:
if pattern.fullmatch(msg):
return pattern.sub(repl, msg)
return msg
def translate_http_detail(detail: Any) -> Any:
"""Translate ``HTTPException.detail`` (str | list | dict) for JSON responses."""
if isinstance(detail, str):
return translate_http_detail_str(detail)
if isinstance(detail, list):
return [translate_http_detail(item) for item in detail]
if isinstance(detail, dict):
out: dict[Any, Any] = {}
for key, value in detail.items():
if key in ("msg", "message") and isinstance(value, str):
out[key] = translate_http_detail_str(value)
else:
out[key] = value
return out
return detail
+36
View File
@@ -0,0 +1,36 @@
"""Tests for HTTPException detail Chinese translation."""
from server.utils.http_errors_zh import translate_http_detail, translate_http_detail_str
def test_translate_server_not_found():
assert translate_http_detail_str("Server not found") == "服务器不存在"
def test_translate_auth_jwt_header():
assert translate_http_detail_str("Missing Authorization header") == "缺少 Authorization 请求头"
def test_translate_chinese_unchanged():
s = "NEXUS_API_BASE_URL 未配置"
assert translate_http_detail_str(s) == s
def test_translate_command_failed_pattern():
assert translate_http_detail_str("Command failed (exit 1)") == "命令失败(退出码 1"
def test_translate_immutable_setting_pattern():
msg = "Setting 'secret_key' is immutable and cannot be modified via API"
assert translate_http_detail_str(msg) == "设置项「secret_key」不可通过 API 修改"
def test_translate_dict_message_key():
out = translate_http_detail({"message": "Script not found", "code": 404})
assert out["message"] == "脚本不存在"
assert out["code"] == 404
def test_translate_list_detail():
out = translate_http_detail(["Server not found", "已配置"])
assert out == ["服务器不存在", "已配置"]