diff --git a/docs/changelog/2026-06-09-service-valueerror-zh.md b/docs/changelog/2026-06-09-service-valueerror-zh.md new file mode 100644 index 00000000..f4a45257 --- /dev/null +++ b/docs/changelog/2026-06-09-service-valueerror-zh.md @@ -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` 全局处理,待部署) diff --git a/server/api/schemas.py b/server/api/schemas.py index 753c87d9..5c94d3a4 100644 --- a/server/api/schemas.py +++ b/server/api/schemas.py @@ -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") diff --git a/server/application/services/script_service.py b/server/application/services/script_service.py index 9832a3a0..76e6bfda 100644 --- a/server/application/services/script_service.py +++ b/server/application/services/script_service.py @@ -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(): diff --git a/server/application/services/server_batch_service.py b/server/application/services/server_batch_service.py index c9397869..18ce3ef6 100644 --- a/server/application/services/server_batch_service.py +++ b/server/application/services/server_batch_service.py @@ -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 不能为空") diff --git a/server/infrastructure/database/crypto.py b/server/infrastructure/database/crypto.py index 8bd6d614..0c128afe 100644 --- a/server/infrastructure/database/crypto.py +++ b/server/infrastructure/database/crypto.py @@ -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 \ No newline at end of file + raise ValueError("凭据解密失败") from e \ No newline at end of file diff --git a/server/infrastructure/redis/script_execution_store.py b/server/infrastructure/redis/script_execution_store.py index a4bc6f03..ad55258b 100644 --- a/server/infrastructure/redis/script_execution_store.py +++ b/server/infrastructure/redis/script_execution_store.py @@ -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, diff --git a/server/infrastructure/ssh/asyncssh_pool.py b/server/infrastructure/ssh/asyncssh_pool.py index 4dc77abc..0efc353c 100644 --- a/server/infrastructure/ssh/asyncssh_pool.py +++ b/server/infrastructure/ssh/asyncssh_pool.py @@ -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) diff --git a/server/utils/schedule_cycle.py b/server/utils/schedule_cycle.py index ffef9f36..6f4e479b 100644 --- a/server/utils/schedule_cycle.py +++ b/server/utils/schedule_cycle.py @@ -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]: