Files

187 lines
9.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""平台配置的消费视图单入口,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