431 lines
14 KiB
Python
431 lines
14 KiB
Python
|
|
# -*- coding: utf-8 -*-
|
|||
|
|
"""
|
|||
|
|
资产推荐服务 (Asset Recommend Service)
|
|||
|
|
|
|||
|
|
用于右侧栏智能推荐的资产信息管理:
|
|||
|
|
- 关键词匹配资产(L1)
|
|||
|
|
- 画像触发运维提醒(L2)
|
|||
|
|
- 角色通用资源(L3)
|
|||
|
|
|
|||
|
|
支持 YAML 配置文件热重载
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
import os
|
|||
|
|
import json
|
|||
|
|
import logging
|
|||
|
|
import yaml
|
|||
|
|
from pathlib import Path
|
|||
|
|
from typing import List, Dict, Optional, Any
|
|||
|
|
from datetime import datetime
|
|||
|
|
from dataclasses import dataclass, asdict
|
|||
|
|
|
|||
|
|
logger = logging.getLogger(__name__)
|
|||
|
|
|
|||
|
|
|
|||
|
|
@dataclass
|
|||
|
|
class RecommendItem:
|
|||
|
|
"""推荐操作项"""
|
|||
|
|
label: str
|
|||
|
|
type: str # download, approval, info, doc, guide, link, contact
|
|||
|
|
url: Optional[str] = None
|
|||
|
|
value: Optional[str] = None
|
|||
|
|
copyable: Optional[bool] = False
|
|||
|
|
description: Optional[str] = None
|
|||
|
|
approval_type: Optional[str] = None
|
|||
|
|
|
|||
|
|
|
|||
|
|
@dataclass
|
|||
|
|
class RecommendCard:
|
|||
|
|
"""推荐卡片"""
|
|||
|
|
id: str
|
|||
|
|
layer: str # L1, L2, L3
|
|||
|
|
layer_label: str
|
|||
|
|
source: str # dify_intent, keyword_assets, profile_trigger, role_assets
|
|||
|
|
title: str
|
|||
|
|
description: Optional[str] = None
|
|||
|
|
icon: Optional[str] = None
|
|||
|
|
items: Optional[List[Dict]] = None
|
|||
|
|
action_url: Optional[str] = None
|
|||
|
|
action_label: Optional[str] = None
|
|||
|
|
confidence: Optional[float] = None
|
|||
|
|
relevance: str = "high" # high, low
|
|||
|
|
priority: int = 50
|
|||
|
|
extra: Optional[Dict] = None
|
|||
|
|
|
|||
|
|
|
|||
|
|
class AssetRecommendService:
|
|||
|
|
"""
|
|||
|
|
资产推荐服务
|
|||
|
|
|
|||
|
|
功能:
|
|||
|
|
1. 从 YAML 配置文件加载资产数据
|
|||
|
|
2. 关键词匹配查询
|
|||
|
|
3. 画像触发规则匹配
|
|||
|
|
4. 角色资源查询
|
|||
|
|
5. 配置热重载
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
_instance = None
|
|||
|
|
|
|||
|
|
def __new__(cls, *args, **kwargs):
|
|||
|
|
"""单例模式"""
|
|||
|
|
if cls._instance is None:
|
|||
|
|
cls._instance = super().__new__(cls)
|
|||
|
|
cls._instance._initialized = False
|
|||
|
|
return cls._instance
|
|||
|
|
|
|||
|
|
def __init__(self, config_path: str = None):
|
|||
|
|
if self._initialized:
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
self._initialized = True
|
|||
|
|
|
|||
|
|
# 配置路径
|
|||
|
|
self.config_path = config_path or os.environ.get(
|
|||
|
|
'ASSETS_CONFIG_PATH',
|
|||
|
|
str(Path(__file__).parent.parent / 'config' / 'assets.yaml')
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# 数据存储
|
|||
|
|
self.keyword_assets: Dict[str, Dict] = {}
|
|||
|
|
self.role_assets: Dict[str, Dict] = {}
|
|||
|
|
self._alias_map: Dict[str, str] = {}
|
|||
|
|
|
|||
|
|
# 加载配置
|
|||
|
|
self.load()
|
|||
|
|
|
|||
|
|
logger.info(f"[AssetRecommend] 服务初始化完成,配置路径: {self.config_path}")
|
|||
|
|
|
|||
|
|
def load(self) -> None:
|
|||
|
|
"""加载配置文件"""
|
|||
|
|
try:
|
|||
|
|
if not os.path.exists(self.config_path):
|
|||
|
|
logger.warning(f"[AssetRecommend] 配置文件不存在: {self.config_path}")
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
with open(self.config_path, 'r', encoding='utf-8') as f:
|
|||
|
|
config = yaml.safe_load(f)
|
|||
|
|
|
|||
|
|
if not config:
|
|||
|
|
logger.warning("[AssetRecommend] 配置文件为空")
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
# 加载关键词资产
|
|||
|
|
keyword_assets = config.get('keyword_assets', {})
|
|||
|
|
self._build_keyword_assets(keyword_assets)
|
|||
|
|
|
|||
|
|
# 加载角色资产
|
|||
|
|
self.role_assets = config.get('role_assets', {})
|
|||
|
|
|
|||
|
|
logger.info(
|
|||
|
|
f"[AssetRecommend] 配置加载完成: "
|
|||
|
|
f"关键词资产 {len(self.keyword_assets)} 个, "
|
|||
|
|
f"角色资产 {len(self.role_assets)} 个"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
logger.error(f"[AssetRecommend] 配置加载失败: {e}")
|
|||
|
|
|
|||
|
|
def _build_keyword_assets(self, keyword_assets: Dict) -> None:
|
|||
|
|
"""构建关键词资产映射,处理别名"""
|
|||
|
|
self.keyword_assets = {}
|
|||
|
|
self._alias_map = {}
|
|||
|
|
|
|||
|
|
for keyword, asset in keyword_assets.items():
|
|||
|
|
# 处理别名
|
|||
|
|
if 'alias_of' in asset:
|
|||
|
|
self._alias_map[keyword] = asset['alias_of']
|
|||
|
|
continue
|
|||
|
|
|
|||
|
|
# 存储资产
|
|||
|
|
self.keyword_assets[keyword] = asset
|
|||
|
|
|
|||
|
|
def reload(self) -> None:
|
|||
|
|
"""热重载配置"""
|
|||
|
|
logger.info("[AssetRecommend] 开始热重载配置")
|
|||
|
|
self.load()
|
|||
|
|
logger.info("[AssetRecommend] 配置热重载完成")
|
|||
|
|
|
|||
|
|
def get_by_keyword(self, keyword: str) -> Optional[Dict]:
|
|||
|
|
"""
|
|||
|
|
根据关键词查询资产
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
keyword: 关键词(如 "vpn"、"打印机")
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
资产信息字典,未找到返回 None
|
|||
|
|
"""
|
|||
|
|
# 查别名
|
|||
|
|
keyword = self._alias_map.get(keyword, keyword)
|
|||
|
|
|
|||
|
|
# 查资产
|
|||
|
|
asset = self.keyword_assets.get(keyword)
|
|||
|
|
|
|||
|
|
if asset:
|
|||
|
|
logger.debug(f"[AssetRecommend] 命中关键词: {keyword}")
|
|||
|
|
|
|||
|
|
return asset
|
|||
|
|
|
|||
|
|
def match_keywords(self, message: str) -> List[RecommendCard]:
|
|||
|
|
"""
|
|||
|
|
从消息中匹配关键词,返回推荐卡片列表
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
message: 用户消息
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
推荐卡片列表
|
|||
|
|
"""
|
|||
|
|
cards = []
|
|||
|
|
message_lower = message.lower()
|
|||
|
|
|
|||
|
|
for keyword, asset in self.keyword_assets.items():
|
|||
|
|
if keyword in message_lower:
|
|||
|
|
card = self._build_card_from_asset(keyword, asset, source='keyword_assets')
|
|||
|
|
cards.append(card)
|
|||
|
|
logger.debug(f"[AssetRecommend] 消息匹配关键词: {keyword}")
|
|||
|
|
|
|||
|
|
return cards
|
|||
|
|
|
|||
|
|
def get_by_role(self, role: str) -> List[RecommendCard]:
|
|||
|
|
"""
|
|||
|
|
根据角色获取推荐卡片
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
role: 角色标识(如 "new_employee"、"developer")
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
推荐卡片列表
|
|||
|
|
"""
|
|||
|
|
cards = []
|
|||
|
|
|
|||
|
|
# 精确匹配
|
|||
|
|
if role in self.role_assets:
|
|||
|
|
asset = self.role_assets[role]
|
|||
|
|
card = self._build_card_from_asset(role, asset, source='role_assets')
|
|||
|
|
cards.append(card)
|
|||
|
|
|
|||
|
|
# 模糊匹配(角色名包含)
|
|||
|
|
for role_key, asset in self.role_assets.items():
|
|||
|
|
if role_key in role or role in role_key:
|
|||
|
|
if role_key != role: # 避免重复
|
|||
|
|
card = self._build_card_from_asset(role_key, asset, source='role_assets')
|
|||
|
|
cards.append(card)
|
|||
|
|
|
|||
|
|
return cards
|
|||
|
|
|
|||
|
|
def _build_card_from_asset(
|
|||
|
|
self,
|
|||
|
|
keyword: str,
|
|||
|
|
asset: Dict,
|
|||
|
|
source: str,
|
|||
|
|
layer: str = 'L1',
|
|||
|
|
layer_label: str = '相关推荐'
|
|||
|
|
) -> RecommendCard:
|
|||
|
|
"""从资产数据构建推荐卡片"""
|
|||
|
|
|
|||
|
|
import uuid
|
|||
|
|
|
|||
|
|
return RecommendCard(
|
|||
|
|
id=f"rec_{uuid.uuid4().hex[:8]}",
|
|||
|
|
layer=layer,
|
|||
|
|
layer_label=layer_label,
|
|||
|
|
source=source,
|
|||
|
|
title=asset.get('title', ''),
|
|||
|
|
description=asset.get('description'),
|
|||
|
|
icon=asset.get('icon'),
|
|||
|
|
items=asset.get('items', []),
|
|||
|
|
relevance='high' if layer == 'L1' else 'low',
|
|||
|
|
priority=80 if layer == 'L1' else 50
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
def build_ws_message(self, recommends: List[RecommendCard]) -> Dict:
|
|||
|
|
"""
|
|||
|
|
构建 WebSocket 推送消息
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
recommends: 推荐卡片列表
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
WS 消息字典
|
|||
|
|
"""
|
|||
|
|
return {
|
|||
|
|
'type': 'asset_recommend',
|
|||
|
|
'data': {
|
|||
|
|
'recommends': [asdict(rec) for rec in recommends],
|
|||
|
|
'layered': True
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
def match_profile_triggers(self, profile: Dict) -> List[RecommendCard]:
|
|||
|
|
"""
|
|||
|
|
画像触发规则匹配
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
profile: 员工画像数据
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
推荐卡片列表(L2)
|
|||
|
|
"""
|
|||
|
|
cards = []
|
|||
|
|
|
|||
|
|
# 1. 火绒终端版本过旧
|
|||
|
|
huorong_version = profile.get('huorong_version', '')
|
|||
|
|
if huorong_version:
|
|||
|
|
try:
|
|||
|
|
current = float(huorong_version.replace('5.0.', ''))
|
|||
|
|
if current < 73: # 假设最新版本是 5.0.73
|
|||
|
|
cards.append(RecommendCard(
|
|||
|
|
id=f"rec_huorong_version_{datetime.now().strftime('%Y%m%d')}",
|
|||
|
|
layer='L2',
|
|||
|
|
layer_label='运维提醒',
|
|||
|
|
source='profile_trigger',
|
|||
|
|
title='火绒终端版本过旧',
|
|||
|
|
description=f'您的火绒终端版本为 {huorong_version},最新版本为 5.0.73,建议更新',
|
|||
|
|
icon='🔶',
|
|||
|
|
action_url='/itportal/resource/huorong-update',
|
|||
|
|
relevance='low',
|
|||
|
|
priority=90
|
|||
|
|
))
|
|||
|
|
except (ValueError, AttributeError):
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
# 2. 火绒病毒库过期
|
|||
|
|
virusdb_date = profile.get('huorong_virusdb_date')
|
|||
|
|
if virusdb_date:
|
|||
|
|
try:
|
|||
|
|
if isinstance(virusdb_date, str):
|
|||
|
|
virusdb_date = datetime.fromisoformat(virusdb_date.replace('Z', '+00:00'))
|
|||
|
|
days_since = (datetime.now() - virusdb_date.replace(tzinfo=None)).days
|
|||
|
|
if days_since > 7:
|
|||
|
|
cards.append(RecommendCard(
|
|||
|
|
id=f"rec_virusdb_{datetime.now().strftime('%Y%m%d')}",
|
|||
|
|
layer='L2',
|
|||
|
|
layer_label='运维提醒',
|
|||
|
|
source='profile_trigger',
|
|||
|
|
title='火绒病毒库过期',
|
|||
|
|
description=f'病毒库已 {days_since} 天未更新,建议立即升级',
|
|||
|
|
icon='🦠',
|
|||
|
|
action_url='/itportal/resource/huorong-update',
|
|||
|
|
relevance='low',
|
|||
|
|
priority=80
|
|||
|
|
))
|
|||
|
|
except (ValueError, AttributeError):
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
# 3. 终端防护离线
|
|||
|
|
offline_days = profile.get('huorong_offline_days', 0)
|
|||
|
|
if offline_days > 3:
|
|||
|
|
cards.append(RecommendCard(
|
|||
|
|
id=f"rec_offline_{datetime.now().strftime('%Y%m%d')}",
|
|||
|
|
layer='L2',
|
|||
|
|
layer_label='运维提醒',
|
|||
|
|
source='profile_trigger',
|
|||
|
|
title='终端防护离线',
|
|||
|
|
description=f'火绒终端已离线 {offline_days} 天,请检查网络连接',
|
|||
|
|
icon='📡',
|
|||
|
|
action_url='/itportal/resource/huorong-reconnect',
|
|||
|
|
relevance='low',
|
|||
|
|
priority=100 # 最高优先级
|
|||
|
|
))
|
|||
|
|
|
|||
|
|
# 4. 系统补丁缺失
|
|||
|
|
missing_patches = profile.get('unionsoft_patches_missing', 0)
|
|||
|
|
if missing_patches > 10:
|
|||
|
|
cards.append(RecommendCard(
|
|||
|
|
id=f"rec_patches_{datetime.now().strftime('%Y%m%d')}",
|
|||
|
|
layer='L2',
|
|||
|
|
layer_label='运维提醒',
|
|||
|
|
source='profile_trigger',
|
|||
|
|
title='系统补丁缺失',
|
|||
|
|
description=f'您的电脑有 {missing_patches} 个安全补丁未安装,可能存在安全风险',
|
|||
|
|
icon='🔧',
|
|||
|
|
action_url='/itportal/resource/windows-update',
|
|||
|
|
relevance='low',
|
|||
|
|
priority=90
|
|||
|
|
))
|
|||
|
|
|
|||
|
|
# 5. 联软违规项
|
|||
|
|
violations = profile.get('unionsoft_violations', [])
|
|||
|
|
if violations:
|
|||
|
|
cards.append(RecommendCard(
|
|||
|
|
id=f"rec_violations_{datetime.now().strftime('%Y%m%d')}",
|
|||
|
|
layer='L2',
|
|||
|
|
layer_label='运维提醒',
|
|||
|
|
source='profile_trigger',
|
|||
|
|
title='安全策略违规',
|
|||
|
|
description=f'检测到 {len(violations)} 项违规项',
|
|||
|
|
icon='⚠️',
|
|||
|
|
action_url='/itportal/resource/security-compliance',
|
|||
|
|
relevance='low',
|
|||
|
|
priority=95,
|
|||
|
|
extra={'violations': violations}
|
|||
|
|
))
|
|||
|
|
|
|||
|
|
return cards
|
|||
|
|
|
|||
|
|
def process_dify_intent(
|
|||
|
|
self,
|
|||
|
|
dify_result: Dict,
|
|||
|
|
employee_id: str,
|
|||
|
|
profile: Optional[Dict] = None
|
|||
|
|
) -> List[RecommendCard]:
|
|||
|
|
"""
|
|||
|
|
处理 Dify 意图输出,生成资产推荐
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
dify_result: Dify 返回的 JSON(包含 intent, need_asset, asset_keywords)
|
|||
|
|
employee_id: 员工 ID
|
|||
|
|
profile: 员工画像(可选)
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
推荐卡片列表
|
|||
|
|
"""
|
|||
|
|
recommends = []
|
|||
|
|
|
|||
|
|
# L1: Dify 意图触发的资产
|
|||
|
|
if dify_result.get('need_asset') and dify_result.get('asset_keywords'):
|
|||
|
|
for keyword in dify_result['asset_keywords']:
|
|||
|
|
asset = self.get_by_keyword(keyword)
|
|||
|
|
if asset:
|
|||
|
|
card = self._build_card_from_asset(
|
|||
|
|
keyword,
|
|||
|
|
asset,
|
|||
|
|
source='dify_intent',
|
|||
|
|
layer='L1',
|
|||
|
|
layer_label='相关推荐'
|
|||
|
|
)
|
|||
|
|
card.confidence = dify_result.get('confidence', 0.85)
|
|||
|
|
recommends.append(card)
|
|||
|
|
|
|||
|
|
# L2: 画像触发(如果提供了 profile)
|
|||
|
|
if profile:
|
|||
|
|
profile_recs = self.match_profile_triggers(profile)
|
|||
|
|
recommends.extend(profile_recs)
|
|||
|
|
|
|||
|
|
# L3: 角色通用(如果提供了 profile)
|
|||
|
|
if profile:
|
|||
|
|
role = profile.get('position') or profile.get('role', '')
|
|||
|
|
if role:
|
|||
|
|
role_recs = self.get_by_role(role)
|
|||
|
|
for rec in role_recs:
|
|||
|
|
rec.layer = 'L3'
|
|||
|
|
rec.layer_label = '常用资源'
|
|||
|
|
rec.relevance = 'low'
|
|||
|
|
recommends.extend(role_recs)
|
|||
|
|
|
|||
|
|
return recommends
|
|||
|
|
|
|||
|
|
|
|||
|
|
# 全局单例
|
|||
|
|
_asset_recommend_service: Optional[AssetRecommendService] = None
|
|||
|
|
|
|||
|
|
|
|||
|
|
def get_asset_recommend_service() -> AssetRecommendService:
|
|||
|
|
"""获取资产推荐服务单例"""
|
|||
|
|
global _asset_recommend_service
|
|||
|
|
if _asset_recommend_service is None:
|
|||
|
|
_asset_recommend_service = AssetRecommendService()
|
|||
|
|
return _asset_recommend_service
|