import json import logging import os from pathlib import Path from typing import Any, Dict, List from utils.metrics_tracker import emit as metrics_emit logger = logging.getLogger("cs_agent") def load_system_inquiry_rules() -> Dict[str, Any]: """加载系统客服询单规则(全局 + 店铺覆盖)。""" from config.config import ( SYSTEM_INQUIRY_ENABLED, SYSTEM_INQUIRY_DEFAULT_ACTION, SYSTEM_INQUIRY_DEFAULT_REPLY, SYSTEM_INQUIRY_RULES_FILE, ) enabled_env = os.getenv("SYSTEM_INQUIRY_ENABLED") enabled = ( enabled_env.lower() in ("1", "true", "yes") if isinstance(enabled_env, str) else bool(SYSTEM_INQUIRY_ENABLED) ) action = (os.getenv("SYSTEM_INQUIRY_DEFAULT_ACTION") or SYSTEM_INQUIRY_DEFAULT_ACTION or "silent").strip().lower() reply = os.getenv("SYSTEM_INQUIRY_DEFAULT_REPLY") or SYSTEM_INQUIRY_DEFAULT_REPLY or "" rules_file = os.getenv("SYSTEM_INQUIRY_RULES_FILE") or str(SYSTEM_INQUIRY_RULES_FILE) defaults: Dict[str, Any] = { "enabled": bool(enabled), "default_action": action, "default_reply": reply, "sender_keywords": ["系统客服", "官方客服", "平台客服", "机器人客服", "商家客服系统"], "message_keywords": ["系统询单", "代客咨询", "平台代问", "系统代发", "客服询单"], "shops": {}, } try: p = Path(rules_file) if p.exists(): with p.open("r", encoding="utf-8") as f: loaded = json.load(f) if isinstance(loaded, dict): defaults.update(loaded) except Exception as e: logger.warning("系统询单规则加载失败,使用默认规则: %s", e) return defaults def normalize_kw_list(v: Any) -> List[str]: if not isinstance(v, list): return [] return [str(x).strip().lower() for x in v if str(x).strip()] def resolve_system_inquiry_policy(client, acc_id: str) -> Dict[str, Any]: """根据店铺合并系统询单策略。""" from config.config import SYSTEM_INQUIRY_SHOPS rules = client._system_inquiry_rules or {} if not bool(rules.get("enabled", True)): return {"enabled": False} shops_env = os.getenv("SYSTEM_INQUIRY_SHOPS", SYSTEM_INQUIRY_SHOPS or "") shop_whitelist = [s.strip() for s in shops_env.split(",") if s.strip()] if shop_whitelist and (acc_id or "") not in shop_whitelist: return {"enabled": False} policy: Dict[str, Any] = { "enabled": True, "action": str(rules.get("default_action", "silent")).strip().lower(), "reply": str(rules.get("default_reply", "")).strip(), "sender_keywords": normalize_kw_list(rules.get("sender_keywords")), "message_keywords": normalize_kw_list(rules.get("message_keywords")), } shop_cfg = (rules.get("shops") or {}).get(acc_id or "", {}) if isinstance(shop_cfg, dict): if "enabled" in shop_cfg and not bool(shop_cfg.get("enabled", True)): return {"enabled": False} if shop_cfg.get("action"): policy["action"] = str(shop_cfg.get("action")).strip().lower() if shop_cfg.get("reply"): policy["reply"] = str(shop_cfg.get("reply")).strip() if isinstance(shop_cfg.get("sender_keywords"), list): policy["sender_keywords"] = normalize_kw_list(shop_cfg.get("sender_keywords")) if isinstance(shop_cfg.get("message_keywords"), list): policy["message_keywords"] = normalize_kw_list(shop_cfg.get("message_keywords")) if policy["action"] not in ("silent", "reply", "transfer"): policy["action"] = "silent" return policy def match_system_inquiry(client, data: dict, policy: Dict[str, Any]) -> bool: """识别是否为系统客服询单消息。""" if not policy.get("enabled", False): return False from_name = client.to_chinese(data.get("from_name", "") or "").lower() from_id = str(data.get("from_id", "") or "").lower() msg = client.to_chinese(data.get("msg", "") or "").lower() sender_hits = 0 for kw in policy.get("sender_keywords", []): if kw and (kw in from_name or kw in from_id): sender_hits += 1 message_hits = 0 for kw in policy.get("message_keywords", []): if kw and kw in msg: message_hits += 1 # 优先看发送者特征;纯文本命中时至少要求两个关键词,降低误判风险 return sender_hits > 0 or message_hits >= 2 async def handle_system_inquiry(client, data: dict) -> bool: """命中系统询单后按策略处理。""" acc_id = data.get("acc_id", "") policy = resolve_system_inquiry_policy(client, acc_id) if not match_system_inquiry(client, data, policy): return False customer_id = data.get("from_id", "") metrics_emit("system_inquiry_detected", customer_id=customer_id, acc_id=acc_id) action = policy.get("action", "silent") logger.info("系统询单命中 | 店铺:%s | 客户:%s | action:%s", acc_id, customer_id, action) if action == "reply": reply = await client._compose_ai_scene_reply( original_msg=data, scene="system_inquiry_reply", intent_hint="这是系统客服询单消息,简短确认已收到并说明会跟进即可。", fallback=(policy.get("reply") or "您好,这边已收到询单消息,稍后由人工客服跟进处理。"), ) await client.send_reply(data, reply) metrics_emit("system_inquiry_auto_reply", customer_id=customer_id, acc_id=acc_id) return True if action == "transfer": await client.transfer_to_human(data, "系统询单转人工") metrics_emit("system_inquiry_transfer", customer_id=customer_id, acc_id=acc_id) return True metrics_emit("system_inquiry_ignored", customer_id=customer_id, acc_id=acc_id) return True