feat: 服务层配置接线——email/push/rate_limit/AI凭据/行为开关/映射表/种子键

Task 9: 服务层对全局 settings 的业务引用清零,每个 DEFAULT_CONFIG 组
都有真实消费方。resolve_*_settings(email/push/ai)以空槽回退 env
(平台默认层);contact 行为开关/种子白名单/国名扩表/限流上限经
manager 的 runtime.config 接线。收口: lead_push session_factory 必填、
config.py 死代码清理、PromptProvider frozen、no_contact 视图直取、
AIService wall 参数定型 CompiledWall。403 测试全绿(389+14)。

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-10-12 21:37:48 +08:00
parent 8453d3cc56
commit 32b582aac8
15 changed files with 676 additions and 164 deletions
+48 -16
View File
@@ -1,12 +1,14 @@
import json
import logging
from collections.abc import AsyncGenerator
from dataclasses import dataclass
from pathlib import Path
from app.engine.providers.base import AIProvider
from app.engine.providers.glm import GLMProvider
from app.engine.config import settings
from app.engine.services.boundary_filter import DEFAULT_WALL
from app.engine.wall_schema import CompiledWall
logger = logging.getLogger(__name__)
@@ -45,9 +47,13 @@ def _format_deflection_pool(deflections) -> str:
)
@dataclass(frozen=True)
class PromptProvider:
"""租户提示词四件套持有者(Task 6, R1 话术单一事实源)。
frozen(Task 9 收口, Task 6 审查遗留): 实例跨租户运行时/模块级
DEFAULT_PROVIDER 共享, 不可变防共享单例被毒化。
- from_dir(dir): 读 system/company_facts/extractor/evaluator 四个 md,
存实例字段——提示词属于租户(seed_profiles/<slug>/prompts/),不再有
代码级 app/engine/prompts 副本;
@@ -57,13 +63,10 @@ class PromptProvider:
- build_extractor/build_evaluator: $existing_info/$messages/$profile/
$recent_messages 替换逻辑自 AIService 原样搬入(行为等价)。
"""
def __init__(self, system: str, company_facts: str,
extractor: str, evaluator: str):
self.system = system
self.company_facts = company_facts
self.extractor = extractor
self.evaluator = evaluator
system: str
company_facts: str
extractor: str
evaluator: str
@classmethod
def from_dir(cls, prompts_dir) -> "PromptProvider":
@@ -100,25 +103,54 @@ DEFAULT_PROVIDER = PromptProvider.from_dir(
Path(__file__).resolve().parents[3] / "seed_profiles" / "jiancai" / "prompts")
def create_provider() -> AIProvider:
if settings.AI_PROVIDER == "glm":
# model 的出厂默认值——视同"未设置"哨兵: 租户未定制(merged 值 == 出厂默认)
# 时回退 env AI_MODEL,部署级模型选择不因接线失效。显式配了其他值则用租户的。
_FACTORY_DEFAULT_MODEL = "glm-4-flash"
def resolve_ai_settings(config_data: dict | None) -> dict:
"""租户 config.ai → provider 实例化参数(Task 9 接线; env 回退)。
api_key/base_url 空槽(None/空串)→ env 智谱凭据(平台默认集团 key,
spec §5.5 "用平台默认"状态下租户侧不存密钥);model 空或等于出厂默认
→ env AI_MODEL。
"""
ai = ((config_data or {}).get("ai")) or {}
model = ai.get("model")
if not model or model == _FACTORY_DEFAULT_MODEL:
model = settings.AI_MODEL or _FACTORY_DEFAULT_MODEL
return {
"provider": ai.get("provider") or settings.AI_PROVIDER,
"api_key": ai.get("api_key") or settings.ZHIPU_API_KEY,
"base_url": ai.get("base_url") or settings.ZHIPU_BASE_URL,
"model": model,
}
def create_provider(config_data: dict | None = None) -> AIProvider:
"""provider 工厂(Task 9: 按租户配置实例化 glm;None 走 env 平台默认)。"""
cfg = resolve_ai_settings(config_data)
if cfg["provider"] == "glm":
return GLMProvider(
api_key=settings.ZHIPU_API_KEY,
model=settings.AI_MODEL,
base_url=settings.ZHIPU_BASE_URL,
api_key=cfg["api_key"],
model=cfg["model"],
base_url=cfg["base_url"],
)
raise ValueError(f"Unknown AI provider: {settings.AI_PROVIDER}")
raise ValueError(f"Unknown AI provider: {cfg['provider']}")
class AIService:
def __init__(self, provider: AIProvider | None = None,
prompts: PromptProvider | None = None,
wall_deflections: dict | None = None):
self.provider = provider or create_provider()
wall: CompiledWall | None = None,
config_data: dict | None = None):
self.provider = provider or create_provider(config_data)
# Task 6: 提示词经 PromptProvider 注入(租户四件套);默认 = jiancai
# 出厂 + 出厂墙话术池(与 DEFAULT_PROVIDER 同源,单一事实源 wall.json)
self.prompts = prompts or DEFAULT_PROVIDER
self.wall_deflections = (wall_deflections if wall_deflections is not None
# Task 9 收口: wall 参数定型为 CompiledWall(话术池只取 .deflections,
# 与 prompts 同为租户运行时产物);缺省 = jiancai 出厂墙
self.wall_deflections = (wall.deflections if wall is not None
else DEFAULT_WALL.deflections)
# 对话基座: 行为 prompt 在前(红线显著性),事实卡居中(参考资料),
# 当轮 context 提醒最后拼接(在 chat/stream_chat 内,保持最新鲜)