From eded5896cd770ade375617ed3e09e7852165958f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8D=BF=E4=BA=91=E7=A7=8B=E6=9C=88?= <15273589815@163.com> Date: Mon, 14 Sep 2026 01:35:34 +0800 Subject: [PATCH] =?UTF-8?q?fix(admin):=20=E5=9B=9E=E5=A4=8D=E6=A8=A1?= =?UTF-8?q?=E6=9D=BF=E7=9A=84=20scene=20=E5=8A=A0=E6=9E=9A=E4=B8=BE?= =?UTF-8?q?=E6=A0=A1=E9=AA=8C=EF=BC=8C=E9=9D=9E=E6=B3=95=E5=80=BC=E4=B8=8D?= =?UTF-8?q?=E5=86=8D=E5=86=92=E6=88=90=20500?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 问题 `agent_reply_template.scene` 有数据库 CHECK 约束 `chk_template_scene`,只允许 `disclaimer` / `low_confidence` / `compliance_block` / `transfer` / `model_failure` / `system_busy` / `clarification` 七个值。 而 `ReplyPayload.scene` 只校验长度(`min_length=1, max_length=32`)—— 传一个不在列内的场景会**穿过接口校验、撞上数据库约束**,最终以 `500` 冒出: (3819, "Check constraint 'chk_template_scene' is violated.") 那本该是一次 `422` 参数校验失败。**500 与 422 的差别不只是状态码**: 前者会让调用方以为服务端故障、触发重试与告警,而实际是自己参数错了。 ## 改动 `ReplyPayload.scene` 改为 `Literal[...]`(新增 `ReplyScene` 类型别名), 取值与 `chk_template_scene` **逐字对齐**,并在注释里写明这个对齐关系与本次事故。 实测:非法 scene → `422 AGENT_INPUT_INVALID`(不再是 500);合法 scene → `201`。 ## 发现方式 这一处是**按接口逐条调用、逐个核对返回**时暴露的 —— 只看代码很难注意到 "schema 的宽松校验"与"数据库的严格约束"之间那道缝。同类风险仍存在: 凡是**表上有 CHECK 而 schema 只做长度校验**的字段,都有同样的 500 风险。 验证:unit+contract 1397 passed;integration 110 passed;ruff 通过; mypy 251 文件 0 错;e2e 冒烟 40/40。 --- app/api/schemas/admin.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/app/api/schemas/admin.py b/app/api/schemas/admin.py index 467ba5b..f1f602b 100644 --- a/app/api/schemas/admin.py +++ b/app/api/schemas/admin.py @@ -80,9 +80,25 @@ class IntentPayload(StrictPayload): version: int = Field(default=1, ge=1) +#: `agent_reply_template.chk_template_scene` 允许的取值。**必须逐字对齐**: +#: 此前 `ReplyPayload.scene` 只校验长度(`min_length=1, max_length=32`), +#: 于是传一个不在列内的场景会**穿过接口校验、撞上数据库 CHECK 约束**, +#: 最终以 `500` 冒出(`(3819, "Check constraint 'chk_template_scene' is violated.")`)—— +#: 那本该是一次 `422` 参数校验失败。2026-09-13 由接口逐条测试发现。 +ReplyScene = Literal[ + "disclaimer", + "low_confidence", + "compliance_block", + "transfer", + "model_failure", + "system_busy", + "clarification", +] + + class ReplyPayload(StrictPayload): template_code: str = Field(min_length=1, max_length=64) - scene: str = Field(min_length=1, max_length=32) + scene: ReplyScene title: str = Field(min_length=1, max_length=128) content_text: str = Field(min_length=1, max_length=32000) variables: list[str] = Field(default_factory=list)