fix(api): localize service-layer ValueError messages to Chinese

So str(exc) passthrough on scripts, batch, sync, and schedule routes
returns Chinese without relying only on the HTTP detail translation layer.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Nexus Agent
2026-06-09 08:53:18 +08:00
parent a842af2405
commit 1c0d7e9eb6
8 changed files with 49 additions and 9 deletions
@@ -0,0 +1,40 @@
# 2026-06-09 — 服务层 ValueError 中文化(str(exc) 透传源头)
## 摘要
将经 API `detail=str(exc)` 透传、且原先为英文的 `ValueError` 文案改为中文,避免 sync/scripts/批量/调度等接口在未走全局 `http_errors_zh` 映射时仍显示英文。
## 动机
待办 #1API detail 英改中)方案 1:优先改下层异常源头,比统一包装 `raise_api_error` 改动小、语义稳定。
## 涉及文件
| 文件 | 变更 |
|------|------|
| `server/application/services/script_service.py` | `Execution not found` → 执行记录不存在 |
| `server/application/services/server_batch_service.py` | `Unknown batch op` → 未知的批量操作 |
| `server/infrastructure/database/crypto.py` | `Failed to decrypt credential` → 凭据解密失败 |
| `server/infrastructure/ssh/asyncssh_pool.py` | SSH 凭据缺失提示中文化 |
| `server/utils/schedule_cycle.py` | 自定义 cron / 字段数 / 周期类型校验中文化 |
| `server/api/schemas.py` | `batch_id` 校验中文化 |
| `server/infrastructure/redis/script_execution_store.py` | 执行记录不存在提示中文化 |
## 迁移 / 重启
- 无需 DB 迁移
- 需重启 API 进程后生效
## 验证
```bash
pytest tests/test_schedule_cycle.py tests/test_http_errors_zh.py -q
bash scripts/local_verify.sh
```
手动:脚本重试不存在 execution、批量未知 op、调度非法 cycle、推送 batch_id 非法 → 422/400 detail 为中文。
## 未覆盖(后续)
- `f"SSH 连接失败: {e}"` 等库异常后缀(方案 2`translate_sync_error_message` 包装)
- API 层字面量 `detail="Server not found"`(已由 `http_errors_zh` 全局处理,待部署)
+1 -1
View File
@@ -225,7 +225,7 @@ class SyncFiles(BaseModel):
return None
s = str(v).strip().lower()
if len(s) != 12 or not all(c in "0123456789abcdef" for c in s):
raise ValueError("batch_id must be 12 hexadecimal characters")
raise ValueError("batch_id 必须为 12 位十六进制字符")
return s
@field_validator("source_path", mode="before")
@@ -622,7 +622,7 @@ class ScriptService:
"""Re-run command for failed / pending / cancelled servers."""
execution = await self.execution_repo.get_by_id(execution_id)
if not execution:
raise ValueError("Execution not found")
raise ValueError("执行记录不存在")
command, was_long = self._parse_execution_meta(execution.command or "")
if not command.strip():
@@ -201,7 +201,7 @@ async def start_batch_job(
params: Optional[dict[str, Any]] = None,
) -> dict[str, Any]:
if op not in OP_LABELS:
raise ValueError(f"Unknown batch op: {op}")
raise ValueError(f"未知的批量操作: {op}")
if not server_ids:
raise ValueError("server_ids 不能为空")
+1 -1
View File
@@ -51,4 +51,4 @@ def decrypt_value(ciphertext: str) -> str:
return get_fernet().decrypt(ciphertext.encode()).decode()
except Exception as e:
logger.warning(f"Fernet decryption failed (key mismatch?): {e}")
raise ValueError("Failed to decrypt credential") from e
raise ValueError("凭据解密失败") from e
@@ -333,7 +333,7 @@ async def apply_update(
if not live:
execution = await execution_repo.get_by_id(execution_id)
if not execution:
raise ValueError(f"Execution {execution_id} not found")
raise ValueError(f"执行记录 {execution_id} 不存在")
res, ev = unpack_results_blob(execution.results)
live = {
"id": execution_id,
+1 -1
View File
@@ -238,7 +238,7 @@ class AsyncSSHPool:
elif server.ssh_key_path:
connect_kwargs["client_keys"] = [server.ssh_key_path]
else:
raise ValueError(f"No valid SSH credentials for server {server.id} ({server.name})")
raise ValueError(f"服务器 {server.id}{server.name})没有可用的 SSH 凭据")
try:
conn = await asyncssh.connect(**connect_kwargs)
+3 -3
View File
@@ -31,10 +31,10 @@ def build_cron_from_cycle(config: dict[str, Any]) -> str:
if kind == "custom":
raw = str(config.get("customCron") or "").strip()
if not raw:
raise ValueError("custom cron empty")
raise ValueError("自定义 cron 不能为空")
parts = raw.split()
if len(parts) != 5:
raise ValueError("cron must be 5 fields")
raise ValueError("cron 表达式必须为 5 个字段")
return raw
minute = _clamp_int(config.get("minute", 0), 0, 59)
@@ -59,7 +59,7 @@ def build_cron_from_cycle(config: dict[str, Any]) -> str:
if kind == "month":
dom = _clamp_int(config.get("monthDay", 1), 1, 31)
return f"{minute} {hour} {dom} * *"
raise ValueError(f"unknown cycle type: {kind}")
raise ValueError(f"未知的周期类型: {kind}")
def _parse_step_or_fixed(field: str) -> tuple[str, int | None]: