Files
wecom_it_smart_desk/backend/app/services/automation/action_registry.py
T

204 lines
7.7 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智能服务台 — 阶段5 自动化 动作适配器注册表
# =============================================================================
# 说明:每个场景动作由对应适配器(handler)真正执行。适配器复用阶段1-4 已落
# 地的集成客户端(Huorong / Lianruan / EHR),对 aTrust(零信任)等
# 密钥未到的系统提供安全的占位实现(明确报错,不静默成功)。
#
# 本期 P0 四个场景适配器:
# - terminal_locate → LianruanClient.query_dev_by_params(员工→终端映射,read
# - virus_scan / virus_quarantine → HuorongClient(扫描/隔离,high
# - software_install_guide → 内部(推送软件安装指引链接,low)
# - password_reset_link → 内部(推送密码重置链接,high,需员工 H5 确认)
# =============================================================================
from __future__ import annotations
import logging
from dataclasses import dataclass, field
from typing import Any, Awaitable, Callable, Dict, Optional
from app.constants import AutomationErrorCode
from app.integrations.base import BaseClientError
from app.services.automation.exception_handler import AutomationException
logger = logging.getLogger(__name__)
# 审计回调签名
AuditFn = Optional[Callable[..., Awaitable[None]]]
@dataclass
class ActionContext:
"""动作执行上下文,传递给各适配器。"""
db: Any
session: Any
action: Any
mapping: Dict[str, Any] = field(default_factory=dict)
clients: Dict[str, Any] = field(default_factory=dict)
employee_id: str = ""
audit: AuditFn = None
class BaseActionHandler:
"""动作适配器基类。"""
# 语义动作类型,用于注册表索引
action_type: str = ""
async def execute(self, ctx: ActionContext) -> Dict[str, Any]:
"""执行动作,返回结果 dict(会被写入 AutoAction.result)。
Raises:
AutomationException: 业务错误(映射失败/配置缺失等)
BaseClientError: 外部系统调用失败(executor 捕获后转 EXTERNAL_CALL_FAILED
"""
raise NotImplementedError
class TerminalLocateHandler(BaseActionHandler):
"""终端定位:通过联软按员工账号查终端(read)。"""
action_type = "terminal_locate"
async def execute(self, ctx: ActionContext) -> Dict[str, Any]:
client = ctx.clients.get("lianruan")
if client is None:
# 联软不可用 → 转 EHR 兜底(映射解析器已尝试,这里直接报错)
raise AutomationException(
AutomationErrorCode.MAPPING_FAILED,
"联软映射客户端未配置,无法定位终端",
)
data = await client.query_dev_by_params(strusername=ctx.employee_id)
items = data.get("items", []) if isinstance(data, dict) else []
terminals = [
{
"strdevname": getattr(t, "strdevname", ""),
"strdevip": getattr(t, "strdevip", ""),
"strusername": getattr(t, "strusername", ""),
"strdeptname": getattr(t, "strdeptname", ""),
}
for t in items
]
return {
"source": "lianruan",
"employee_id": ctx.employee_id,
"terminals": terminals,
"total": len(terminals),
}
class VirusScanHandler(BaseActionHandler):
"""病毒扫描:对映射到的终端发起快速扫描(low)。"""
action_type = "virus_scan"
async def execute(self, ctx: ActionContext) -> Dict[str, Any]:
client_ids = (ctx.mapping or {}).get("client_ids") or []
if not client_ids:
raise AutomationException(
AutomationErrorCode.MAPPING_FAILED, "未解析到目标终端,无法扫描"
)
client = ctx.clients.get("huorong")
if client is None:
raise AutomationException(
AutomationErrorCode.CONFIG_ERROR, "火绒客户端未配置,无法执行病毒扫描"
)
result = await client.create_scan_task(client_ids=client_ids, scan_type="quick_scan")
return {"client_ids": client_ids, "task": result}
class VirusQuarantineHandler(BaseActionHandler):
"""病毒查杀/隔离:隔离目标终端(high,需审批)。"""
action_type = "virus_quarantine"
async def execute(self, ctx: ActionContext) -> Dict[str, Any]:
client_ids = (ctx.mapping or {}).get("client_ids") or []
if not client_ids:
raise AutomationException(
AutomationErrorCode.MAPPING_FAILED, "未解析到目标终端,无法隔离"
)
client = ctx.clients.get("huorong")
if client is None:
raise AutomationException(
AutomationErrorCode.CONFIG_ERROR, "火绒客户端未配置,无法隔离终端"
)
result = await client.isolate_terminal(client_ids=client_ids)
return {"client_ids": client_ids, "task": result, "isolated": True}
class SoftwareInstallHandler(BaseActionHandler):
"""软件自安装指引:构造安装指引与下载链接(low,内部)。"""
action_type = "software_install_guide"
async def execute(self, ctx: ActionContext) -> Dict[str, Any]:
payload = ctx.action.payload or {}
software_name = payload.get("software_name", "")
download_url = payload.get("download_url", "")
guide = (
f"请按以下步骤自助安装「{software_name or '所需软件'}」:\n"
"1. 打开下载链接并完成安装;\n"
"2. 如提示需要管理员权限,请在企业微信中发起「软件安装申请」审批;\n"
"3. 安装完成后重启应用即可使用。"
)
return {
"software_name": software_name,
"download_url": download_url,
"guide": guide,
}
class PasswordResetHandler(BaseActionHandler):
"""密码重置:构造密码重置链接(high,需员工 H5 确认,内部)。"""
action_type = "password_reset_link"
async def execute(self, ctx: ActionContext) -> Dict[str, Any]:
payload = ctx.action.payload or {}
reset_link = payload.get("reset_link", "")
# aTrust(零信任)密钥未到,本期以「重置链接推送」形式落地:
# 链接由管理端配置(AUTOMATION_PASSWORD_RESET_URL),缺省给出占位说明。
if not reset_link:
reset_link = "about:blank#password-reset-not-configured"
return {
"employee_id": ctx.employee_id,
"reset_link": reset_link,
"note": "密码重置链接已生成,需员工在 H5 二次确认后推送至本人。",
}
# --------------------------------------------------------------------------
# 注册表
# --------------------------------------------------------------------------
HANDLER_REGISTRY: Dict[str, BaseActionHandler] = {}
def _register(handler: BaseActionHandler) -> None:
HANDLER_REGISTRY[handler.action_type] = handler
def register_builtin_handlers() -> None:
"""注册内置动作适配器(幂等)。"""
for cls in (
TerminalLocateHandler,
VirusScanHandler,
VirusQuarantineHandler,
SoftwareInstallHandler,
PasswordResetHandler,
):
_register(cls())
def get_handler(action_type: str) -> Optional[BaseActionHandler]:
"""按动作类型取适配器;未注册返回 None。"""
if not HANDLER_REGISTRY:
register_builtin_handlers()
return HANDLER_REGISTRY.get(action_type)
# 模块导入即注册
register_builtin_handlers()