Files
wecom_it_smart_desk/backend/app/services/approval_matcher.py
T

228 lines
8.4 KiB
Python
Raw 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.
# =============================================================================
# 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)
# 优先级 3category 匹配
options = self._match_by_category(approval_type)
if options:
return self._build_multi_card(approval_type, options)
# 优先级 4title 匹配
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