f36803cf91
Co-Authored-By: Claude Code <noreply@anthropic.com>
187 lines
9.3 KiB
Python
187 lines
9.3 KiB
Python
"""平台配置的消费视图单入口,Phase 3 编辑器/试金石与这里对接。
|
||
|
||
Task 10(清理单 #3)收拢: 六个 resolver 原散在各服务模块——
|
||
resolve_email_settings ← email_service / resolve_push_settings ←
|
||
lead_push_service / resolve_ai_settings ← ai_service / resolve_contact_flags
|
||
+ context_seed_map ← conversation / name_to_region_extras ←
|
||
ConversationManager.__init__ 内联取值。函数体原样搬入,行为不变;服务层
|
||
与测试一律经本模块 import(单一定义点纪律,旧定义已删)。
|
||
|
||
Phase 4 Task 5A 起 email/ai 两组的回退链为三级: 租户槽位 → 平台设置快照
|
||
(settings_store,键名对齐 admin 平台设置页: smtp.host/smtp.port/smtp.user/
|
||
smtp.password 与 ai.api_key/ai.base_url/ai.model)→ env settings(env 键名
|
||
不变做最终兜底)。push 无平台推荐键(平台设置页只有 ai.*/smtp.* 两组),
|
||
保持租户槽位 → env 两级;contact_flags/context_seed_map/name_to_region_extras
|
||
无 env 兜底,不涉回退链。
|
||
|
||
依赖方向: 本模块在 platform 层,只 import engine 的 env settings
|
||
(app.engine.config,与各服务原状一致)与同层 settings_store,无新循环;
|
||
不得反向 import 服务模块——resolver 是纯消费视图,不触服务逻辑。
|
||
"""
|
||
from app.engine.config import settings
|
||
from app.platform import settings_store
|
||
|
||
# 平台默认发件人显示名(Task 9 前的硬编码值, 租户未配 sender_name 时沿用)
|
||
_DEFAULT_SENDER_NAME = "HESUPIPE"
|
||
|
||
# model 的出厂默认值——视同"未设置"哨兵: 租户未定制(merged 值 == 出厂默认)
|
||
# 时回退 env AI_MODEL,部署级模型选择不因接线失效。显式配了其他值则用租户的。
|
||
_FACTORY_DEFAULT_MODEL = "glm-4-flash"
|
||
|
||
|
||
def _platform_smtp_port():
|
||
"""平台设置快照的 smtp.port 取值(容错归一): int 原样;数字串转 int
|
||
(自由 KV 手工 API 可存字符串);其余(空串/None/非法)视同未设置 → None,
|
||
由 ``or`` 链条落到 env SMTP_PORT。"""
|
||
raw = settings_store.get("smtp.port")
|
||
if isinstance(raw, bool): # bool 是 int 子类,先排除
|
||
return None
|
||
if isinstance(raw, int):
|
||
return raw
|
||
if isinstance(raw, str) and raw.strip().lstrip("+").isdigit():
|
||
return int(raw.strip())
|
||
return None
|
||
|
||
|
||
def resolve_email_settings(config_data: dict | None) -> dict:
|
||
"""租户 config.email → 发信参数(三级回退: 租户槽位 → 平台设置快照
|
||
smtp.* → env SMTP_*)。
|
||
|
||
回退规则(语义钉在 test_config_merge: 显式 None = 清空/未设置):
|
||
- smtp 缺省或显式 None → 整段走 平台设置快照 → env;
|
||
- smtp 给了 dict → 段内逐键回退(缺键/空串 → 平台设置快照对应键 →
|
||
env 对应值, 租户可只改密码/只换 host);
|
||
- sender_name 空 → 平台默认(_DEFAULT_SENDER_NAME);
|
||
- notify_emails 空列表 → 租户 brand.email 单元素(用户 2026-10-21 要求:
|
||
默认取每个租户配置的站点邮箱) → env NOTIFY_EMAIL 单元素。
|
||
"""
|
||
email = ((config_data or {}).get("email")) or {}
|
||
smtp = email.get("smtp") or {}
|
||
notify = [e for e in (email.get("notify_emails") or []) if e]
|
||
if not notify:
|
||
# 回退 1:租户品牌配置里的 email(站点对外展示邮箱——客户留资通知
|
||
# 默认发到客户自己的联系邮箱,不需要每个租户显式配 notify_emails)
|
||
brand_email = ((config_data or {}).get("brand") or {}).get("email")
|
||
if brand_email:
|
||
notify = [brand_email]
|
||
return {
|
||
"host": (smtp.get("host")
|
||
or settings_store.get("smtp.host")
|
||
or settings.SMTP_HOST),
|
||
"port": (smtp.get("port")
|
||
or _platform_smtp_port()
|
||
or settings.SMTP_PORT),
|
||
"user": (smtp.get("user")
|
||
or settings_store.get("smtp.user")
|
||
or settings.SMTP_USER),
|
||
"password": (smtp.get("password")
|
||
or settings_store.get("smtp.password")
|
||
or settings.SMTP_PASS),
|
||
"sender_name": email.get("sender_name") or _DEFAULT_SENDER_NAME,
|
||
"notify_emails": notify or (
|
||
[settings.NOTIFY_EMAIL] if settings.NOTIFY_EMAIL else []),
|
||
}
|
||
|
||
|
||
def resolve_push_settings(config_data: dict | None) -> dict:
|
||
"""租户 config.push → 推送参数(Task 9 接线; env settings 回退)。
|
||
|
||
平台设置页无 push 推荐键(只有 ai.*/smtp.* 两组,Phase 4 Task 5A
|
||
接线范围),本组保持租户槽位 → env 两级回退。
|
||
|
||
空槽回退哨兵(审查 Important-1, 与 ai.model 同款纪律):
|
||
- 布尔/字符串: falsy 出厂默认(False/空串)视同"未设置"→ 回退 env
|
||
PUSH_*——部署级 .env 开关是平台默认集的一部分,租户未配置时继承,
|
||
不静默关停;
|
||
- 数值字段(timeout/max_attempts/retry_backoff_base): 出厂 None =
|
||
"未设置"→ 回退 env(settings 自带默认 10.0/3/1.0,最终行为与出厂
|
||
truthy 默认时一致),租户显式数值(含显式 0)优先于 env。
|
||
config_data=None 路径仅供测试直调,生产调用一律传 runtime.config.data。
|
||
已知边界: env 开 + 租户显式 false 无法区分于未配置(布尔空槽只有
|
||
False),Phase 2 admin 落地时以"平台清 env、配置显式写 false"解决。
|
||
"""
|
||
p = ((config_data or {}).get("push")) or {}
|
||
return {
|
||
"enabled": bool(p.get("enabled") or settings.PUSH_ENABLED),
|
||
"url": p.get("url") or settings.PUSH_API_URL,
|
||
"api_key": p.get("api_key") or settings.PUSH_API_KEY,
|
||
"prefix": p.get("prefix") or settings.PUSH_EXTERNAL_KEY_PREFIX,
|
||
# 数值哨兵: None(出厂/显式清空)→ env;显式数值优先
|
||
"timeout": (p.get("timeout") if p.get("timeout") is not None
|
||
else settings.PUSH_TIMEOUT_SECONDS),
|
||
"max_attempts": (p.get("max_attempts")
|
||
if p.get("max_attempts") is not None
|
||
else settings.PUSH_MAX_ATTEMPTS),
|
||
"retry_backoff_base": (p.get("retry_backoff_base")
|
||
if p.get("retry_backoff_base") is not None
|
||
else settings.PUSH_RETRY_BACKOFF_BASE),
|
||
}
|
||
|
||
|
||
def resolve_ai_settings(config_data: dict | None) -> dict:
|
||
"""租户 config.ai → provider 实例化参数(三级回退: 租户槽位 → 平台
|
||
设置快照 ai.* → env)。
|
||
|
||
api_key/base_url 空槽(None/空串)→ 平台设置快照 → env 智谱凭据
|
||
(平台默认集团 key,spec §5.5 "用平台默认"状态下租户侧不存密钥);
|
||
model 空或等于出厂默认 → 平台设置 ai.model(显式存储,不做出厂哨兵
|
||
检查)→ env AI_MODEL。provider 无平台键,租户槽位 → env AI_PROVIDER。
|
||
已知边界: provider 出厂 "glm" 同样遮 env AI_PROVIDER——当前单 provider
|
||
无实害,多 provider 接入时再改哨兵结构(与 push 数值同款纪律)。
|
||
config_data=None 路径仅供测试直调,生产调用一律传 runtime.config.data。
|
||
"""
|
||
ai = ((config_data or {}).get("ai")) or {}
|
||
model = ai.get("model")
|
||
if not model or model == _FACTORY_DEFAULT_MODEL:
|
||
model = (settings_store.get("ai.model")
|
||
or settings.AI_MODEL or _FACTORY_DEFAULT_MODEL)
|
||
return {
|
||
"provider": ai.get("provider") or settings.AI_PROVIDER,
|
||
"api_key": (ai.get("api_key")
|
||
or settings_store.get("ai.api_key")
|
||
or settings.ZHIPU_API_KEY),
|
||
"base_url": (ai.get("base_url")
|
||
or settings_store.get("ai.base_url")
|
||
or settings.ZHIPU_BASE_URL),
|
||
"model": model,
|
||
}
|
||
|
||
|
||
def resolve_contact_flags(config_data: dict | None) -> dict:
|
||
"""租户 config.contact → 联系方式行为开关。
|
||
|
||
cc_completion: 号码区号补全(判定树/确认状态机/CC 强制墙总闸);
|
||
email_light_ask: 邮箱轻补(缺失渠道一次性追问)。False = 该行为整体
|
||
跳过(引擎级门控: 不建状态/不注入/不写计数键)。
|
||
(template_languages 暂无运行期消费方, 不在此解析——Phase 2 后台批次
|
||
再接。)
|
||
"""
|
||
contact = ((config_data or {}).get("contact")) or {}
|
||
return {
|
||
"cc_completion": bool(contact.get("cc_completion", True)),
|
||
"email_light_ask": bool(contact.get("email_light_ask", True)),
|
||
}
|
||
|
||
|
||
def context_seed_map(config_data: dict | None) -> dict:
|
||
"""context 种子映射 = config.engine.context_seed_keys(ctx_key → raw_key)。
|
||
|
||
Task 9 前白名单(product_name/product_id)硬编码在 conversation——两租户
|
||
差异(auto 是 vehicle_*)由此配置化;create_conversation 是唯一消费口(按
|
||
映射值写 raw_info,白名单 = 本映射的键集合——审查 Minor-4: 单一取值
|
||
路径,不留第二条内联读法)。
|
||
"""
|
||
engine = ((config_data or {}).get("engine")) or {}
|
||
return dict(engine.get("context_seed_keys") or {})
|
||
|
||
|
||
def name_to_region_extras(config_data: dict | None) -> dict | None:
|
||
"""租户 misc.name_to_region_extras → CC 国名映射扩表(dict 或 None)。
|
||
|
||
Task 10 前是 ConversationManager.__init__ 的内联取值——``or None``
|
||
语义原样保留(缺省/空 dict → None),manager 构造期消费一次,cc 判定树
|
||
经 extras 参数逐轮取用(空表与 None 对查表方等价,均可 falsy 跳过)。
|
||
"""
|
||
misc = ((config_data or {}).get("misc")) or {}
|
||
return misc.get("name_to_region_extras") or None
|