204 lines
7.7 KiB
Python
204 lines
7.7 KiB
Python
|
|
# =============================================================================
|
|||
|
|
# 企微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()
|