From 1bd341c21ac78ef01b0e3bae36a1bf7dec68c3fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=85=E5=BF=B5?= Date: Tue, 13 Oct 2026 09:05:09 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=95=B0=E6=8D=AE=E6=BA=90=E5=AE=A1?= =?UTF-8?q?=E6=9F=A5=E5=B0=8F=E4=BF=AE=EF=BC=88from=5Fconfig=20=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B=E5=AE=88=E5=8D=AB/PROMPTS=5FKEYS=20=E6=B6=88=E9=87=8D?= =?UTF-8?q?/=E5=A5=91=E7=BA=A6=E6=8E=AA=E8=BE=9E=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Code --- app/engine/services/ai_service.py | 8 +++++++- app/platform/tenant_source.py | 8 +++++--- tests/test_db_tenant_source.py | 6 ++---- tests/test_prompt_provider.py | 8 ++++++++ 4 files changed, 22 insertions(+), 8 deletions(-) diff --git a/app/engine/services/ai_service.py b/app/engine/services/ai_service.py index 68e40fe..dbd4401 100644 --- a/app/engine/services/ai_service.py +++ b/app/engine/services/ai_service.py @@ -80,7 +80,13 @@ class PromptProvider: def from_config(cls, md: dict) -> "PromptProvider": """快照 prompts 组 → PromptProvider。缺键/非字符串显性炸带键名 (DB 快照坏数据不留到 build_system 才炸)。""" - bad = [k for k in PROMPTS_KEYS if not isinstance((md or {}).get(k), str)] + if not isinstance(md, dict): + # 入口类型守卫(Task 6 的 PUT prompts 会接请求体): 字符串/ + # 列表进来给 ValueError,不让 .get 的 AttributeError 漏成 500 + raise ValueError( + f"prompts 必须是 dict: {{{', '.join(PROMPTS_KEYS)}}}: " + f"实际 {type(md).__name__}") + bad = [k for k in PROMPTS_KEYS if not isinstance(md.get(k), str)] if bad: raise ValueError(f"prompts 配置缺少(或非字符串)键: {', '.join(bad)}") return cls( diff --git a/app/platform/tenant_source.py b/app/platform/tenant_source.py index ba00fa8..f0906c8 100644 --- a/app/platform/tenant_source.py +++ b/app/platform/tenant_source.py @@ -16,10 +16,12 @@ registry 只认本模块的 TenantSource 协议(async load_all / load_tenant), ——旧路径 PromptProvider.from_dir 直读盘、不进快照;对齐后 DB 源(prompts 嵌 config_json)与文件源输出形态一致,registry 不再感知源差异。 -错误契约(两源一致,registry 依赖): +错误契约(registry 依赖,加载期显性失败不流入运行期): - 未知 slug → LookupError(registry_reload 映射为 UnknownTenantError); -- 数据坏(缺版本/坏 JSON/零站点/缺 wall·prompts 组)→ ValueError 带 - 租户名(加载期显性失败,不流入运行期)。 +- DB 源数据坏(缺版本/坏 JSON/非对象/零站点/缺 wall·prompts 组)→ + ValueError 带租户名(与版本号); +- 文件源数据坏:坏 JSON/slug 与目录不一致/零站点 → ValueError 带路径, + wall/prompts 文件缺失 → FileNotFoundError 带路径(read_text 原样)。 """ import json import pathlib diff --git a/tests/test_db_tenant_source.py b/tests/test_db_tenant_source.py index 6635e45..6d3ec87 100644 --- a/tests/test_db_tenant_source.py +++ b/tests/test_db_tenant_source.py @@ -20,7 +20,7 @@ import pytest from sqlalchemy import text from sqlalchemy.ext.asyncio import AsyncSession -from app.engine.services.ai_service import PromptProvider +from app.engine.services.ai_service import PROMPTS_KEYS, PromptProvider from app.platform.db import ( Site, SiteDomain, @@ -36,8 +36,6 @@ REPO_ROOT = Path(__file__).resolve().parents[1] SEED_DIR = REPO_ROOT / "seed_profiles" JC_DOMAIN = "db.jc.example.com" # DB 源测试专属域名(不与文件源种子撞) -_PROMPTS_KEYS = ("system", "company_facts", "extractor", "evaluator") - def _jiancai_snapshot(prefix="ai-kefu-jc"): """版本 config_json 构造助手:seed jiancai 的 config 组 + wall.json + @@ -51,7 +49,7 @@ def _jiancai_snapshot(prefix="ai-kefu-jc"): (SEED_DIR / "jiancai" / "wall.json").read_text(encoding="utf-8")) snapshot["prompts"] = { name: (prompts_dir / f"{name}.md").read_text(encoding="utf-8") - for name in _PROMPTS_KEYS + for name in PROMPTS_KEYS } return snapshot diff --git a/tests/test_prompt_provider.py b/tests/test_prompt_provider.py index 1f4891d..65251a6 100644 --- a/tests/test_prompt_provider.py +++ b/tests/test_prompt_provider.py @@ -153,6 +153,14 @@ def test_from_config_non_string_key_raises(): "extractor": "e", "evaluator": "v"}) +def test_from_config_non_dict_raises(): + # 入口类型守卫: Task 6 的 PUT prompts 会把请求体直传这里——字符串/ + # 列表进来必须是 ValueError,不能让 .get 的 AttributeError 漏成 500 + for bad in ("system=...", ["system", "company_facts"], 42, None): + with pytest.raises(ValueError, match="dict"): + PromptProvider.from_config(bad) + + def test_from_config_equals_from_dir(): d = JC_PROMPTS cfg = {name: (d / f"{name}.md").read_text(encoding="utf-8")