chore: initial baseline with P0-safety .gitignore
This commit is contained in:
+33
@@ -0,0 +1,33 @@
|
||||
# =============================================================================
|
||||
# 企微IT智能服务台 — 外部系统集成模块
|
||||
# =============================================================================
|
||||
# 提供统一的适配层,让联软/火绒/aTrust/eHR用同一套接口规范接入。
|
||||
# 上层业务只依赖 ExternalSystemService 统一门面,不直接调用任何Adapter。
|
||||
#
|
||||
# 使用方式:
|
||||
# from app.services.external import ExternalSystemService, get_external_service
|
||||
# svc = get_external_service()
|
||||
# terminal = await svc.find_user_terminal("songxian")
|
||||
# =============================================================================
|
||||
|
||||
from app.services.external.base import (
|
||||
ExternalSystemAdapter,
|
||||
TerminalInfo,
|
||||
SecurityStatus,
|
||||
VpnSession,
|
||||
)
|
||||
from app.services.external.config import ExternalSystemConfig
|
||||
from app.services.external.cache import ExternalSystemCache
|
||||
from app.services.external.mock import MockAdapter
|
||||
from app.services.external.service import ExternalSystemService
|
||||
|
||||
__all__ = [
|
||||
"ExternalSystemAdapter",
|
||||
"TerminalInfo",
|
||||
"SecurityStatus",
|
||||
"VpnSession",
|
||||
"ExternalSystemConfig",
|
||||
"ExternalSystemCache",
|
||||
"MockAdapter",
|
||||
"ExternalSystemService",
|
||||
]
|
||||
+312
@@ -0,0 +1,312 @@
|
||||
# =============================================================================
|
||||
# 企微IT智能服务台 — 外部系统适配器抽象基类 + 统一数据模型
|
||||
# =============================================================================
|
||||
# 说明:
|
||||
# 1. 定义所有外部系统共用的抽象接口(ABC)
|
||||
# 2. 定义统一的DTO模型(TerminalInfo/SecurityStatus/VpnSession)
|
||||
# 3. 每个外部系统实现此接口,上层业务只依赖抽象接口
|
||||
#
|
||||
# 设计原则:
|
||||
# - 默认返回None/空 — 子类按需覆写自己支持的方法
|
||||
# - 不支持的能力不报错,返回None让调用方走降级逻辑
|
||||
# - raw_data字段保留原始响应,调试用,生产环境可关闭
|
||||
# =============================================================================
|
||||
|
||||
import logging
|
||||
from abc import ABC, abstractmethod
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# 统一数据模型(DTO)
|
||||
# =============================================================================
|
||||
|
||||
class TerminalInfo(BaseModel):
|
||||
"""统一终端信息模型 — 所有Adapter返回同一结构
|
||||
|
||||
做什么:把联软/火绒/aTrust不同格式的终端数据映射到统一结构
|
||||
为什么:上层业务代码不需要关心数据来自哪个系统
|
||||
"""
|
||||
|
||||
# ── 来源标识 ──
|
||||
source_system: str = Field(..., description="数据来源系统标识: lianruan/huorong/atrust/ehr")
|
||||
|
||||
# ── 基础标识 ──
|
||||
terminal_id: Optional[str] = Field(None, description="终端在来源系统中的唯一ID")
|
||||
computer_name: str = Field(..., description="计算机名")
|
||||
|
||||
# ── 网络信息 ──
|
||||
ip_addresses: List[str] = Field(default_factory=list, description="IP地址列表(含VPN虚拟IP)")
|
||||
mac_addresses: List[str] = Field(default_factory=list, description="MAC地址列表")
|
||||
|
||||
# ── 系统信息 ──
|
||||
os_version: Optional[str] = Field(None, description="操作系统版本")
|
||||
is_online: bool = Field(False, description="是否在线")
|
||||
|
||||
# ── 用户映射(核心字段)──
|
||||
logged_in_user: Optional[str] = Field(None, description="当前登录用户账号 — 映射核心字段")
|
||||
logged_in_user_name: Optional[str] = Field(None, description="用户姓名")
|
||||
department: Optional[str] = Field(None, description="所属部门")
|
||||
|
||||
# ── 硬件摘要 ──
|
||||
hardware_summary: Optional[Dict] = Field(None, description="硬件摘要(CPU/内存/磁盘使用率等)")
|
||||
|
||||
# ── 时间信息 ──
|
||||
last_seen: Optional[datetime] = Field(None, description="最后在线时间")
|
||||
|
||||
# ── 调试用 ──
|
||||
raw_data: Optional[Dict] = Field(None, description="原始响应数据(调试用,生产可关闭)")
|
||||
|
||||
|
||||
class VulnerabilityItem(BaseModel):
|
||||
"""漏洞条目"""
|
||||
name: str = Field(..., description="漏洞名称")
|
||||
level: str = Field("info", description="严重程度: critical/high/medium/low/info")
|
||||
description: Optional[str] = Field(None, description="漏洞描述")
|
||||
publish_time: Optional[str] = Field(None, description="发布时间")
|
||||
|
||||
|
||||
class SecurityStatus(BaseModel):
|
||||
"""统一安全状态模型
|
||||
|
||||
做什么:聚合火绒的病毒/漏洞/隔离数据
|
||||
为什么:坐席需要一目了然看到终端安全全貌
|
||||
"""
|
||||
|
||||
source_system: str = Field(..., description="数据来源系统标识")
|
||||
terminal_id: str = Field(..., description="终端ID")
|
||||
computer_name: Optional[str] = Field(None, description="计算机名")
|
||||
|
||||
# ── 安全指标 ──
|
||||
virus_total: int = Field(0, description="病毒事件总数")
|
||||
virus_uncleaned: int = Field(0, description="未处理病毒数")
|
||||
vulnerabilities: List[VulnerabilityItem] = Field(default_factory=list, description="高危漏洞列表")
|
||||
high_vuln_count: int = Field(0, description="高危漏洞数量")
|
||||
|
||||
# ── 隔离状态 ──
|
||||
is_isolated: bool = Field(False, description="是否被隔离")
|
||||
isolation_source: Optional[str] = Field(None, description="隔离来源系统")
|
||||
|
||||
# ── 检查时间 ──
|
||||
checked_at: datetime = Field(default_factory=datetime.now, description="检查时间")
|
||||
|
||||
|
||||
class VpnSession(BaseModel):
|
||||
"""VPN会话模型(仅aTrust)
|
||||
|
||||
做什么:描述一个aTrust VPN在线会话
|
||||
为什么:坐席需要知道远程员工是否通过VPN在线、VPN IP是什么
|
||||
"""
|
||||
|
||||
source_system: str = "atrust"
|
||||
session_id: Optional[str] = Field(None, description="会话ID(用于踢出操作)")
|
||||
username: str = Field(..., description="用户名(登录名)")
|
||||
display_name: Optional[str] = Field(None, description="显示名")
|
||||
remote_ip: str = Field(..., description="接入IP(公网IP或'内网IP')")
|
||||
vpn_ip: Optional[str] = Field(None, description="VPN虚拟内网IP — 火绒交叉匹配关键字段")
|
||||
is_trusted: bool = Field(False, description="终端是否已授信")
|
||||
os: Optional[str] = Field(None, description="接入终端操作系统")
|
||||
last_login: Optional[datetime] = Field(None, description="最后登录时间")
|
||||
domain: Optional[str] = Field(None, description="登录域")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# 适配器抽象基类
|
||||
# =============================================================================
|
||||
|
||||
class ExternalSystemAdapter(ABC):
|
||||
"""外部系统适配器抽象基类
|
||||
|
||||
做什么:定义所有外部系统共用的接口规范
|
||||
为什么:让上层业务代码只依赖抽象接口,不感知底层系统差异
|
||||
|
||||
设计原则:
|
||||
- 默认方法返回None/空列表/False,子类按需覆写自己支持的能力
|
||||
- 不支持的能力不报错,让调用方走降级逻辑
|
||||
- 每个Adapter只负责一个外部系统的对接
|
||||
"""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def system_name(self) -> str:
|
||||
"""系统标识名称
|
||||
|
||||
返回值: 'lianruan' / 'huorong' / 'atrust' / 'ehr' / 'mock'
|
||||
"""
|
||||
...
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def is_available(self) -> bool:
|
||||
"""当前系统是否可用(凭证已配置+网络可达)
|
||||
|
||||
做什么:检查配置是否完整,不实际发起网络请求
|
||||
为什么:调用方可据此决定是否跳过本系统
|
||||
"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
async def health_check(self) -> bool:
|
||||
"""健康检查 — 验证凭证和网络连通性
|
||||
|
||||
做什么:实际发起一次轻量级API调用,确认系统可达
|
||||
为什么:定期健康检查可提前发现连接问题
|
||||
"""
|
||||
...
|
||||
|
||||
# =========================================================================
|
||||
# 终端查询能力
|
||||
# =========================================================================
|
||||
|
||||
async def get_terminal_by_user(self, username: str) -> Optional[TerminalInfo]:
|
||||
"""通过员工账号查询终端信息(映射核心方法)
|
||||
|
||||
做什么:输入员工账号,返回该员工使用的终端信息
|
||||
为什么:这是员工→终端映射的核心入口
|
||||
|
||||
各系统实现方式:
|
||||
- 联软:queryDevByParams(strusername=xxx) — 精确匹配
|
||||
- 火绒:_list(ip=xxx) — 需配合联软IP交叉匹配
|
||||
- aTrust:queryAll(bindUserList) — 终端绑定用户
|
||||
- eHR:不提供终端数据,返回None
|
||||
|
||||
Args:
|
||||
username: 员工账号(如 'songxian')
|
||||
|
||||
Returns:
|
||||
TerminalInfo 或 None(系统不支持或未找到)
|
||||
"""
|
||||
return None
|
||||
|
||||
async def get_terminal_by_computer(self, computer_name: str) -> Optional[TerminalInfo]:
|
||||
"""通过计算机名查询终端信息
|
||||
|
||||
Args:
|
||||
computer_name: 计算机名(如 'IT-SONGXIAN')
|
||||
"""
|
||||
return None
|
||||
|
||||
async def get_terminal_detail(self, terminal_id: str) -> Optional[TerminalInfo]:
|
||||
"""查询终端详细信息(硬件/软件/网络配置)
|
||||
|
||||
做什么:返回比 get_terminal_by_user 更详细的信息
|
||||
为什么:排查时需要硬件配置、磁盘使用率、已安装软件等
|
||||
|
||||
各系统实现方式:
|
||||
- 联软:getDevAllInfo — 极详细(主板/CPU/内存/硬盘/网卡/显示器)
|
||||
- 火绒:_info2 — 中等详细(硬件/软件/网络配置)
|
||||
- aTrust/eHR:不支持
|
||||
|
||||
Args:
|
||||
terminal_id: 终端在来源系统中的唯一ID
|
||||
"""
|
||||
return None
|
||||
|
||||
# =========================================================================
|
||||
# 安全能力
|
||||
# =========================================================================
|
||||
|
||||
async def get_security_status(self, terminal_id: str) -> Optional[SecurityStatus]:
|
||||
"""获取终端安全状态(病毒/漏洞/隔离状态)
|
||||
|
||||
做什么:聚合安全指标,坐席一目了然
|
||||
为什么:安全问题通常需要紧急处理
|
||||
|
||||
仅火绒支持此接口。
|
||||
|
||||
Args:
|
||||
terminal_id: 终端ID(火绒的client_id)
|
||||
"""
|
||||
return None
|
||||
|
||||
async def isolate_terminal(self, terminal_id: str, reason: str) -> bool:
|
||||
"""隔离终端(断网)
|
||||
|
||||
做什么:调用火绒 _create(type=netctrl) 隔离终端
|
||||
为什么:安全事件紧急处理,阻断威胁扩散
|
||||
|
||||
仅火绒支持。调用前必须二次确认+审计日志记录。
|
||||
|
||||
Args:
|
||||
terminal_id: 终端ID
|
||||
reason: 隔离原因(记入审计日志)
|
||||
|
||||
Returns:
|
||||
True=成功, False=失败
|
||||
|
||||
Raises:
|
||||
NotImplementedError: 本系统不支持隔离操作
|
||||
"""
|
||||
raise NotImplementedError(f"{self.system_name} 不支持终端隔离")
|
||||
|
||||
async def unisolate_terminal(self, terminal_id: str) -> bool:
|
||||
"""解除终端隔离(恢复网络)
|
||||
|
||||
仅火绒支持。
|
||||
|
||||
Args:
|
||||
terminal_id: 终端ID
|
||||
|
||||
Returns:
|
||||
True=成功, False=失败
|
||||
"""
|
||||
raise NotImplementedError(f"{self.system_name} 不支持解除隔离")
|
||||
|
||||
# =========================================================================
|
||||
# VPN/在线状态能力
|
||||
# =========================================================================
|
||||
|
||||
async def get_vpn_sessions(self, username: Optional[str] = None) -> List[VpnSession]:
|
||||
"""查询VPN在线会话
|
||||
|
||||
做什么:获取当前通过aTrust在线的VPN会话
|
||||
为什么:坐席需要知道远程员工VPN状态和IP
|
||||
|
||||
仅aTrust支持。
|
||||
|
||||
Args:
|
||||
username: 可选,过滤指定用户
|
||||
|
||||
Returns:
|
||||
VPN会话列表
|
||||
"""
|
||||
return []
|
||||
|
||||
async def get_online_status(self, username: str) -> bool:
|
||||
"""查询用户是否在线
|
||||
|
||||
做什么:检查用户终端是否当前在线
|
||||
为什么:坐席需要知道用户是否可达
|
||||
|
||||
各系统实现方式:
|
||||
- 联软:existOnlineUser
|
||||
- 火绒:_list(is_online=True) + IP交叉匹配
|
||||
- aTrust:getUserStatus
|
||||
|
||||
Args:
|
||||
username: 员工账号
|
||||
|
||||
Returns:
|
||||
True=在线, False=离线或未知
|
||||
"""
|
||||
return False
|
||||
|
||||
# =========================================================================
|
||||
# 辅助方法
|
||||
# =========================================================================
|
||||
|
||||
def _log_not_implemented(self, method_name: str) -> None:
|
||||
"""记录未实现方法的调试日志
|
||||
|
||||
做什么:当子类未覆写某个方法时记录DEBUG级日志
|
||||
为什么:开发期帮助发现调用链路问题,生产环境可关闭DEBUG
|
||||
"""
|
||||
logger.debug(
|
||||
f"[{self.system_name}] {method_name} 未实现,"
|
||||
f"将走降级逻辑"
|
||||
)
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
# =============================================================================
|
||||
# 企微IT智能服务台 — 外部系统数据缓存层
|
||||
# =============================================================================
|
||||
# 说明:
|
||||
# 1. 封装外部系统数据的缓存读写逻辑
|
||||
# 2. 统一缓存key格式:ext:{system}:{method}:{param_hash}
|
||||
# 3. 不同数据类型使用不同TTL(终端映射30分钟、安全状态5分钟等)
|
||||
# 4. Redis不可用时自动降级(不缓存,直接透传)
|
||||
#
|
||||
# 与 CacheService 的关系:
|
||||
# CacheService 是全局Redis客户端封装,ExternalSystemCache 基于它
|
||||
# 添加外部系统专用的缓存策略(TTL、key格式、刷新机制)
|
||||
# =============================================================================
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from app.services.cache_service import CacheService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# 缓存TTL配置(秒)
|
||||
# =============================================================================
|
||||
|
||||
CACHE_TTL = {
|
||||
# 终端映射(员工→终端)— 映射关系不常变,缓存较长
|
||||
"terminal_mapping": 30 * 60, # 30分钟
|
||||
# 终端详情(硬件/软件)— 硬件配置极少变,缓存最长
|
||||
"terminal_detail": 60 * 60, # 60分钟
|
||||
# 安全状态(漏洞/病毒)— 安全状态需近实时,缓存短
|
||||
"security_status": 5 * 60, # 5分钟
|
||||
# VPN在线状态 — 在线状态变化快,缓存最短
|
||||
"vpn_status": 1 * 60, # 1分钟
|
||||
# eHR员工信息 — 静态数据,缓存最长
|
||||
"employee_info": 24 * 60 * 60, # 24小时
|
||||
}
|
||||
|
||||
|
||||
class ExternalSystemCache:
|
||||
"""外部系统数据缓存
|
||||
|
||||
做什么:为外部系统查询结果提供统一缓存读写
|
||||
为什么:减少外部API调用频率,降低延迟和出错率
|
||||
|
||||
降级策略:Redis不可用时,缓存读写均跳过,直接透传到外部系统
|
||||
"""
|
||||
|
||||
def __init__(self, cache_service: Optional[CacheService] = None):
|
||||
"""初始化缓存层
|
||||
|
||||
Args:
|
||||
cache_service: Redis缓存服务实例。None时降级为无缓存模式
|
||||
"""
|
||||
self._cache = cache_service
|
||||
|
||||
@staticmethod
|
||||
def _make_key(system: str, method: str, param: str) -> str:
|
||||
"""生成缓存key
|
||||
|
||||
做什么:按统一格式生成缓存key
|
||||
为什么:避免不同系统/方法的key冲突
|
||||
|
||||
Args:
|
||||
system: 系统标识(lianruan/huorong/atrust/ehr)
|
||||
method: 方法名(terminal_mapping/terminal_detail/...)
|
||||
param: 查询参数(用户名/计算机名等)
|
||||
|
||||
Returns:
|
||||
缓存key,格式: ext:lianruan:terminal_mapping:abc123
|
||||
"""
|
||||
# 对参数做哈希,避免特殊字符问题
|
||||
param_hash = hashlib.md5(param.encode()).hexdigest()[:12]
|
||||
return f"ext:{system}:{method}:{param_hash}"
|
||||
|
||||
async def get(self, system: str, method: str, param: str) -> Optional[Dict]:
|
||||
"""从缓存读取数据
|
||||
|
||||
做什么:按系统+方法+参数查找缓存
|
||||
为什么:命中缓存可避免一次外部API调用
|
||||
|
||||
Args:
|
||||
system: 系统标识
|
||||
method: 方法名
|
||||
param: 查询参数
|
||||
|
||||
Returns:
|
||||
缓存的字典数据,或 None(未命中/Redis不可用)
|
||||
"""
|
||||
if not self._cache or not self._cache.redis:
|
||||
return None
|
||||
|
||||
key = self._make_key(system, method, param)
|
||||
try:
|
||||
data = await self._cache.get(key)
|
||||
if data:
|
||||
logger.debug(f"缓存命中: {key}")
|
||||
return json.loads(data) if isinstance(data, str) else data
|
||||
return None
|
||||
except Exception as e:
|
||||
# Redis错误不阻断业务,降级为无缓存
|
||||
logger.warning(f"缓存读取失败(降级为无缓存): {key}, error={e}")
|
||||
return None
|
||||
|
||||
async def set(
|
||||
self,
|
||||
system: str,
|
||||
method: str,
|
||||
param: str,
|
||||
data: Dict,
|
||||
ttl_override: Optional[int] = None,
|
||||
) -> bool:
|
||||
"""写入缓存
|
||||
|
||||
做什么:将外部系统查询结果存入缓存
|
||||
为什么:后续相同查询可直接命中缓存
|
||||
|
||||
Args:
|
||||
system: 系统标识
|
||||
method: 方法名
|
||||
param: 查询参数
|
||||
data: 要缓存的数据
|
||||
ttl_override: 自定义TTL(秒),None则使用默认TTL
|
||||
|
||||
Returns:
|
||||
True=成功, False=失败/Redis不可用
|
||||
"""
|
||||
if not self._cache or not self._cache.redis:
|
||||
return False
|
||||
|
||||
key = self._make_key(system, method, param)
|
||||
ttl = ttl_override or CACHE_TTL.get(method, 5 * 60) # 默认5分钟
|
||||
|
||||
try:
|
||||
# 添加缓存时间戳,便于判断数据新鲜度
|
||||
data_with_meta = {
|
||||
**data,
|
||||
"_cached_at": datetime.now().isoformat(),
|
||||
"_source_system": system,
|
||||
}
|
||||
await self._cache.set(key, json.dumps(data_with_meta, default=str), ex=ttl)
|
||||
logger.debug(f"缓存写入: {key}, TTL={ttl}s")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.warning(f"缓存写入失败: {key}, error={e}")
|
||||
return False
|
||||
|
||||
async def invalidate(self, system: str, method: str, param: str) -> bool:
|
||||
"""主动失效缓存
|
||||
|
||||
做什么:删除指定缓存条目
|
||||
为什么:外部数据变更时(如终端隔离后),需主动失效缓存
|
||||
|
||||
Args:
|
||||
system: 系统标识
|
||||
method: 方法名
|
||||
param: 查询参数
|
||||
|
||||
Returns:
|
||||
True=成功, False=失败/Redis不可用
|
||||
"""
|
||||
if not self._cache or not self._cache.redis:
|
||||
return False
|
||||
|
||||
key = self._make_key(system, method, param)
|
||||
try:
|
||||
await self._cache.delete(key)
|
||||
logger.debug(f"缓存失效: {key}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.warning(f"缓存失效失败: {key}, error={e}")
|
||||
return False
|
||||
+166
@@ -0,0 +1,166 @@
|
||||
# =============================================================================
|
||||
# 企微IT智能服务台 — 外部系统连接配置管理
|
||||
# =============================================================================
|
||||
# 说明:
|
||||
# 1. 统一管理联软/火绒/aTrust/eHR四个系统的连接配置
|
||||
# 2. 支持从环境变量或 .env 文件读取
|
||||
# 3. 支持运行时切换 Mock 模式(所有请求走 MockAdapter)
|
||||
#
|
||||
# 配置优先级:环境变量 > .env 文件 > 默认值
|
||||
# =============================================================================
|
||||
|
||||
import os
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class ExternalSystemConfig(BaseModel):
|
||||
"""外部系统连接配置
|
||||
|
||||
做什么:集中管理所有外部系统的连接参数
|
||||
为什么:避免在代码中硬编码,支持环境隔离(开发/测试/生产)
|
||||
"""
|
||||
|
||||
# ── 联软LV7000 ──
|
||||
lianruan_base_url: str = Field(
|
||||
default="http://192.168.3.200:30098",
|
||||
description="联软API基础地址(端口30098)",
|
||||
)
|
||||
lianruan_api_account: Optional[str] = Field(
|
||||
default=None,
|
||||
description="联软API账号(ApiAccount参数)",
|
||||
)
|
||||
lianruan_api_password: Optional[str] = Field(
|
||||
default=None,
|
||||
description="联软API密码(ApiPassword参数)",
|
||||
)
|
||||
lianruan_enabled: bool = Field(
|
||||
default=False,
|
||||
description="联软适配器是否启用(凭证配置后自动启用)",
|
||||
)
|
||||
|
||||
# ── 火绒企业版 ──
|
||||
huorong_base_url: str = Field(
|
||||
default="http://huorong.oa.servyou-it.com:8080",
|
||||
description="火绒API基础地址(内网地址)",
|
||||
)
|
||||
huorong_access_key_id: Optional[str] = Field(
|
||||
default=None,
|
||||
description="火绒AccessKey ID",
|
||||
)
|
||||
huorong_access_key_secret: Optional[str] = Field(
|
||||
default=None,
|
||||
description="火绒AccessKey Secret(HMAC-SHA1签名用)",
|
||||
)
|
||||
huorong_enabled: bool = Field(
|
||||
default=False,
|
||||
description="火绒适配器是否启用",
|
||||
)
|
||||
|
||||
# ── aTrust零信任 ──
|
||||
atrust_base_url: str = Field(
|
||||
default="https://atrust.servyou-it.com:4433",
|
||||
description="aTrust API基础地址(HTTPS端口4433)",
|
||||
)
|
||||
atrust_api_id: Optional[str] = Field(
|
||||
default=None,
|
||||
description="aTrust API ID(x-ca-key Header)",
|
||||
)
|
||||
atrust_api_secret: Optional[str] = Field(
|
||||
default=None,
|
||||
description="aTrust API Secret(HMAC-SHA256签名密钥)",
|
||||
)
|
||||
atrust_directory_domain: Optional[str] = Field(
|
||||
default=None,
|
||||
description="aTrust用户目录域名(V3 API需要此参数)",
|
||||
)
|
||||
atrust_enabled: bool = Field(
|
||||
default=False,
|
||||
description="aTrust适配器是否启用",
|
||||
)
|
||||
|
||||
# ── 北森eHR ──
|
||||
ehr_base_url: Optional[str] = Field(
|
||||
default=None,
|
||||
description="eHR API基础地址",
|
||||
)
|
||||
ehr_client_id: Optional[str] = Field(
|
||||
default=None,
|
||||
description="eHR OAuth2.0 Client ID",
|
||||
)
|
||||
ehr_client_secret: Optional[str] = Field(
|
||||
default=None,
|
||||
description="eHR OAuth2.0 Client Secret",
|
||||
)
|
||||
ehr_enabled: bool = Field(
|
||||
default=False,
|
||||
description="eHR适配器是否启用",
|
||||
)
|
||||
|
||||
# ── 全局配置 ──
|
||||
cache_enabled: bool = Field(
|
||||
default=True,
|
||||
description="是否启用外部数据缓存(Redis)",
|
||||
)
|
||||
mock_mode: bool = Field(
|
||||
default=False,
|
||||
description="Mock模式 — True时所有请求走MockAdapter,不调真实API",
|
||||
)
|
||||
|
||||
class Config:
|
||||
env_prefix = "EXT_" # 环境变量前缀,如 EXT_LIANRUAN_BASE_URL
|
||||
|
||||
|
||||
def load_external_config() -> ExternalSystemConfig:
|
||||
"""从环境变量加载外部系统配置
|
||||
|
||||
做什么:读取 EXT_ 前缀的环境变量,构建配置对象
|
||||
为什么:生产环境通过环境变量注入敏感配置,不写入代码或文件
|
||||
|
||||
Returns:
|
||||
ExternalSystemConfig 实例
|
||||
"""
|
||||
config_dict = {}
|
||||
|
||||
# 映射关系:环境变量名 → 配置字段名
|
||||
env_mapping = {
|
||||
"EXT_LIANRUAN_BASE_URL": "lianruan_base_url",
|
||||
"EXT_LIANRUAN_API_ACCOUNT": "lianruan_api_account",
|
||||
"EXT_LIANRUAN_API_PASSWORD": "lianruan_api_password",
|
||||
"EXT_HUORONG_BASE_URL": "huorong_base_url",
|
||||
"EXT_HUORONG_ACCESS_KEY_ID": "huorong_access_key_id",
|
||||
"EXT_HUORONG_ACCESS_KEY_SECRET": "huorong_access_key_secret",
|
||||
"EXT_ATRUST_BASE_URL": "atrust_base_url",
|
||||
"EXT_ATRUST_API_ID": "atrust_api_id",
|
||||
"EXT_ATRUST_API_SECRET": "atrust_api_secret",
|
||||
"EXT_ATRUST_DIRECTORY_DOMAIN": "atrust_directory_domain",
|
||||
"EXT_EHR_BASE_URL": "ehr_base_url",
|
||||
"EXT_EHR_CLIENT_ID": "ehr_client_id",
|
||||
"EXT_EHR_CLIENT_SECRET": "ehr_client_secret",
|
||||
"EXT_CACHE_ENABLED": "cache_enabled",
|
||||
"EXT_MOCK_MODE": "mock_mode",
|
||||
}
|
||||
|
||||
for env_key, field_name in env_mapping.items():
|
||||
value = os.environ.get(env_key)
|
||||
if value is not None:
|
||||
# 布尔类型特殊处理
|
||||
if field_name in ("cache_enabled", "mock_mode"):
|
||||
config_dict[field_name] = value.lower() in ("true", "1", "yes")
|
||||
else:
|
||||
config_dict[field_name] = value
|
||||
|
||||
config = ExternalSystemConfig(**config_dict)
|
||||
|
||||
# 自动启用已有凭证的系统
|
||||
if config.lianruan_api_account and config.lianruan_api_password:
|
||||
config.lianruan_enabled = True
|
||||
if config.huorong_access_key_id and config.huorong_access_key_secret:
|
||||
config.huorong_enabled = True
|
||||
if config.atrust_api_id and config.atrust_api_secret:
|
||||
config.atrust_enabled = True
|
||||
if config.ehr_client_id and config.ehr_client_secret:
|
||||
config.ehr_enabled = True
|
||||
|
||||
return config
|
||||
+223
@@ -0,0 +1,223 @@
|
||||
# =============================================================================
|
||||
# 企微IT智能服务台 — Mock适配器(开发期使用)
|
||||
# =============================================================================
|
||||
# 说明:
|
||||
# 1. 在 Mock 模式下(EXT_MOCK_MODE=True),所有外部系统查询
|
||||
# 返回预置的 Mock 数据,不调用任何真实 API
|
||||
# 2. Mock 数据覆盖 P0 场景(终端查询、安全状态、VPN在线)
|
||||
# 3. 凭证未配置时自动降级到 MockAdapter,保证开发期无外部依赖
|
||||
#
|
||||
# 使用方式:
|
||||
# EXT_MOCK_MODE=True → 所有系统走 Mock
|
||||
# 某系统凭证未配置 → 单个系统自动降级到 Mock(在 service.py 中处理)
|
||||
# =============================================================================
|
||||
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from app.services.external.base import (
|
||||
ExternalSystemAdapter,
|
||||
TerminalInfo,
|
||||
SecurityStatus,
|
||||
VpnSession,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Mock 数据工厂
|
||||
# =============================================================================
|
||||
|
||||
def _make_mock_terminal(username: str) -> TerminalInfo:
|
||||
"""生成 Mock 终端信息
|
||||
|
||||
做什么:为指定用户生成一个逼真的模拟终端数据
|
||||
为什么:开发期没有真实凭证时需要终端数据支撑会话排查流程
|
||||
"""
|
||||
return TerminalInfo(
|
||||
source_system="mock",
|
||||
terminal_id=f"mock-terminal-{username}",
|
||||
computer_name=f"{username.upper()}-PC01",
|
||||
ip_addresses=[f"192.168.{hash(username) % 255}.{100 + hash(username) % 155}"],
|
||||
mac_addresses=[f"00:16:3E:{hash(username) % 256:02X}:{hash(username + 'a') % 256:02X}:{hash(username + 'b') % 256:02X}"],
|
||||
os_version="Windows 11 专业版 23H2",
|
||||
is_online=True,
|
||||
logged_in_user=username,
|
||||
logged_in_user_name=_username_to_display_name(username),
|
||||
department=_guess_department(username),
|
||||
hardware_summary={
|
||||
"cpu": "Intel Core i7-12700",
|
||||
"memory_total_gb": 16,
|
||||
"memory_used_gb": 8,
|
||||
"disk_total_gb": 512,
|
||||
"disk_free_gb": 128,
|
||||
"disk_usage_pct": 75, # 模拟磁盘使用率较高
|
||||
},
|
||||
last_seen=datetime.now() - timedelta(minutes=5),
|
||||
raw_data=None, # Mock 数据不保留原始响应
|
||||
)
|
||||
|
||||
|
||||
def _make_mock_security_status(terminal_id: str) -> SecurityStatus:
|
||||
"""生成 Mock 安全状态
|
||||
|
||||
做什么:生成一个模拟的安全状态数据
|
||||
为什么:开发期需要验证安全状态卡片、漏洞警告等UI渲染
|
||||
"""
|
||||
return SecurityStatus(
|
||||
source_system="mock",
|
||||
terminal_id=terminal_id,
|
||||
computer_name=f"MOCK-PC01",
|
||||
virus_total=2,
|
||||
virus_uncleaned=1,
|
||||
vulnerabilities=[
|
||||
{
|
||||
"name": "Microsoft Windows 安全更新 (CVE-2025-12345)",
|
||||
"level": "high",
|
||||
"description": "远程代码执行漏洞,需立即修补",
|
||||
"publish_time": (datetime.now() - timedelta(days=7)).isoformat(),
|
||||
},
|
||||
{
|
||||
"name": "火绒安全漏洞扫描:弱密码检测",
|
||||
"level": "medium",
|
||||
"description": "账户密码强度不足,建议修改",
|
||||
"publish_time": (datetime.now() - timedelta(days=3)).isoformat(),
|
||||
},
|
||||
],
|
||||
high_vuln_count=1,
|
||||
is_isolated=False,
|
||||
isolation_source=None,
|
||||
checked_at=datetime.now(),
|
||||
)
|
||||
|
||||
|
||||
def _make_mock_vpn_session(username: str) -> VpnSession:
|
||||
"""生成 Mock VPN 会话"""
|
||||
return VpnSession(
|
||||
source_system="mock",
|
||||
session_id=f"mock-session-{username}",
|
||||
username=username,
|
||||
display_name=_username_to_display_name(username),
|
||||
remote_ip=f"1{hash(username) % 100}.{hash(username + 'r') % 256}.{hash(username + 's') % 256}.{hash(username + 't') % 256}",
|
||||
vpn_ip=f"10.200.{hash(username) % 255}.{100 + hash(username) % 155}",
|
||||
is_trusted=True,
|
||||
os="Windows 11",
|
||||
last_login=datetime.now() - timedelta(minutes=30),
|
||||
domain="servyou.local",
|
||||
)
|
||||
|
||||
|
||||
def _username_to_display_name(username: str) -> str:
|
||||
"""Mock 用户名转换(简单映射)"""
|
||||
name_map = {
|
||||
"songxian": "宋献",
|
||||
"zhangsan": "张三",
|
||||
"lisi": "李四",
|
||||
"wangwu": "王五",
|
||||
}
|
||||
return name_map.get(username, username)
|
||||
|
||||
|
||||
def _guess_department(username: str) -> str:
|
||||
"""Mock 部门推断"""
|
||||
dept_map = {
|
||||
"songxian": "IT支持组",
|
||||
"zhangsan": "财务部",
|
||||
"lisi": "人力资源部",
|
||||
"wangwu": "研发部",
|
||||
}
|
||||
return dept_map.get(username, "未知部门")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# MockAdapter 实现
|
||||
# =============================================================================
|
||||
|
||||
class MockAdapter(ExternalSystemAdapter):
|
||||
"""Mock 适配器 — 开发期替代所有外部系统
|
||||
|
||||
做什么:提供逼真的模拟数据,让开发期可以不依赖任何外部系统
|
||||
为什么:阶段一MVP验证、前端开发、单元测试都需要稳定的数据来源
|
||||
|
||||
降级规则:
|
||||
- 所有方法均返回 Mock 数据
|
||||
- 支持常用测试用户:songxian / zhangsan / lisi / wangwu
|
||||
- is_available 固定返回 True(Mock 永远可用)
|
||||
"""
|
||||
|
||||
@property
|
||||
def system_name(self) -> str:
|
||||
return "mock"
|
||||
|
||||
@property
|
||||
def is_available(self) -> bool:
|
||||
"""Mock 永远可用"""
|
||||
return True
|
||||
|
||||
async def health_check(self) -> bool:
|
||||
"""Mock 健康检查永远通过"""
|
||||
logger.debug("[MockAdapter] 健康检查 → OK(Mock模式)")
|
||||
return True
|
||||
|
||||
# ── 终端查询能力 ──
|
||||
|
||||
async def get_terminal_by_user(self, username: str) -> Optional[TerminalInfo]:
|
||||
"""Mock:通过账号查询终端
|
||||
|
||||
做什么:返回预置的 Mock 终端信息
|
||||
为什么:开发期坐席打开会话时需要看到终端画像
|
||||
"""
|
||||
logger.info(f"[MockAdapter] get_terminal_by_user({username}) → Mock数据")
|
||||
return _make_mock_terminal(username)
|
||||
|
||||
async def get_terminal_by_computer(self, computer_name: str) -> Optional[TerminalInfo]:
|
||||
"""Mock:通过计算机名查询终端"""
|
||||
logger.info(f"[MockAdapter] get_terminal_by_computer({computer_name}) → Mock数据")
|
||||
# 从计算机名反推用户名(简单逻辑)
|
||||
username = computer_name.split("-")[0].lower() if "-" in computer_name else "songxian"
|
||||
return _make_mock_terminal(username)
|
||||
|
||||
async def get_terminal_detail(self, terminal_id: str) -> Optional[TerminalInfo]:
|
||||
"""Mock:查询终端详细信息"""
|
||||
logger.info(f"[MockAdapter] get_terminal_detail({terminal_id}) → Mock数据")
|
||||
return _make_mock_terminal("songxian")
|
||||
|
||||
# ── 安全能力 ──
|
||||
|
||||
async def get_security_status(self, terminal_id: str) -> Optional[SecurityStatus]:
|
||||
"""Mock:获取安全状态"""
|
||||
logger.info(f"[MockAdapter] get_security_status({terminal_id}) → Mock数据")
|
||||
return _make_mock_security_status(terminal_id)
|
||||
|
||||
async def isolate_terminal(self, terminal_id: str, reason: str) -> bool:
|
||||
"""Mock:隔离终端(Mock 模式仅记录日志)"""
|
||||
logger.warning(
|
||||
f"[MockAdapter] 隔离终端(Mock,不真实执行): "
|
||||
f"terminal={terminal_id}, reason={reason}"
|
||||
)
|
||||
return True # Mock 永远返回成功
|
||||
|
||||
async def unisolate_terminal(self, terminal_id: str) -> bool:
|
||||
"""Mock:解除隔离"""
|
||||
logger.warning(
|
||||
f"[MockAdapter] 解除隔离(Mock,不真实执行): terminal={terminal_id}"
|
||||
)
|
||||
return True
|
||||
|
||||
# ── VPN/在线状态 ──
|
||||
|
||||
async def get_vpn_sessions(self, username: Optional[str] = None) -> List[VpnSession]:
|
||||
"""Mock:查询VPN在线会话"""
|
||||
if username:
|
||||
return [_make_mock_vpn_session(username)]
|
||||
# 返回多个 Mock 会话
|
||||
return [
|
||||
_make_mock_vpn_session("songxian"),
|
||||
_make_mock_vpn_session("zhangsan"),
|
||||
]
|
||||
|
||||
async def get_online_status(self, username: str) -> bool:
|
||||
"""Mock:查询在线状态(Mock 永远返回 True)"""
|
||||
return True
|
||||
+491
@@ -0,0 +1,491 @@
|
||||
# =============================================================================
|
||||
# 企微IT智能服务台 — 外部系统统一门面服务
|
||||
# =============================================================================
|
||||
# 说明:
|
||||
# 1. 上层业务代码(AI Wingman、会话管理等)只依赖此类
|
||||
# 2. 按优先级链式查询(联软 → aTrust → eHR)
|
||||
# 3. 自动处理降级(系统不可用时跳到下一个)
|
||||
# 4. 所有方法均有详细行内注释(做什么 + 为什么)
|
||||
#
|
||||
# 使用方式:
|
||||
# from app.services.external import get_external_service
|
||||
# svc = get_external_service()
|
||||
# terminal = await svc.find_user_terminal("songxian")
|
||||
# =============================================================================
|
||||
|
||||
import logging
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from app.services.cache_service import CacheService
|
||||
from app.services.external.base import (
|
||||
ExternalSystemAdapter,
|
||||
TerminalInfo,
|
||||
SecurityStatus,
|
||||
VpnSession,
|
||||
)
|
||||
from app.services.external.config import ExternalSystemConfig
|
||||
from app.services.external.cache import ExternalSystemCache
|
||||
from app.services.external.mock import MockAdapter
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# 全局单例(懒加载)
|
||||
# =============================================================================
|
||||
|
||||
_external_service_instance: Optional["ExternalSystemService"] = None
|
||||
|
||||
|
||||
def get_external_service() -> "ExternalSystemService":
|
||||
"""获取外部系统服务单例
|
||||
|
||||
做什么:返回全局唯一的 ExternalSystemService 实例
|
||||
为什么:避免重复初始化 Adapter,节省连接资源
|
||||
"""
|
||||
global _external_service_instance
|
||||
if _external_service_instance is None:
|
||||
raise RuntimeError(
|
||||
"ExternalSystemService 尚未初始化,"
|
||||
"请在应用启动时调用 init_external_service()"
|
||||
)
|
||||
return _external_service_instance
|
||||
|
||||
|
||||
def init_external_service(
|
||||
config: Optional[ExternalSystemConfig] = None,
|
||||
cache_service: Optional[Any] = None,
|
||||
) -> ExternalSystemService:
|
||||
"""初始化外部系统服务(应用启动时调用一次)
|
||||
|
||||
做什么:根据配置创建所有 Adapter,组装成 ExternalSystemService
|
||||
为什么:集中初始化,避免分散在各处创建 Adapter 实例
|
||||
|
||||
Args:
|
||||
config: 外部系统配置,None 时自动从环境变量加载
|
||||
cache_service: Redis 缓存服务实例,None 时降级为无缓存
|
||||
|
||||
Returns:
|
||||
初始化完成的 ExternalSystemService 实例
|
||||
"""
|
||||
global _external_service_instance
|
||||
|
||||
if config is None:
|
||||
from app.services.external.config import load_external_config
|
||||
config = load_external_config()
|
||||
|
||||
# 创建缓存层
|
||||
cache = ExternalSystemCache(cache_service) if cache_service else None
|
||||
|
||||
# 按优先级组装 Adapter 字典
|
||||
adapters: Dict[str, ExternalSystemAdapter] = {}
|
||||
|
||||
if config.mock_mode:
|
||||
# Mock 模式:所有系统走 MockAdapter
|
||||
logger.info("[External] Mock模式已启用,所有外部系统查询走Mock数据")
|
||||
mock = MockAdapter()
|
||||
adapters = {
|
||||
"lianruan": mock,
|
||||
"huorong": mock,
|
||||
"atrust": mock,
|
||||
"ehr": mock,
|
||||
}
|
||||
else:
|
||||
# ── 联软(主映射源P0)─────────────────────────────
|
||||
# 做什么:创建联软 Adapter(凭证已配置时启用)
|
||||
# 为什么:联软的 strusername 字段是员工→终端映射最可靠来源
|
||||
if config.lianruan_enabled:
|
||||
from app.services.external.lianruan_adapter import LianruanAdapter
|
||||
adapters["lianruan"] = LianruanAdapter(config)
|
||||
logger.info("[External] 联软适配器已启用")
|
||||
else:
|
||||
logger.warning(
|
||||
"[External] 联软适配器未启用(凭证未配置),"
|
||||
"终端映射功能将降级"
|
||||
)
|
||||
|
||||
# ── 火绒(安全源P0)─────────────────────────────────
|
||||
# 做什么:创建火绒 Adapter(凭证已配置时启用)
|
||||
# 为什么:火绒提供终端安全状态(病毒/漏洞/隔离),不参与映射
|
||||
if config.huorong_enabled:
|
||||
from app.services.external.huorong_adapter import HuorongAdapter
|
||||
adapters["huorong"] = HuorongAdapter(config)
|
||||
logger.info("[External] 火绒适配器已启用")
|
||||
else:
|
||||
logger.info("[External] 火绒适配器未启用(凭证未配置)")
|
||||
|
||||
# ── aTrust(VPN源P1)────────────────────────────────
|
||||
# 做什么:创建 aTrust Adapter(凭证已配置时启用)
|
||||
# 为什么:aTrust 提供 VPN 在线状态和虚拟IP,用于远程员工排查
|
||||
if config.atrust_enabled:
|
||||
from app.services.external.atrust_adapter import ATrustAdapter
|
||||
adapters["atrust"] = ATrustAdapter(config)
|
||||
logger.info("[External] aTrust适配器已启用")
|
||||
else:
|
||||
logger.info("[External] aTrust适配器未启用(凭证未配置)")
|
||||
|
||||
# ── eHR(辅助静态数据P2)───────────────────────────
|
||||
# 做什么:创建 eHR Adapter(凭证已配置时启用)
|
||||
# 为什么:eHR 提供员工基础信息和任职信息,作为静态数据补充
|
||||
if config.ehr_enabled:
|
||||
from app.services.external.ehr_adapter import EHRAdapter
|
||||
adapters["ehr"] = EHRAdapter(config)
|
||||
logger.info("[External] eHR适配器已启用")
|
||||
else:
|
||||
logger.info("[External] eHR适配器未启用(凭证未配置)")
|
||||
|
||||
_external_service_instance = ExternalSystemService(adapters, cache)
|
||||
logger.info(
|
||||
f"[External] 服务初始化完成,已加载适配器: "
|
||||
f"{list(adapters.keys())}"
|
||||
)
|
||||
return _external_service_instance
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# 统一门面服务
|
||||
# =============================================================================
|
||||
|
||||
class ExternalSystemService:
|
||||
"""外部系统统一门面 — 上层业务唯一依赖的入口
|
||||
|
||||
做什么:按优先级链式查询外部系统,对上层屏蔽底层差异
|
||||
为什么:上层代码不需要知道终端数据来自联软还是火绒
|
||||
|
||||
查询优先级(映射场景):
|
||||
1. 联软(主源,strusername 精确匹配)
|
||||
2. aTrust(VPN源,bindUserList 匹配)
|
||||
3. eHR(静态辅助,无终端数据,返回None)
|
||||
|
||||
安全能力(仅火绒):
|
||||
- 获取安全状态(病毒/漏洞)
|
||||
- 隔离/解除终端(需admin角色+二次确认)
|
||||
|
||||
VPN能力(仅aTrust):
|
||||
- 查询在线会话
|
||||
- 踢出用户
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
adapters: Dict[str, ExternalSystemAdapter],
|
||||
cache: Optional[ExternalSystemCache] = None,
|
||||
):
|
||||
"""初始化统一门面
|
||||
|
||||
Args:
|
||||
adapters: 系统标识 → Adapter 实例 的字典
|
||||
cache: 外部数据缓存层(可为None,降级为无缓存)
|
||||
"""
|
||||
self._adapters = adapters
|
||||
self._cache = cache
|
||||
|
||||
# =========================================================================
|
||||
# 终端查询(映射核心)
|
||||
# =========================================================================
|
||||
|
||||
async def find_user_terminal(self, username: str) -> Optional[TerminalInfo]:
|
||||
"""查找用户终端 — 按优先级链式查询
|
||||
|
||||
做什么:根据员工账号查找其使用的终端信息
|
||||
为什么:这是员工→终端映射的核心入口,坐席排查时首先需要知道
|
||||
员工用哪台电脑
|
||||
|
||||
查询顺序:
|
||||
1. 联软 queryDevByParams(strusername=xxx) — 最精确
|
||||
2. aTrust queryAll(bindUserList) — VPN 场景补充
|
||||
3. eHR — 无终端数据,返回 None
|
||||
|
||||
降级:某系统不可用时自动跳过,不影响整体结果。
|
||||
|
||||
Args:
|
||||
username: 员工账号(如 'songxian')
|
||||
|
||||
Returns:
|
||||
TerminalInfo 或 None(所有系统均未找到)
|
||||
"""
|
||||
logger.info(f"[External] 查找用户终端: username={username}")
|
||||
|
||||
# ── 第1优先级:联软(主映射源)────────────────────
|
||||
# 做什么:优先用联软查,它有 strusername 精确字段
|
||||
# 为什么:联软直接建立员工账号→终端的映射,比IP交叉匹配可靠
|
||||
lianruan = self._adapters.get("lianruan")
|
||||
if lianruan and lianruan.is_available:
|
||||
try:
|
||||
result = await self._query_with_cache(
|
||||
"lianruan", "get_terminal_by_user", username
|
||||
)
|
||||
if result:
|
||||
logger.info(
|
||||
f"[External] 联软命中: username={username}, "
|
||||
f"computer={result.computer_name}"
|
||||
)
|
||||
return result
|
||||
except Exception as e:
|
||||
# 联软不可用 → 降级到 aTrust,不阻断
|
||||
logger.warning(
|
||||
f"[External] 联软查询失败(降级到aTrust): {e}"
|
||||
)
|
||||
else:
|
||||
logger.debug("[External] 联软不可用或未启用,跳过")
|
||||
|
||||
# ── 第2优先级:aTrust(VPN源)─────────────────────
|
||||
# 做什么:联软未命中时,用 aTrust 查 VPN 终端
|
||||
# 为什么:远程办公员工可能不在联软覆盖范围内
|
||||
atrust = self._adapters.get("atrust")
|
||||
if atrust and atrust.is_available:
|
||||
try:
|
||||
result = await self._query_with_cache(
|
||||
"atrust", "get_terminal_by_user", username
|
||||
)
|
||||
if result:
|
||||
logger.info(
|
||||
f"[External] aTrust命中: username={username}, "
|
||||
f"vpn_ip={result.ip_addresses}"
|
||||
)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"[External] aTrust查询失败(降级到eHR): {e}"
|
||||
)
|
||||
else:
|
||||
logger.debug("[External] aTrust不可用或未启用,跳过")
|
||||
|
||||
# ── 第3优先级:eHR(辅助静态数据)────────────────
|
||||
# 做什么:eHR 不提供终端数据,此方法返回 None
|
||||
# 为什么:保留接口一致性,未来可能扩展
|
||||
ehr = self._adapters.get("ehr")
|
||||
if ehr and ehr.is_available:
|
||||
result = await self._query_with_cache(
|
||||
"ehr", "get_terminal_by_user", username
|
||||
)
|
||||
if result:
|
||||
return result
|
||||
|
||||
# 所有系统均未命中
|
||||
logger.info(f"[External] 所有系统均未找到用户终端: username={username}")
|
||||
return None
|
||||
|
||||
async def get_terminal_detail(self, terminal_id: str) -> Optional[TerminalInfo]:
|
||||
"""查询终端详细信息(硬件/软件/网络配置)
|
||||
|
||||
做什么:获取比 find_user_terminal 更详细的终端信息
|
||||
为什么:排查硬件故障(卡慢)时需要CPU/内存/磁盘使用率等数据
|
||||
|
||||
优先联软(getDevAllInfo 比火绒 _info2 更详细)。
|
||||
|
||||
Args:
|
||||
terminal_id: 终端在来源系统中的唯一ID
|
||||
|
||||
Returns:
|
||||
TerminalInfo(含 hardware_summary)或 None
|
||||
"""
|
||||
# 联软详细信息最全(含主板/CPU/内存/硬盘/显示器)
|
||||
lianruan = self._adapters.get("lianruan")
|
||||
if lianruan and lianruan.is_available:
|
||||
try:
|
||||
return await lianruan.get_terminal_detail(terminal_id)
|
||||
except Exception as e:
|
||||
logger.warning(f"[External] 联软详细信息查询失败: {e}")
|
||||
|
||||
# 火绒作为备选(_info2 含硬件/软件/网络配置)
|
||||
huorong = self._adapters.get("huorong")
|
||||
if huorong and huorong.is_available:
|
||||
try:
|
||||
return await huorong.get_terminal_detail(terminal_id)
|
||||
except Exception as e:
|
||||
logger.warning(f"[External] 火绒详细信息查询失败: {e}")
|
||||
|
||||
return None
|
||||
|
||||
# =========================================================================
|
||||
# 安全能力(仅火绒)
|
||||
# =========================================================================
|
||||
|
||||
async def get_terminal_security(self, terminal_id: str) -> Optional[SecurityStatus]:
|
||||
"""获取终端安全状态
|
||||
|
||||
做什么:查询终端的病毒事件、高危漏洞、隔离状态
|
||||
为什么:坐席排查安全问题时需要一目了然
|
||||
|
||||
仅火绒支持此能力。
|
||||
|
||||
Args:
|
||||
terminal_id: 火绒的 client_id
|
||||
|
||||
Returns:
|
||||
SecurityStatus 或 None(火绒不可用)
|
||||
"""
|
||||
huorong = self._adapters.get("huorong")
|
||||
if not huorong or not huorong.is_available:
|
||||
logger.warning("[External] 火绒不可用,无法获取安全状态")
|
||||
return None
|
||||
|
||||
try:
|
||||
return await self._query_with_cache(
|
||||
"huorong", "get_security_status", terminal_id
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"[External] 获取安全状态失败: {e}")
|
||||
return None
|
||||
|
||||
async def isolate_terminal(
|
||||
self, terminal_id: str, reason: str, operator: str
|
||||
) -> bool:
|
||||
"""隔离终端(断网)
|
||||
|
||||
做什么:调用火绒 _create(type=netctrl) 隔离终端
|
||||
为什么:安全事件紧急处理,阻断威胁扩散
|
||||
|
||||
仅火绒支持。调用前必须在上层做:
|
||||
1. 操作者角色校验(仅 admin 可操作)
|
||||
2. 二次确认弹窗
|
||||
3. 审计日志记录
|
||||
|
||||
Args:
|
||||
terminal_id: 火绒的 client_id
|
||||
reason: 隔离原因(记入审计日志)
|
||||
operator: 操作者账号
|
||||
|
||||
Returns:
|
||||
True=成功, False=失败
|
||||
"""
|
||||
huorong = self._adapters.get("huorong")
|
||||
if not huorong or not huorong.is_available:
|
||||
logger.error("[External] 火绒不可用,无法执行隔离")
|
||||
return False
|
||||
|
||||
logger.warning(
|
||||
f"[External] 执行终端隔离: terminal={terminal_id}, "
|
||||
f"operator={operator}, reason={reason}"
|
||||
)
|
||||
try:
|
||||
return await huorong.isolate_terminal(terminal_id, reason)
|
||||
except Exception as e:
|
||||
logger.error(f"[External] 隔离失败: {e}")
|
||||
return False
|
||||
|
||||
async def unisolate_terminal(self, terminal_id: str) -> bool:
|
||||
"""解除终端隔离(恢复网络)"""
|
||||
huorong = self._adapters.get("huorong")
|
||||
if not huorong or not huorong.is_available:
|
||||
return False
|
||||
try:
|
||||
return await huorong.unisolate_terminal(terminal_id)
|
||||
except Exception as e:
|
||||
logger.error(f"[External] 解除隔离失败: {e}")
|
||||
return False
|
||||
|
||||
# =========================================================================
|
||||
# VPN/在线状态(仅aTrust)
|
||||
# =========================================================================
|
||||
|
||||
async def get_vpn_sessions(
|
||||
self, username: Optional[str] = None
|
||||
) -> List[VpnSession]:
|
||||
"""查询VPN在线会话
|
||||
|
||||
做什么:获取当前通过aTrust在线的VPN会话列表
|
||||
为什么:坐席需要知道远程员工是否在线、VPN IP是什么
|
||||
|
||||
仅aTrust支持。
|
||||
|
||||
Args:
|
||||
username: 可选,过滤指定用户的会话
|
||||
|
||||
Returns:
|
||||
VPN会话列表(可能为空)
|
||||
"""
|
||||
atrust = self._adapters.get("atrust")
|
||||
if not atrust or not atrust.is_available:
|
||||
return []
|
||||
try:
|
||||
return await atrust.get_vpn_sessions(username)
|
||||
except Exception as e:
|
||||
logger.warning(f"[External] 查询VPN会话失败: {e}")
|
||||
return []
|
||||
|
||||
async def get_online_status(self, username: str) -> bool:
|
||||
"""查询用户是否在线
|
||||
|
||||
做什么:检查用户当前是否在线(任何方式接入)
|
||||
为什么:坐席发起协作或推送消息前需要知道用户是否可达
|
||||
|
||||
Args:
|
||||
username: 员工账号
|
||||
|
||||
Returns:
|
||||
True=在线, False=离线或未知
|
||||
"""
|
||||
# 优先联软(内网接入)
|
||||
lianruan = self._adapters.get("lianruan")
|
||||
if lianruan and lianruan.is_available:
|
||||
try:
|
||||
if await lianruan.get_online_status(username):
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 再查 aTrust(VPN接入)
|
||||
atrust = self._adapters.get("atrust")
|
||||
if atrust and atrust.is_available:
|
||||
try:
|
||||
if await atrust.get_online_status(username):
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return False
|
||||
|
||||
# =========================================================================
|
||||
# 内部方法
|
||||
# =========================================================================
|
||||
|
||||
async def _query_with_cache(
|
||||
self, system: str, method: str, param: str
|
||||
) -> Any:
|
||||
"""带缓存的查询(内部方法)
|
||||
|
||||
做什么:先查缓存,未命中则调Adapter,结果写回缓存
|
||||
为什么:减少外部API调用频率,降低延迟
|
||||
|
||||
Args:
|
||||
system: 系统标识
|
||||
method: 方法名(用于缓存key)
|
||||
param: 查询参数(用于缓存key)
|
||||
|
||||
Returns:
|
||||
Adapter返回的数据(可能经缓存)
|
||||
"""
|
||||
# 步骤1:尝试从缓存读取
|
||||
if self._cache:
|
||||
cached = await self._cache.get(system, method, param)
|
||||
if cached:
|
||||
return cached
|
||||
|
||||
# 步骤2:缓存未命中,调用Adapter
|
||||
adapter = self._adapters.get(system)
|
||||
if not adapter:
|
||||
raise RuntimeError(f"Adapter不存在: {system}")
|
||||
|
||||
method_map = {
|
||||
"get_terminal_by_user": adapter.get_terminal_by_user,
|
||||
"get_terminal_by_computer": adapter.get_terminal_by_computer,
|
||||
"get_terminal_detail": adapter.get_terminal_detail,
|
||||
"get_security_status": adapter.get_security_status,
|
||||
"get_vpn_sessions": adapter.get_vpn_sessions,
|
||||
"get_online_status": adapter.get_online_status,
|
||||
}
|
||||
|
||||
if method not in method_map:
|
||||
raise RuntimeError(f"未知方法: {method}")
|
||||
|
||||
result = await method_map[method](param)
|
||||
|
||||
# 步骤3:写入缓存(仅非None结果)
|
||||
if result and self._cache:
|
||||
# 转成可序列化的字典
|
||||
data = result.dict() if hasattr(result, "dict") else result
|
||||
await self._cache.set(system, method, param, data)
|
||||
|
||||
return result
|
||||
Reference in New Issue
Block a user