fix(servers): server_ids 批量上限 2000 并修复列表 422 中文
全选 101 台批量操作不再因 max_length=50 被拒;Pydantic 列表 too_long 错误显示完整中文。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,25 @@
|
|||||||
|
# 审计 — server_ids 批量上限 2000
|
||||||
|
|
||||||
|
**Changelog**: `docs/changelog/2026-06-08-server-ids-batch-max-2000.md`
|
||||||
|
|
||||||
|
## 变更文件
|
||||||
|
|
||||||
|
`schemas.py` · `validation_errors_zh.py` · `test_validation_errors_zh.py` · `test_security_unit.py`
|
||||||
|
|
||||||
|
## Step 3
|
||||||
|
|
||||||
|
| 项 | 结论 |
|
||||||
|
|----|------|
|
||||||
|
| server_ids max 2000 与 Sync/Script 一致 | PASS |
|
||||||
|
| too_long 列表 ctx 中文完整 | PASS |
|
||||||
|
| 101 台 BatchAgentAction 校验通过 | PASS |
|
||||||
|
|
||||||
|
## Closure
|
||||||
|
|
||||||
|
Gate 7/7 PASS · 生产部署
|
||||||
|
|
||||||
|
## DoD
|
||||||
|
|
||||||
|
- [x] 101 台批量不再 422
|
||||||
|
- [x] 列表超长错误纯中文
|
||||||
|
- [x] 单测
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# 批量 server_ids 上限与 422 列表错误中文
|
||||||
|
|
||||||
|
**日期**:2026-06-08
|
||||||
|
|
||||||
|
## 变更摘要
|
||||||
|
|
||||||
|
- `BatchAgentAction` / `ServerCheck` / `BatchCategoryUpdate` 的 `server_ids` 上限由 50/200 统一为 **2000**(与推送/脚本一致)。
|
||||||
|
- 修复 Pydantic `too_long` 列表错误翻译残缺(「最多 50 项s after validation, not 101」→「最多 50 项,当前 101 项」)。
|
||||||
|
|
||||||
|
## 动机
|
||||||
|
|
||||||
|
全选筛选 101 台批量操作时触发 422;错误文案中英混杂。
|
||||||
|
|
||||||
|
## 涉及文件
|
||||||
|
|
||||||
|
- `server/api/schemas.py` — `SERVER_IDS_BATCH_MAX`
|
||||||
|
- `server/utils/validation_errors_zh.py` — `too_long`/`too_short` + ctx
|
||||||
|
- `tests/test_validation_errors_zh.py` · `tests/test_security_unit.py`
|
||||||
|
|
||||||
|
## 迁移 / 重启
|
||||||
|
|
||||||
|
后端部署即可。
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
- 101 台 `POST /servers/batch/detect-path` 应 200 入队
|
||||||
|
- 2001 台应 422 且 msg 为纯中文
|
||||||
@@ -16,6 +16,10 @@ from server.api.schema_path_validators import (
|
|||||||
|
|
||||||
# ── Setting ──
|
# ── Setting ──
|
||||||
|
|
||||||
|
# 与 SyncFiles / ScriptExecute 一致:批量选机上限
|
||||||
|
SERVER_IDS_BATCH_MAX = 2000
|
||||||
|
|
||||||
|
|
||||||
class SettingUpdatePayload(BaseModel):
|
class SettingUpdatePayload(BaseModel):
|
||||||
value: str = Field(..., min_length=0, description="设置值(字符串存储)")
|
value: str = Field(..., min_length=0, description="设置值(字符串存储)")
|
||||||
|
|
||||||
@@ -115,7 +119,7 @@ class ServerUpdate(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class ServerCheck(BaseModel):
|
class ServerCheck(BaseModel):
|
||||||
server_ids: List[int] = Field(..., min_length=1, max_length=50)
|
server_ids: List[int] = Field(..., min_length=1, max_length=SERVER_IDS_BATCH_MAX)
|
||||||
|
|
||||||
|
|
||||||
class ServerImportResult(BaseModel):
|
class ServerImportResult(BaseModel):
|
||||||
@@ -130,7 +134,7 @@ class ServerImportResult(BaseModel):
|
|||||||
|
|
||||||
class BatchAgentAction(BaseModel):
|
class BatchAgentAction(BaseModel):
|
||||||
"""Batch install/upgrade Agent on multiple servers."""
|
"""Batch install/upgrade Agent on multiple servers."""
|
||||||
server_ids: List[int] = Field(..., min_length=1, max_length=50)
|
server_ids: List[int] = Field(..., min_length=1, max_length=SERVER_IDS_BATCH_MAX)
|
||||||
|
|
||||||
|
|
||||||
class BatchAgentResultItem(BaseModel):
|
class BatchAgentResultItem(BaseModel):
|
||||||
@@ -147,7 +151,7 @@ class BatchAgentResult(BaseModel):
|
|||||||
|
|
||||||
class BatchCategoryUpdate(BaseModel):
|
class BatchCategoryUpdate(BaseModel):
|
||||||
"""Batch update server category for selected IDs."""
|
"""Batch update server category for selected IDs."""
|
||||||
server_ids: List[int] = Field(..., min_length=1, max_length=200)
|
server_ids: List[int] = Field(..., min_length=1, max_length=SERVER_IDS_BATCH_MAX)
|
||||||
category: str = Field("", max_length=100, description="分类名称;留空表示清除分类")
|
category: str = Field("", max_length=100, description="分类名称;留空表示清除分类")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -24,23 +24,37 @@ _MSG_EXACT: dict[str, str] = {
|
|||||||
"Input should be a valid email address": "必须是有效的邮箱",
|
"Input should be a valid email address": "必须是有效的邮箱",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Regex patterns for parameterized Pydantic messages
|
# Regex patterns for parameterized Pydantic messages (legacy / string fields)
|
||||||
_MSG_PATTERNS: list[tuple[re.Pattern[str], str]] = [
|
_MSG_PATTERNS: list[tuple[re.Pattern[str], str]] = [
|
||||||
(re.compile(r"^String should have at least (\d+) character"), r"至少需要 \1 个字符"),
|
(re.compile(r"^String should have at least (\d+) characters?$"), r"至少需要 \1 个字符"),
|
||||||
(re.compile(r"^String should have at most (\d+) character"), r"最多 \1 个字符"),
|
(re.compile(r"^String should have at most (\d+) characters?$"), r"最多 \1 个字符"),
|
||||||
(re.compile(r"^String should match pattern '(.+)'$"), r"格式不符合要求"),
|
(re.compile(r"^String should match pattern '(.+)'$"), r"格式不符合要求"),
|
||||||
(re.compile(r"^Input should be greater than or equal to (\d+)"), r"不能小于 \1"),
|
(re.compile(r"^Input should be greater than or equal to (\d+)"), r"不能小于 \1"),
|
||||||
(re.compile(r"^Input should be less than or equal to (\d+)"), r"不能大于 \1"),
|
(re.compile(r"^Input should be less than or equal to (\d+)"), r"不能大于 \1"),
|
||||||
(re.compile(r"^Input should be greater than (\d+)"), r"必须大于 \1"),
|
(re.compile(r"^Input should be greater than (\d+)"), r"必须大于 \1"),
|
||||||
(re.compile(r"^Input should be less than (\d+)"), r"必须小于 \1"),
|
(re.compile(r"^Input should be less than (\d+)"), r"必须小于 \1"),
|
||||||
(re.compile(r"^List should have at least (\d+) item"), r"至少需要 \1 项"),
|
(re.compile(r"^List should have at least (\d+) items?(?: .*)?$"), r"至少需要 \1 项"),
|
||||||
(re.compile(r"^List should have at most (\d+) item"), r"最多 \1 项"),
|
(re.compile(r"^List should have at most (\d+) items?(?: after validation, not (\d+))?$"), r"最多 \1 项"),
|
||||||
(re.compile(r"^Value error, (.+)$"), r"\1"),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def translate_validation_msg(msg: str, err_type: str = "") -> str:
|
def translate_validation_msg(msg: str, err_type: str = "", ctx: dict[str, Any] | None = None) -> str:
|
||||||
"""Return Chinese message when a known translation exists."""
|
"""Return Chinese message when a known translation exists."""
|
||||||
|
ctx = ctx or {}
|
||||||
|
if err_type == "too_long" and ctx.get("field_type") == "List":
|
||||||
|
max_len = ctx.get("max_length")
|
||||||
|
actual = ctx.get("actual_length")
|
||||||
|
if max_len is not None and actual is not None:
|
||||||
|
return f"最多 {max_len} 项,当前 {actual} 项"
|
||||||
|
if max_len is not None:
|
||||||
|
return f"最多 {max_len} 项"
|
||||||
|
if err_type == "too_short" and ctx.get("field_type") == "List":
|
||||||
|
min_len = ctx.get("min_length")
|
||||||
|
actual = ctx.get("actual_length")
|
||||||
|
if min_len is not None and actual is not None:
|
||||||
|
return f"至少需要 {min_len} 项,当前 {actual} 项"
|
||||||
|
if min_len is not None:
|
||||||
|
return f"至少需要 {min_len} 项"
|
||||||
if err_type == "missing":
|
if err_type == "missing":
|
||||||
return "必填"
|
return "必填"
|
||||||
if not msg:
|
if not msg:
|
||||||
@@ -49,7 +63,14 @@ def translate_validation_msg(msg: str, err_type: str = "") -> str:
|
|||||||
return _MSG_EXACT[msg]
|
return _MSG_EXACT[msg]
|
||||||
for pattern, repl in _MSG_PATTERNS:
|
for pattern, repl in _MSG_PATTERNS:
|
||||||
if pattern.match(msg):
|
if pattern.match(msg):
|
||||||
return pattern.sub(repl, msg)
|
out = pattern.sub(repl, msg)
|
||||||
|
# too_long list with "not N" suffix — append actual count when present
|
||||||
|
m = re.search(r"not (\d+)$", msg)
|
||||||
|
if m and "最多" in out and "当前" not in out:
|
||||||
|
return f"{out},当前 {m.group(1)} 项"
|
||||||
|
return out
|
||||||
|
if msg.startswith("Value error, "):
|
||||||
|
return msg[len("Value error, "):]
|
||||||
if err_type == "string_type":
|
if err_type == "string_type":
|
||||||
return "必须是字符串"
|
return "必须是字符串"
|
||||||
if err_type in ("int_type", "int_parsing"):
|
if err_type in ("int_type", "int_parsing"):
|
||||||
@@ -66,6 +87,10 @@ def translate_validation_errors(errors: list[dict[str, Any]]) -> list[dict[str,
|
|||||||
translated: list[dict[str, Any]] = []
|
translated: list[dict[str, Any]] = []
|
||||||
for err in errors:
|
for err in errors:
|
||||||
item = dict(err)
|
item = dict(err)
|
||||||
item["msg"] = translate_validation_msg(str(err.get("msg", "")), str(err.get("type", "")))
|
item["msg"] = translate_validation_msg(
|
||||||
|
str(err.get("msg", "")),
|
||||||
|
str(err.get("type", "")),
|
||||||
|
err.get("ctx") if isinstance(err.get("ctx"), dict) else None,
|
||||||
|
)
|
||||||
translated.append(item)
|
translated.append(item)
|
||||||
return translated
|
return translated
|
||||||
|
|||||||
@@ -21,14 +21,15 @@ def test_dangerous_command_safe_empty():
|
|||||||
assert check_dangerous_command("echo hello") == []
|
assert check_dangerous_command("echo hello") == []
|
||||||
|
|
||||||
|
|
||||||
def test_server_check_server_ids_max_fifty():
|
def test_server_check_server_ids_max_two_thousand():
|
||||||
from pydantic import ValidationError
|
from pydantic import ValidationError
|
||||||
|
|
||||||
from server.api.schemas import ServerCheck
|
from server.api.schemas import BatchAgentAction, ServerCheck
|
||||||
|
|
||||||
ServerCheck(server_ids=list(range(1, 51)))
|
ServerCheck(server_ids=list(range(1, 2001)))
|
||||||
|
BatchAgentAction(server_ids=list(range(1, 2001)))
|
||||||
with pytest.raises(ValidationError):
|
with pytest.raises(ValidationError):
|
||||||
ServerCheck(server_ids=list(range(1, 52)))
|
ServerCheck(server_ids=list(range(1, 2002)))
|
||||||
|
|
||||||
|
|
||||||
def test_sync_files_server_ids_max_two_thousand():
|
def test_sync_files_server_ids_max_two_thousand():
|
||||||
|
|||||||
@@ -31,6 +31,17 @@ def test_setting_update_payload_coerces_int():
|
|||||||
assert payload.value == "160"
|
assert payload.value == "160"
|
||||||
|
|
||||||
|
|
||||||
def test_setting_update_payload_coerces_float_whole():
|
def test_translate_list_too_long_ctx():
|
||||||
payload = SettingUpdatePayload.model_validate({"value": 80.0})
|
msg = translate_validation_msg(
|
||||||
assert payload.value == "80"
|
"List should have at most 50 items after validation, not 101",
|
||||||
|
"too_long",
|
||||||
|
{"field_type": "List", "max_length": 50, "actual_length": 101},
|
||||||
|
)
|
||||||
|
assert msg == "最多 50 项,当前 101 项"
|
||||||
|
|
||||||
|
|
||||||
|
def test_batch_agent_accepts_101_ids():
|
||||||
|
from server.api.schemas import BatchAgentAction
|
||||||
|
|
||||||
|
payload = BatchAgentAction.model_validate({"server_ids": list(range(1, 102))})
|
||||||
|
assert len(payload.server_ids) == 101
|
||||||
|
|||||||
Reference in New Issue
Block a user