"""平台配置的消费视图单入口,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(单一定义点纪律,旧定义已删)。 依赖方向: 本模块在 platform 层,只 import engine 的 env settings (app.engine.config,与各服务原状一致),无新循环;不得反向 import 服务 模块——resolver 是纯消费视图,不触服务逻辑。 """ from app.engine.config import settings # 平台默认发件人显示名(Task 9 前的硬编码值, 租户未配 sender_name 时沿用) _DEFAULT_SENDER_NAME = "HESUPIPE" # model 的出厂默认值——视同"未设置"哨兵: 租户未定制(merged 值 == 出厂默认) # 时回退 env AI_MODEL,部署级模型选择不因接线失效。显式配了其他值则用租户的。 _FACTORY_DEFAULT_MODEL = "glm-4-flash" def resolve_email_settings(config_data: dict | None) -> dict: """租户 config.email → 发信参数(Task 9 接线; env settings 回退)。 回退规则(语义钉在 test_config_merge: 显式 None = 清空/未设置): - smtp 缺省或显式 None → 整段用 env SMTP_*; - smtp 给了 dict → 段内逐键回退(缺键/空串 → env 对应值, 租户可只改 密码/只换 host); - sender_name 空 → 平台默认(_DEFAULT_SENDER_NAME); - notify_emails 空列表 → 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] return { "host": smtp.get("host") or settings.SMTP_HOST, "port": smtp.get("port") or settings.SMTP_PORT, "user": smtp.get("user") or settings.SMTP_USER, "password": smtp.get("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 回退)。 空槽回退哨兵(审查 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 实例化参数(Task 9 接线; env 回退)。 api_key/base_url 空槽(None/空串)→ env 智谱凭据(平台默认集团 key, spec §5.5 "用平台默认"状态下租户侧不存密钥);model 空或等于出厂默认 → env AI_MODEL。 已知边界: 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.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 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