# ============================================================================= # 企微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()