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:
@@ -0,0 +1,40 @@
|
||||
# 2026-06-09 — 服务层 ValueError 中文化(str(exc) 透传源头)
|
||||
|
||||
## 摘要
|
||||
|
||||
将经 API `detail=str(exc)` 透传、且原先为英文的 `ValueError` 文案改为中文,避免 sync/scripts/批量/调度等接口在未走全局 `http_errors_zh` 映射时仍显示英文。
|
||||
|
||||
## 动机
|
||||
|
||||
待办 #1(API 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` 全局处理,待部署)
|
||||
@@ -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 不能为空")
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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]:
|
||||
|
||||
Reference in New Issue
Block a user