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:
@@ -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
@@ -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 ──
|
||||
|
||||
@@ -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
|
||||
@@ -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 == ["服务器不存在", "已配置"]
|
||||
Reference in New Issue
Block a user