# ============================================================================= # IT智能服务台 — 审批卡片匹配引擎 # ============================================================================= # 职责:统一审批匹配逻辑,替代前端 APPROVAL_OPTIONS 和后端分散的匹配代码 # # 匹配优先级: # 1. approval_type 精确匹配模板 ID(如 "zero_trust_vpn") # 2. approval_type 关键词匹配模板 keywords(如 "VPN账号申请" 含 "VPN") # 3. approval_type 匹配 category(如 "账号权限申请" → 返回该分类下所有模板) # 4. title 匹配模板 name(如 "VPN账号申请" → zero_trust_vpn) # 5. 降级:用户原始文本遍历所有 keywords # # 输出:标准化卡片数据 card_data,前端纯渲染 # ============================================================================= import logging from typing import Optional from app.api.approval import APPROVAL_TEMPLATES logger = logging.getLogger(__name__) class ApprovalMatcher: """审批卡片匹配器 — 后端统一匹配入口""" # ------------------------------------------------------------------ # 公开方法 # ------------------------------------------------------------------ def match_and_build_card( self, approval_type: Optional[str], title: Optional[str] = None ) -> Optional[dict]: """一站式匹配 + 构建标准化卡片数据(Dify 正常路径)。 Args: approval_type: Dify 返回的审批类型(可能是 ID、中文分类名或具体名称) title: Dify 返回的具体审批名称(如"VPN账号申请") Returns: 标准化 card_data,或 None(匹配失败) """ if not approval_type: return None # 优先级 1:精确 ID 匹配 template = self._match_by_id(approval_type) if template: return self._build_single_card(template) # 优先级 2:关键词匹配 template = self._match_by_keyword(approval_type) if template: return self._build_single_card(template) # 优先级 3:category 匹配 options = self._match_by_category(approval_type) if options: return self._build_multi_card(approval_type, options) # 优先级 4:title 匹配 if title: template = self._match_by_name(title) if template: return self._build_single_card(template) # 优先级 5:文本 keywords 兜底 template = self._match_by_keyword(approval_type) if template: return self._build_single_card(template) return None def match_by_keywords(self, user_text: str) -> Optional[dict]: """Dify 不可用时,用用户原始文本关键词降级匹配。 Args: user_text: 用户输入的原始文本 Returns: 标准化 card_data,或 None """ if not user_text: return None text_lower = user_text.lower() # 遍历所有模板的 keywords best_template = None best_score = 0 for template_id, template in APPROVAL_TEMPLATES.items(): keywords = template.get("keywords", []) score = 0 for kw in keywords: if kw.lower() in text_lower: score += len(kw) # 关键词越长,匹配越准确 if score > best_score: best_score = score best_template = template if best_template and best_score > 0: logger.info( f"[ApprovalMatcher] 关键词降级匹配: text={user_text[:30]} -> " f"template={best_template['id']}, score={best_score}" ) return self._build_single_card(best_template) return None # ------------------------------------------------------------------ # 内部匹配方法 # ------------------------------------------------------------------ def _match_by_id(self, approval_type: str) -> Optional[dict]: """精确匹配模板 ID(如 "zero_trust_vpn")。""" return APPROVAL_TEMPLATES.get(approval_type) def _match_by_keyword(self, approval_type: str) -> Optional[dict]: """通过关键词匹配模板。 遍历所有模板的 keywords,检查 approval_type 是否包含任一关键词。 使用最长匹配优先策略(避免"设备升级"误匹配到"升级")。 """ approval_lower = approval_type.lower() best_template = None best_len = 0 for template_id, template in APPROVAL_TEMPLATES.items(): keywords = template.get("keywords", []) for kw in keywords: if kw.lower() in approval_lower: if len(kw) > best_len: best_len = len(kw) best_template = template return best_template def _match_by_category(self, approval_type: str) -> list[dict]: """按 category 匹配,返回该分类下所有模板的卡片选项。""" result = [] for template_id, template in APPROVAL_TEMPLATES.items(): category = template.get("category", "") if category == approval_type: result.append(self._template_to_option(template)) return result def _match_by_name(self, title: str) -> Optional[dict]: """通过模板 name 精确/模糊匹配(空格容错)。""" normalized = title.replace(" ", "") # 去除空格容错 for template_id, template in APPROVAL_TEMPLATES.items(): name_clean = template["name"].replace(" ", "") if name_clean == normalized or normalized in name_clean: return template return None # ------------------------------------------------------------------ # 卡片数据构建 # ------------------------------------------------------------------ def _build_single_card(self, template: dict) -> dict: """构建单选项标准化卡片数据。""" return { "card_type": "single", "title": template["name"], "description": template.get("desc", ""), "options": [self._template_to_option(template)], } def _build_multi_card(self, category_name: str, options: list[dict]) -> dict: """构建多选项标准化卡片数据。""" return { "card_type": "multiple", "title": f"{category_name}({len(options)}项)", "description": "请选择具体审批类型", "options": options, } def _template_to_option(self, template: dict) -> dict: """将模板数据转换为前端卡片选项。""" return { "name": template["name"], "icon": template.get("icon", "orders-o"), "desc": template.get("desc", ""), "url": template.get("url", ""), "category": template.get("category", ""), } def get_all_categories(self) -> list[dict]: """获取所有分类及其选项(供前端全部展示用)。""" categories = {} for template_id, template in APPROVAL_TEMPLATES.items(): cat = template.get("category", "其他") if cat not in categories: categories[cat] = [] categories[cat].append(self._template_to_option(template)) result = [] for cat, options in categories.items(): result.append({ "category": cat, "options": options, }) return result def get_all_templates(self) -> list[dict]: """获取所有模板(含新增字段)。""" return [ {**template, "template_id": tid} for tid, template in APPROVAL_TEMPLATES.items() ] # ------------------------------------------------------------------ # 单例工厂 # ------------------------------------------------------------------ _approval_matcher: Optional[ApprovalMatcher] = None def get_approval_matcher() -> ApprovalMatcher: """获取 ApprovalMatcher 单例。""" global _approval_matcher if _approval_matcher is None: _approval_matcher = ApprovalMatcher() return _approval_matcher