WIP-CHECKPOINT[auth-refactor]: 固化工程师崩溃前部分成果 + 同树其他未提交WIP(仅源码,不含密钥/二进制)-- 待重激活工程师续作
This commit is contained in:
@@ -0,0 +1,203 @@
|
||||
# =============================================================================
|
||||
# 企微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()
|
||||
Reference in New Issue
Block a user