feat: 2026-07-12~13 全量更新 - AI对话链路改造+H5 v4/v5+坐席端v5+上下文感知诊断+知识库迭代3
## H5 员工端 v4 (2026-07-13 00:48 已部署)
- 人工按钮三态文案统一为"人工坐席"
- 按钮位置移至发送键和语音按钮上方(垂直堆叠)
- 点按钮直接调 store.shakeAgent(),删除 CallAgentModal 弹窗动画
- 截图快捷键提示改为"截图->粘贴:Alt+Shift+A-Ctrl+V ---> Ctrl+V"
- 移动端隐藏截图提示(CSS 媒体查询)
- AI转人工提示改为"已为您呼叫人工坐席,请稍等!"
- 坐席接入提示改为"坐席正在查看您的信息,请等待处理回复!"
- 删除"摇铃呼叫坐席"入口和文案
- 删除孤儿组件 MessageList.vue + shake 动画 CSS
## H5 员工端 v5 (2026-07-13 02:08 已部署)
- RightPanel v2.1:删除"软件安装"和"资源权限"标签页
- 移除标签栏,智能推荐(DynamicRecommend)直接展示
- 删除 SoftwareDownloads/ApprovalLinks 引用和相关 CSS
## AI 对话链路全栈改造 Phase 1-6 (已部署)
- Phase 1: Dify JSON输出 + 后端blocking解析 + 双WS推送 + 错误降级
- Phase 2: 关键词收窄(~25强意图词) + 两级分类Prompt + 删除前端checkApprovalIntent
- Phase 3: WS扩展(ai_thinking+dynamic_recommend) + ai_structured气泡 + RightPanel v2 + 选项回传
- Phase 4: VisionService接入 + 图片消息融合(5秒窗口) + 降级策略
- Phase 5: 坐席端ai_thinking指示器 + ai_structured/byod_card渲染 + handleNewMessage修复
- Phase 6: diagnosis_stage(6值) + response_time_ms计时 + 慢响应告警(>10s)
## 坐席端 v5 (2026-07-13 01:38 已部署)
- ai_structured/byod_card 只读渲染
- AI思考指示器 UI
- handleNewMessage 透传 msg_type/extra_data 修复
- 布局优化v2.0: QuickReplyBar L1+L2悬浮 + ReplyBox左右分区 + 右栏260/560px切换
- 键盘快捷键v2.3: 纯数字路由 + ESC分层撤销 + Shift+Space用event.code
## 上下文感知智能诊断闭环 (2026-07-12 已部署)
- 三层诊断(API→Script→AI) + 三段排队(VIP→info_locked→not locked)
- 答题插队 + 五场景关闭
- 迁移052(6表+6列) + queue_service + quiz_service + closing_service
- H5前端: QueueWaiting + RightPanel双Tab + InputBar三态 + ResolveConfirmCard
- 坐席前端: pending_close结单流程 + 信息锁定(Dify步骤完成+有效回答率≥70%)
## 知识库迭代3 (2026-07-12 已部署)
- 分诊交互(H5+坐席+Dify独立应用)
- 拓扑预览(ECharts只读)
- 代答排除(4种匹配器: keyword/regex/intent/category)
- 迁移051 + 44文件43测试通过
## 后端变更
- 6个Python文件改造(h5_ai_task.py/h5.py/ai_service.py/closing_service.py等)
- funny_phrase_service.py: shake/connected/keyword 默认文案更新
- session_service.py: 企微消息文案同步
- 新增: queue.py/quiz.py/triage.py/exclusion_rules.py 等API端点
- 新增: diagnostic.py/quiz.py/triage_session.py 等模型
- 新增: closing_service/queue_service/quiz_service/triage_service 等服务
## 文档更新
- CHANGELOG.md: 新增 [未发布] 区全部变更记录
- 项目管理主文档 v2.5: 新增v0.7.3版本 + 已完成看板 + 最近搞定
- 版本记录: 新增v0.7.3条目
- AI对话链路实施计划: Phase 1-6 全部标记✅已实施
- 新增架构图/时序图/类图(mermaid)
## 部署路径修正
- 服务器项目根路径: /opt/wecom-it-desk/
- 所有前端dist均为ro bind mount,只能在宿主机源路径操作
- 服务器nginx /h5/ 是静态文件服务(非proxy_pass)
- elFinder上传二进制不可靠(MD5不匹配),改用base64分块上传
This commit is contained in:
@@ -0,0 +1,646 @@
|
||||
# =============================================================================
|
||||
# 企微IT智能服务台 — IT 健康聚合服务
|
||||
# =============================================================================
|
||||
# 说明:整合联软(设备信息/CPU/内存/硬盘)、火绒(安全状态/病毒/漏洞)、
|
||||
# 资产服务(资产编号/启用时间)数据源,返回前端 BasicInfoCard.vue 期望的数据结构。
|
||||
#
|
||||
# 数据流:
|
||||
# 1. 用 employee_id (企微UserID) 作为联软 strusername 查询终端
|
||||
# 2. 联软 get_dev_all_info() 获取详细硬件/磁盘/网卡信息
|
||||
# 3. 火绒 list_terminals() 按计算机名匹配,获取安全状态
|
||||
# 4. 火绒 list_terminal_leaks() 检查漏洞,get_virus_events() 检查病毒
|
||||
# 5. 资产服务 find_asset() 查资产编号和启用时间
|
||||
#
|
||||
# 降级策略:
|
||||
# - 联软/火绒未配置 → 返回 Mock 数据(标记 data_source: "mock")
|
||||
# - 联软配置但火绒未配置 → 设备信息真实,安全状态为 pending
|
||||
# - 任一API调用失败 → 该部分数据返回 None,不影响其他部分
|
||||
# =============================================================================
|
||||
|
||||
import logging
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ITHealthService:
|
||||
"""IT 健康聚合服务。
|
||||
|
||||
从联软、火绒、资产服务获取数据,聚合为前端期望的格式。
|
||||
所有外部 API 调用均做了异常隔离——任一数据源失败不影响整体。
|
||||
"""
|
||||
|
||||
def __init__(self, db: AsyncSession):
|
||||
"""初始化服务。
|
||||
|
||||
Args:
|
||||
db: 数据库会话(用于读取 system_configs 表中的集成配置)
|
||||
"""
|
||||
self.db = db
|
||||
|
||||
async def get_it_health(self, employee_id: str) -> Dict[str, Any]:
|
||||
"""获取员工终端的 IT 健康信息。
|
||||
|
||||
这是主入口方法,聚合所有数据源,返回前端期望的 JSON 结构。
|
||||
|
||||
Args:
|
||||
employee_id: 员工企微 UserID(对应联软的 strusername)
|
||||
|
||||
Returns:
|
||||
Dict: 包含 current_device / other_devices / data_source / health_score
|
||||
"""
|
||||
# 尝试从联软获取真实设备信息
|
||||
device_info = await self._get_device_from_lianruan(employee_id)
|
||||
|
||||
if device_info is None:
|
||||
# 联软不可用 → 返回 Mock 数据
|
||||
return self._get_mock_data(employee_id)
|
||||
|
||||
# 联软数据可用,尝试从火绒获取安全状态
|
||||
security_info = await self._get_security_from_huorong(
|
||||
device_info.get("device_name", "")
|
||||
)
|
||||
|
||||
# 尝试从资产服务获取资产编号
|
||||
asset_info = await self._get_asset_info(device_info.get("device_name", ""))
|
||||
|
||||
# 聚合数据
|
||||
current_device = self._build_current_device(device_info, security_info, asset_info)
|
||||
|
||||
# 获取其他设备(联软中该用户的其他终端)
|
||||
other_devices = await self._get_other_devices(employee_id, device_info.get("device_name", ""))
|
||||
|
||||
return {
|
||||
"current_device": current_device,
|
||||
"other_devices": other_devices,
|
||||
"data_source": "real",
|
||||
"generated_at": datetime.now(timezone.utc).isoformat(),
|
||||
}
|
||||
|
||||
# ==========================================================================
|
||||
# 联软数据获取
|
||||
# ==========================================================================
|
||||
|
||||
async def _get_device_from_lianruan(self, employee_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""从联软查终端设备信息。
|
||||
|
||||
流程:
|
||||
1. 用 employee_id 作为 strusername 查终端列表
|
||||
2. 取第一个(最近活跃的)终端
|
||||
3. 调 get_dev_all_info() 获取详细硬件信息
|
||||
|
||||
Args:
|
||||
employee_id: 员工账号
|
||||
|
||||
Returns:
|
||||
Dict: 设备信息字典,联软不可用时返回 None
|
||||
"""
|
||||
try:
|
||||
from app.integrations.lianruan.config import get_lianruan_client
|
||||
|
||||
client = await get_lianruan_client(self.db)
|
||||
|
||||
# 按员工账号查终端列表
|
||||
result = await client.query_dev_by_params(strusername=employee_id, per_page=10)
|
||||
terminals = result.get("items", [])
|
||||
|
||||
if not terminals:
|
||||
logger.info(f"联软未找到 employee_id={employee_id} 的终端")
|
||||
return None
|
||||
|
||||
# 取第一个终端(联软默认按最近活跃排序)
|
||||
terminal = terminals[0]
|
||||
device_name = terminal.strdevname
|
||||
|
||||
if not device_name:
|
||||
logger.warning(f"联软返回的终端无计算机名: {terminal}")
|
||||
return None
|
||||
|
||||
# 获取详细信息
|
||||
detail = await client.get_dev_all_info(strdevname=device_name)
|
||||
|
||||
# 构建设备信息字典
|
||||
device = {
|
||||
"device_name": device_name,
|
||||
"is_online": terminal.istatus == "1",
|
||||
"ip_address": terminal.strdevip or detail.strip1,
|
||||
"mac": terminal.strmac or detail.strmac,
|
||||
"os": detail.stros or "",
|
||||
"location": terminal.strdeptname or "",
|
||||
"department": terminal.strdeptname or "",
|
||||
"switch_name": terminal.strswitchname or "",
|
||||
"uptime": self._format_uptime(detail.dtdevuptime),
|
||||
"last_online_time": detail.dtdevuptime or "",
|
||||
"last_offline_time": detail.dtdevdowntime or "",
|
||||
"device_type": detail.strdevtype or "台式机",
|
||||
"serial_number": detail.strserialnumber or "",
|
||||
"mainboard": detail.strmainboardtype or "",
|
||||
# 硬件详情
|
||||
"cpu_list": [
|
||||
{"name": c.name, "model": c.model, "vendor": c.vendor}
|
||||
for c in detail.cpu
|
||||
] if detail.cpu else [],
|
||||
"memory_list": [
|
||||
{"name": m.name, "capacity": m.capacity, "vendor": m.vendor}
|
||||
for m in detail.memory
|
||||
] if detail.memory else [],
|
||||
"logical_disks": [
|
||||
{
|
||||
"label": d.name,
|
||||
"total": d.total_size,
|
||||
"free": d.free_space,
|
||||
"usage_percent": d.usage_percent,
|
||||
}
|
||||
for d in detail.logical_disk
|
||||
] if detail.logical_disk else [],
|
||||
"network_cards": [
|
||||
{"name": n.name, "mac": n.mac, "is_wireless": n.is_wireless}
|
||||
for n in detail.network_card
|
||||
] if detail.network_card else [],
|
||||
}
|
||||
|
||||
logger.info(f"联软获取设备成功: {device_name} (employee={employee_id})")
|
||||
return device
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"联软获取设备信息失败: {e}")
|
||||
return None
|
||||
|
||||
async def _get_other_devices(
|
||||
self, employee_id: str, exclude_device: str
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""获取员工的其他设备(联软中该用户的其他终端)。
|
||||
|
||||
Args:
|
||||
employee_id: 员工账号
|
||||
exclude_device: 要排除的当前设备名
|
||||
|
||||
Returns:
|
||||
List: 其他设备列表
|
||||
"""
|
||||
try:
|
||||
from app.integrations.lianruan.config import get_lianruan_client
|
||||
|
||||
client = await get_lianruan_client(self.db)
|
||||
result = await client.query_dev_by_params(strusername=employee_id, per_page=10)
|
||||
terminals = result.get("items", [])
|
||||
|
||||
other = []
|
||||
for t in terminals:
|
||||
if t.strdevname and t.strdevname != exclude_device:
|
||||
other.append({
|
||||
"device_type": t.strdevtype or "设备",
|
||||
"device_name": t.strdevname,
|
||||
"last_login_time": t.istatus == "1" and "在线" or "离线",
|
||||
"last_login_location": t.strdeptname or "",
|
||||
})
|
||||
|
||||
return other
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"获取其他设备失败: {e}")
|
||||
return []
|
||||
|
||||
# ==========================================================================
|
||||
# 火绒安全数据获取
|
||||
# ==========================================================================
|
||||
|
||||
async def _get_security_from_huorong(
|
||||
self, computer_name: str
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""从火绒查终端安全状态。
|
||||
|
||||
流程:
|
||||
1. list_terminals() 全量分页搜索,按 computer_name 匹配
|
||||
2. 找到后 get_terminal_detail() 获取硬件/资产/网络配置
|
||||
3. list_terminal_leaks() 检查是否在漏洞清单中
|
||||
4. get_virus_events() 查病毒事件统计
|
||||
|
||||
Args:
|
||||
computer_name: 计算机名(联软的 strdevname)
|
||||
|
||||
Returns:
|
||||
Dict: 安全状态字典,火绒不可用时返回 None
|
||||
"""
|
||||
try:
|
||||
from app.integrations.huorong.config import get_huorong_client
|
||||
|
||||
client = await get_huorong_client(self.db)
|
||||
|
||||
# 分页搜索终端,按计算机名匹配
|
||||
target_client_id = None
|
||||
page = 1
|
||||
while page <= 10: # 最多查10页(2000台)
|
||||
result = await client.list_terminals(page=page, per_page=200)
|
||||
items = result.get("items", [])
|
||||
|
||||
for item in items:
|
||||
if item.computer_name and item.computer_name.upper() == computer_name.upper():
|
||||
target_client_id = item.client_id
|
||||
break
|
||||
|
||||
if target_client_id:
|
||||
break
|
||||
|
||||
if len(items) < 200:
|
||||
break # 没有更多数据
|
||||
page += 1
|
||||
|
||||
if not target_client_id:
|
||||
# 火绒中未找到该终端 → 可能未安装火绒
|
||||
return {
|
||||
"huorong_installed": False,
|
||||
"is_online": False,
|
||||
"version": "",
|
||||
"definitions": "",
|
||||
"high_risk_leaks": 0,
|
||||
"virus_count": 0,
|
||||
"virus_uncleaned": 0,
|
||||
}
|
||||
|
||||
# 获取终端详情
|
||||
detail = await client.get_terminal_detail(
|
||||
client_id=target_client_id,
|
||||
optional_fields=["hardware", "assets", "netconf"],
|
||||
)
|
||||
|
||||
# 检查漏洞清单
|
||||
leak_count = 0
|
||||
try:
|
||||
leaks_result = await client.list_terminal_leaks()
|
||||
for leak_item in leaks_result.get("items", []):
|
||||
if leak_item.hostname and leak_item.hostname.upper() == computer_name.upper():
|
||||
leak_count = 1 # 在漏洞清单中说明有高危漏洞
|
||||
break
|
||||
except Exception as e:
|
||||
logger.warning(f"火绒漏洞查询失败: {e}")
|
||||
|
||||
# 查病毒事件
|
||||
virus_count = 0
|
||||
virus_uncleaned = 0
|
||||
try:
|
||||
virus_result = await client.get_virus_events(
|
||||
client_id=target_client_id, type=0
|
||||
)
|
||||
for stat in virus_result.get("items", []):
|
||||
virus_count += stat.count
|
||||
if stat.result:
|
||||
virus_uncleaned += stat.result.fail + stat.result.ignored
|
||||
except Exception as e:
|
||||
logger.warning(f"火绒病毒事件查询失败: {e}")
|
||||
|
||||
return {
|
||||
"huorong_installed": True,
|
||||
"is_online": True, # 从 list_terminals 已确认存在
|
||||
"version": detail.computer_name and "" or "", # 火绒版本从 list 获取
|
||||
"definitions": "",
|
||||
"high_risk_leaks": leak_count,
|
||||
"virus_count": virus_count,
|
||||
"virus_uncleaned": virus_uncleaned,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"火绒获取安全状态失败: {e}")
|
||||
return None
|
||||
|
||||
# ==========================================================================
|
||||
# 资产服务
|
||||
# ==========================================================================
|
||||
|
||||
async def _get_asset_info(self, device_name: str) -> Optional[Dict[str, Any]]:
|
||||
"""从资产服务查设备资产编号和启用时间。
|
||||
|
||||
Args:
|
||||
device_name: 计算机名(用于日志,资产查询通过资产编号)
|
||||
|
||||
Returns:
|
||||
Dict: 资产信息(asset_tag / activate_date),不可用时返回 None
|
||||
"""
|
||||
try:
|
||||
from app.services.asset_service import AssetService
|
||||
|
||||
asset_svc = AssetService()
|
||||
# 资产服务目前通过资产编号查询,设备名无法直接查
|
||||
# 这里先返回 None,后续需要联软 devassetno → 资产编号 → 查询
|
||||
# 或者资产Excel按计算机名匹配
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.warning(f"资产服务查询失败: {e}")
|
||||
return None
|
||||
|
||||
# ==========================================================================
|
||||
# 数据聚合
|
||||
# ==========================================================================
|
||||
|
||||
def _build_current_device(
|
||||
self,
|
||||
device_info: Dict[str, Any],
|
||||
security_info: Optional[Dict[str, Any]],
|
||||
asset_info: Optional[Dict[str, Any]],
|
||||
) -> Dict[str, Any]:
|
||||
"""聚合联软+火绒+资产数据为前端期望的格式。
|
||||
|
||||
前端 BasicInfoCard.vue 期望的数据结构:
|
||||
- device_name / is_online / asset_tag / activate_date
|
||||
- ip_address / public_ip / location / os / mac / uptime
|
||||
- cpu / memory / disks (进度条)
|
||||
- security_checks / compliance_checks (状态数组)
|
||||
|
||||
Args:
|
||||
device_info: 联软设备信息
|
||||
security_info: 火绒安全状态(可能为 None)
|
||||
asset_info: 资产信息(可能为 None)
|
||||
|
||||
Returns:
|
||||
Dict: 前端期望的设备数据结构
|
||||
"""
|
||||
# CPU 使用率(联软不提供实时使用率,用硬件型号代替)
|
||||
cpu_model = ""
|
||||
cpu_usage = 0
|
||||
if device_info.get("cpu_list"):
|
||||
cpu = device_info["cpu_list"][0]
|
||||
cpu_model = f"{cpu.get('vendor', '')} {cpu.get('name', '')}".strip()
|
||||
cpu_usage = 0 # 联软不提供实时使用率
|
||||
|
||||
# 内存总量
|
||||
memory_total = ""
|
||||
memory_usage = 0
|
||||
if device_info.get("memory_list"):
|
||||
mem = device_info["memory_list"][0]
|
||||
memory_total = mem.get("capacity", "") or ""
|
||||
# 联软返回的是硬件容量,不是使用率
|
||||
|
||||
# 磁盘分区
|
||||
disks = []
|
||||
for disk in device_info.get("logical_disks", []):
|
||||
try:
|
||||
usage = int(float(disk.get("usage_percent", "0").replace("%", "").strip() or "0"))
|
||||
except (ValueError, TypeError):
|
||||
usage = 0
|
||||
|
||||
total = disk.get("total", "0")
|
||||
free = disk.get("free", "0")
|
||||
# 计算已用空间
|
||||
used = self._calc_used_space(total, free)
|
||||
|
||||
disks.append({
|
||||
"label": f"硬盘{disk.get('label', 'C盘')}",
|
||||
"usage": usage,
|
||||
"used": used,
|
||||
"total": total,
|
||||
})
|
||||
|
||||
# 安全检查状态数组(6项)
|
||||
security_checks = self._build_security_checks(security_info)
|
||||
|
||||
# 合规检查状态数组(2项)
|
||||
compliance_checks = self._build_compliance_checks(device_info, security_info)
|
||||
|
||||
# 健康评分
|
||||
health_score = self._calc_health_score(security_checks, compliance_checks, device_info.get("is_online", False))
|
||||
|
||||
return {
|
||||
"device_name": device_info.get("device_name", ""),
|
||||
"is_online": device_info.get("is_online", False),
|
||||
"asset_tag": asset_info.get("asset_tag", "") if asset_info else "",
|
||||
"activate_date": asset_info.get("activate_date", "") if asset_info else "",
|
||||
"ip_address": device_info.get("ip_address", ""),
|
||||
"public_ip": "", # 公网出口IP需要额外查询
|
||||
"location": device_info.get("location", ""),
|
||||
"os": device_info.get("os", ""),
|
||||
"mac": device_info.get("mac", ""),
|
||||
"uptime": device_info.get("uptime", ""),
|
||||
"cpu": {"usage": cpu_usage, "model": cpu_model},
|
||||
"memory": {"usage": memory_usage, "total": memory_total},
|
||||
"disks": disks,
|
||||
"security_checks": security_checks,
|
||||
"compliance_checks": compliance_checks,
|
||||
"health_score": health_score,
|
||||
}
|
||||
|
||||
def _build_security_checks(
|
||||
self, security_info: Optional[Dict[str, Any]]
|
||||
) -> List[Dict[str, str]]:
|
||||
"""构建安全检查状态数组(6项)。
|
||||
|
||||
前端期望6个检查项:
|
||||
0. 火绒安装状态
|
||||
1. 系统补丁(高危漏洞)
|
||||
2. 高危软件
|
||||
3. 病毒状态
|
||||
4. 内部攻击(接入中)
|
||||
5. 网络代理(接入中)
|
||||
|
||||
Args:
|
||||
security_info: 火绒安全状态(可能为 None)
|
||||
|
||||
Returns:
|
||||
List: 6个状态对象 [{status: "pass"|"warning"|"danger"|"pending"}]
|
||||
"""
|
||||
if security_info is None:
|
||||
# 火绒未配置 → 全部 pending
|
||||
return [{"status": "pending"}] * 6
|
||||
|
||||
checks = []
|
||||
|
||||
# 0. 火绒安装
|
||||
if security_info.get("huorong_installed"):
|
||||
checks.append({"status": "pass"})
|
||||
else:
|
||||
checks.append({"status": "danger"}) # 未安装火绒 = 危险
|
||||
|
||||
# 1. 系统补丁(高危漏洞)
|
||||
if security_info.get("high_risk_leaks", 0) > 0:
|
||||
checks.append({"status": "danger"})
|
||||
else:
|
||||
checks.append({"status": "pass"})
|
||||
|
||||
# 2. 高危软件(火绒不直接提供,暂返回 pass)
|
||||
checks.append({"status": "pass"})
|
||||
|
||||
# 3. 病毒状态
|
||||
uncleaned = security_info.get("virus_uncleaned", 0)
|
||||
if uncleaned > 0:
|
||||
checks.append({"status": "danger"})
|
||||
elif security_info.get("virus_count", 0) > 0:
|
||||
checks.append({"status": "warning"})
|
||||
else:
|
||||
checks.append({"status": "pass"})
|
||||
|
||||
# 4. 内部攻击(接入中 — 联软尚未对接此数据源)
|
||||
checks.append({"status": "pending"})
|
||||
|
||||
# 5. 网络代理(接入中 — 联软尚未对接此数据源)
|
||||
checks.append({"status": "pending"})
|
||||
|
||||
return checks
|
||||
|
||||
def _build_compliance_checks(
|
||||
self, device_info: Dict[str, Any], security_info: Optional[Dict[str, Any]]
|
||||
) -> List[Dict[str, str]]:
|
||||
"""构建合规检查状态数组(2项)。
|
||||
|
||||
前端期望2个检查项:
|
||||
0. 自备电脑检查
|
||||
1. 未审批商业软件检查
|
||||
|
||||
Args:
|
||||
device_info: 联软设备信息
|
||||
security_info: 火绒安全状态
|
||||
|
||||
Returns:
|
||||
List: 2个状态对象
|
||||
"""
|
||||
# 自备电脑:联软设备类型中如果有"自备"标记则 danger
|
||||
device_type = device_info.get("device_type", "")
|
||||
if "自备" in device_type:
|
||||
return [{"status": "danger"}, {"status": "pass"}]
|
||||
|
||||
# 默认通过
|
||||
return [{"status": "pass"}, {"status": "pass"}]
|
||||
|
||||
def _calc_health_score(
|
||||
self,
|
||||
security_checks: List[Dict[str, str]],
|
||||
compliance_checks: List[Dict[str, str]],
|
||||
is_online: bool,
|
||||
) -> int:
|
||||
"""计算 IT 健康评分(0-100)。
|
||||
|
||||
评分算法(与前端 BasicInfoCard.vue 一致):
|
||||
- 安全项 danger: -15 / warning: -8 / pending: 0
|
||||
- 合规项 danger: -10 / warning: -5
|
||||
- 离线设备权重 60%
|
||||
|
||||
Args:
|
||||
security_checks: 安全检查状态数组
|
||||
compliance_checks: 合规检查状态数组
|
||||
is_online: 设备是否在线
|
||||
|
||||
Returns:
|
||||
int: 健康评分 0-100
|
||||
"""
|
||||
score = 100
|
||||
|
||||
for check in security_checks:
|
||||
status = check.get("status", "pending")
|
||||
if status == "danger":
|
||||
score -= 15
|
||||
elif status == "warning":
|
||||
score -= 8
|
||||
|
||||
for check in compliance_checks:
|
||||
status = check.get("status", "pass")
|
||||
if status == "danger":
|
||||
score -= 10
|
||||
elif status == "warning":
|
||||
score -= 5
|
||||
|
||||
if not is_online:
|
||||
score = round(score * 0.6)
|
||||
|
||||
return max(0, score)
|
||||
|
||||
# ==========================================================================
|
||||
# 工具方法
|
||||
# ==========================================================================
|
||||
|
||||
def _format_uptime(self, last_online_time: str) -> str:
|
||||
"""格式化运行时长。
|
||||
|
||||
联软返回的是最近上线时间字符串,计算距现在的时长。
|
||||
如果无法解析则返回空字符串。
|
||||
|
||||
Args:
|
||||
last_online_time: 联软返回的上线时间字符串
|
||||
|
||||
Returns:
|
||||
str: 如 "12天3小时" 或空字符串
|
||||
"""
|
||||
if not last_online_time:
|
||||
return ""
|
||||
|
||||
try:
|
||||
# 联软时间格式可能是 "2026-07-12 08:30:00" 或类似
|
||||
dt = datetime.strptime(last_online_time.replace("T", " "), "%Y-%m-%d %H:%M:%S")
|
||||
now = datetime.now()
|
||||
delta = now - dt
|
||||
|
||||
days = delta.days
|
||||
hours = delta.seconds // 3600
|
||||
|
||||
if days > 0:
|
||||
return f"{days}天{hours}小时"
|
||||
else:
|
||||
minutes = delta.seconds // 60
|
||||
return f"{minutes}分钟"
|
||||
except (ValueError, TypeError):
|
||||
return ""
|
||||
|
||||
def _calc_used_space(self, total: str, free: str) -> str:
|
||||
"""计算已用空间。
|
||||
|
||||
Args:
|
||||
total: 总容量字符串(如 "256GB")
|
||||
free: 可用空间字符串(如 "86GB")
|
||||
|
||||
Returns:
|
||||
str: 已用空间(如 "170GB")
|
||||
"""
|
||||
try:
|
||||
# 尝试提取数字部分
|
||||
total_num = float("".join(c for c in total if c.isdigit() or c == "."))
|
||||
free_num = float("".join(c for c in free if c.isdigit() or c == "."))
|
||||
|
||||
used_num = total_num - free_num
|
||||
if used_num < 0:
|
||||
used_num = 0
|
||||
|
||||
# 保留单位
|
||||
unit = "".join(c for c in total if c.isalpha())
|
||||
if unit:
|
||||
return f"{int(used_num)}{unit}"
|
||||
return str(int(used_num))
|
||||
except (ValueError, TypeError):
|
||||
return ""
|
||||
|
||||
# ==========================================================================
|
||||
# Mock 数据(联软/火绒未配置时降级)
|
||||
# ==========================================================================
|
||||
|
||||
def _get_mock_data(self, employee_id: str) -> Dict[str, Any]:
|
||||
"""返回 Mock 数据(联软不可用时降级)。
|
||||
|
||||
Args:
|
||||
employee_id: 员工ID(用于日志)
|
||||
|
||||
Returns:
|
||||
Dict: 与真实数据结构一致的 Mock 数据
|
||||
"""
|
||||
return {
|
||||
"current_device": {
|
||||
"device_name": "DESKTOP-MOCK",
|
||||
"is_online": True,
|
||||
"asset_tag": "",
|
||||
"activate_date": "",
|
||||
"ip_address": "10.90.5.x",
|
||||
"public_ip": "218.75.34.87",
|
||||
"location": "待获取",
|
||||
"os": "待获取",
|
||||
"mac": "",
|
||||
"uptime": "",
|
||||
"cpu": {"usage": 0, "model": ""},
|
||||
"memory": {"usage": 0, "total": ""},
|
||||
"disks": [],
|
||||
"security_checks": [{"status": "pending"}] * 6,
|
||||
"compliance_checks": [{"status": "pass"}, {"status": "pass"}],
|
||||
"health_score": 100,
|
||||
},
|
||||
"other_devices": [],
|
||||
"data_source": "mock",
|
||||
"generated_at": datetime.now(timezone.utc).isoformat(),
|
||||
}
|
||||
Reference in New Issue
Block a user