"""外部系统目录、用户可见授权和密文连接的持久化服务层。""" from __future__ import annotations from datetime import datetime, timezone from typing import Any, Optional from urllib.parse import urlparse from uuid import UUID from sqlalchemy import delete, exists, or_, select from sqlalchemy.exc import IntegrityError from core.storage import session_scope from core.storage.models import ExternalSystem, ExternalSystemDefinition, User from .crypto import configured as crypto_configured from .crypto import decrypt_secret, encrypt_secret, mask_username from .factory import FactoryMesClient, FactoryMesConfig, FactoryMesError class ExternalSystemError(RuntimeError): pass def _factory_config(data: dict[str, Any]) -> FactoryMesConfig: try: return FactoryMesConfig.from_mapping(data) except (FactoryMesError, TypeError, ValueError) as exc: raise ExternalSystemError(str(exc)) from exc def _normalized_config(data: dict[str, Any]) -> dict[str, Any]: cfg = _factory_config(data) return { "base_url": cfg.base_url, "openapi_url": cfg.openapi_url, "login_path": cfg.login_path, "allowed_post_operations": sorted(cfg.allowed_post_operations), "timeout_seconds": cfg.timeout_seconds, "max_result_bytes": cfg.max_result_bytes, "verify_tls": cfg.verify_tls, } def _definition_view(row: ExternalSystemDefinition, *, include_config: bool) -> dict[str, Any]: config = row.config or {} result = { "definition_id": str(row.definition_id), "provider": row.provider, "name": row.name, "enabled": row.enabled, "access_mode": row.access_mode, "host": urlparse(str(config.get("base_url") or "")).hostname or "", "created_at": row.created_at.isoformat() if row.created_at else None, "updated_at": row.updated_at.isoformat() if row.updated_at else None, } if include_config: result["config"] = config return result def _validate_access_mode(access_mode: str) -> str: mode = (access_mode or "selected").strip().lower() if mode not in {"all", "selected"}: raise ExternalSystemError("access_mode 必须是 all 或 selected") return mode def _selected_user_ids(s: Any, definition_id: UUID) -> list[str]: return [ str(uid) for uid in s.execute( select(ExternalSystem.user_id) .where(ExternalSystem.definition_id == definition_id) .order_by(ExternalSystem.user_id) ).scalars().all() ] def _sync_selected_users( s: Any, definition: ExternalSystemDefinition, selected_user_ids: list[UUID], ) -> None: wanted = set(selected_user_ids) if wanted: existing_users = set(s.execute( select(User.user_id).where(User.user_id.in_(wanted)) ).scalars().all()) missing = wanted - existing_users if missing: raise ExternalSystemError("包含不存在的用户: " + ", ".join(sorted(map(str, missing)))) current_rows = s.execute( select(ExternalSystem).where( ExternalSystem.definition_id == definition.definition_id ) ).scalars().all() current = {row.user_id: row for row in current_rows} for uid, row in current.items(): if uid not in wanted: s.delete(row) # 撤权同时删除该用户的密文凭据 for uid in wanted - set(current): s.add(ExternalSystem( user_id=uid, definition_id=definition.definition_id, provider=definition.provider, connector="openapi", name=definition.name, credentials={}, config={}, status="pending", )) def provider_catalog(user_id: UUID) -> list[dict[str, Any]]: try: with session_scope() as s: rows = s.execute( select(ExternalSystemDefinition) .where( ExternalSystemDefinition.provider == "factory_mes", ExternalSystemDefinition.enabled.is_(True), or_( ExternalSystemDefinition.access_mode == "all", exists( select(ExternalSystem.external_system_id).where( ExternalSystem.definition_id == ExternalSystemDefinition.definition_id, ExternalSystem.user_id == user_id, ) ), ), ) .order_by(ExternalSystemDefinition.name) ).scalars().all() definitions = [_definition_view(row, include_config=False) for row in rows] except Exception: definitions = [] key_ok = crypto_configured() return [{ "provider": "factory_mes", "title": "Factory MES", "connector": "openapi", "configured": bool(definitions and key_ok), "reason": "" if key_ok else "ZCBOT_CREDENTIAL_MASTER_KEY 未配置或少于 32 字符", "definitions": definitions, }] def list_external_system_definitions() -> list[dict[str, Any]]: with session_scope() as s: rows = s.execute( select(ExternalSystemDefinition).order_by(ExternalSystemDefinition.name) ).scalars().all() results = [] for row in rows: item = _definition_view(row, include_config=True) item["selected_user_ids"] = _selected_user_ids(s, row.definition_id) results.append(item) return results def create_external_system_definition( admin_user_id: UUID, *, provider: str, name: str, config: dict[str, Any], enabled: bool = True, access_mode: str = "selected", selected_user_ids: Optional[list[UUID]] = None, ) -> dict[str, Any]: provider = (provider or "").strip() name = (name or "").strip() if provider != "factory_mes": raise ExternalSystemError("首版只支持 factory_mes") if not name or len(name) > 80: raise ExternalSystemError("系统名称不能为空且不能超过 80 字符") row = ExternalSystemDefinition( provider=provider, name=name, config=_normalized_config(config), enabled=bool(enabled), access_mode=_validate_access_mode(access_mode), created_by=admin_user_id, ) try: with session_scope() as s: s.add(row) s.flush() if row.access_mode == "selected": _sync_selected_users(s, row, selected_user_ids or []) s.flush() result = _definition_view(row, include_config=True) result["selected_user_ids"] = _selected_user_ids(s, row.definition_id) return result except IntegrityError as exc: raise ExternalSystemError("同名外部系统定义已存在") from exc def update_external_system_definition( definition_id: UUID, *, name: str, config: dict[str, Any], enabled: bool, access_mode: str, selected_user_ids: Optional[list[UUID]] = None, ) -> dict[str, Any]: name = (name or "").strip() if not name or len(name) > 80: raise ExternalSystemError("系统名称不能为空且不能超过 80 字符") try: with session_scope() as s: row = s.execute( select(ExternalSystemDefinition).where( ExternalSystemDefinition.definition_id == definition_id ) ).scalar_one_or_none() if row is None: raise ExternalSystemError("external system definition not found") row.name = name row.config = _normalized_config(config) row.enabled = bool(enabled) row.access_mode = _validate_access_mode(access_mode) if row.access_mode == "selected": _sync_selected_users(s, row, selected_user_ids or []) s.flush() result = _definition_view(row, include_config=True) result["selected_user_ids"] = _selected_user_ids(s, row.definition_id) return result except IntegrityError as exc: raise ExternalSystemError("同名外部系统定义已存在") from exc def delete_external_system_definition(definition_id: UUID) -> bool: try: with session_scope() as s: result = s.execute( delete(ExternalSystemDefinition).where( ExternalSystemDefinition.definition_id == definition_id ) ) return bool(result.rowcount) except IntegrityError as exc: raise ExternalSystemError("该系统已有用户连接,请先停用而不是删除") from exc def get_definition(definition_id: UUID, *, enabled_only: bool = False) -> ExternalSystemDefinition: with session_scope() as s: stmt = select(ExternalSystemDefinition).where( ExternalSystemDefinition.definition_id == definition_id ) if enabled_only: stmt = stmt.where(ExternalSystemDefinition.enabled.is_(True)) row = s.execute(stmt).scalar_one_or_none() if row is None: raise ExternalSystemError("external system definition not found") s.expunge(row) return row def get_definition_for_user(user_id: UUID, definition_id: UUID) -> ExternalSystemDefinition: with session_scope() as s: row = s.execute( select(ExternalSystemDefinition).where( ExternalSystemDefinition.definition_id == definition_id, ExternalSystemDefinition.enabled.is_(True), or_( ExternalSystemDefinition.access_mode == "all", exists( select(ExternalSystem.external_system_id).where( ExternalSystem.definition_id == definition_id, ExternalSystem.user_id == user_id, ) ), ), ) ).scalar_one_or_none() if row is None: raise ExternalSystemError("external system definition not found") s.expunge(row) return row def _client( provider: str, username: str, password: str, config: dict[str, Any] ) -> FactoryMesClient: if provider != "factory_mes": raise ExternalSystemError(f"unsupported external system provider: {provider}") return FactoryMesClient(username, password, _factory_config(config)) def _credentials(username: str, password: str) -> dict[str, str]: username = (username or "").strip() if not username or not password: raise ExternalSystemError("用户名和密码不能为空") try: return {"username": encrypt_secret(username), "password": encrypt_secret(password)} except (RuntimeError, ValueError) as exc: raise ExternalSystemError(str(exc)) from exc def credentials_for(row: ExternalSystem) -> tuple[str, str]: try: return ( decrypt_secret(row.credentials["username"]), decrypt_secret(row.credentials["password"]), ) except (KeyError, RuntimeError) as exc: raise ExternalSystemError(str(exc)) from exc def client_for_external_system(row: ExternalSystem) -> FactoryMesClient: definition = get_definition_for_user(row.user_id, row.definition_id) username, password = credentials_for(row) return _client(definition.provider, username, password, definition.config or {}) def _view(row: ExternalSystem, definition: ExternalSystemDefinition) -> dict[str, Any]: try: username, _ = credentials_for(row) masked = mask_username(username) credential_ok = True except ExternalSystemError: masked = "***" credential_ok = False return { "external_system_id": str(row.external_system_id), "definition_id": str(row.definition_id), "system_name": definition.name, "provider": definition.provider, "connector": row.connector, "name": row.name, "status": row.status if definition.enabled else "disabled", "username_masked": masked, "credential_configured": credential_ok, "last_verified_at": row.last_verified_at.isoformat() if row.last_verified_at else None, "created_at": row.created_at.isoformat() if row.created_at else None, "updated_at": row.updated_at.isoformat() if row.updated_at else None, } def list_external_systems(user_id: UUID) -> list[dict[str, Any]]: with session_scope() as s: rows = s.execute( select(ExternalSystem, ExternalSystemDefinition) .join( ExternalSystemDefinition, ExternalSystemDefinition.definition_id == ExternalSystem.definition_id, ) .where(ExternalSystem.user_id == user_id) .where(ExternalSystem.status != "pending") .order_by(ExternalSystem.created_at) ).all() return [_view(row, definition) for row, definition in rows] def get_external_system(user_id: UUID, system_id: UUID, *, active_only: bool = False) -> ExternalSystem: with session_scope() as s: stmt = select(ExternalSystem).where( ExternalSystem.external_system_id == system_id, ExternalSystem.user_id == user_id, ) if active_only: stmt = stmt.where(ExternalSystem.status == "active") row = s.execute(stmt).scalar_one_or_none() if row is None: raise ExternalSystemError("external system not found") s.expunge(row) return row def create_external_system( user_id: UUID, *, definition_id: UUID, name: str, username: str, password: str, ) -> dict[str, Any]: if not crypto_configured(): raise ExternalSystemError("ZCBOT_CREDENTIAL_MASTER_KEY 未配置或少于 32 字符") definition = get_definition_for_user(user_id, definition_id) name = (name or definition.name).strip() if not name or len(name) > 80: raise ExternalSystemError("连接名称不能为空且不能超过 80 字符") try: probe = _client(definition.provider, username.strip(), password, definition.config).test_connection() except FactoryMesError as exc: raise ExternalSystemError(str(exc)) from exc try: with session_scope() as s: row = s.execute( select(ExternalSystem).where( ExternalSystem.user_id == user_id, ExternalSystem.definition_id == definition.definition_id, ) ).scalar_one_or_none() if row is not None and row.status != "pending": raise ExternalSystemError("该 MES 已连接,请使用更新凭据") if row is None: row = ExternalSystem( user_id=user_id, definition_id=definition.definition_id, provider=definition.provider, connector="openapi", ) s.add(row) row.name = name row.credentials = _credentials(username, password) row.config = {"operation_count": probe.get("operation_count", 0)} row.status = "active" row.last_verified_at = datetime.now(timezone.utc) s.flush() return _view(row, definition) except IntegrityError as exc: raise ExternalSystemError("同名 MES 连接已存在") from exc def update_external_system_credentials( user_id: UUID, system_id: UUID, *, username: str, password: str ) -> dict[str, Any]: row = get_external_system(user_id, system_id) definition = get_definition(row.definition_id, enabled_only=True) try: probe = _client(definition.provider, username.strip(), password, definition.config).test_connection() except FactoryMesError as exc: raise ExternalSystemError(str(exc)) from exc with session_scope() as s: current = s.execute( select(ExternalSystem).where( ExternalSystem.external_system_id == system_id, ExternalSystem.user_id == user_id, ) ).scalar_one() current.credentials = _credentials(username, password) current.config = {**(current.config or {}), "operation_count": probe.get("operation_count", 0)} current.status = "active" current.last_verified_at = datetime.now(timezone.utc) s.flush() return _view(current, definition) def test_external_system(user_id: UUID, system_id: UUID) -> dict[str, Any]: row = get_external_system(user_id, system_id) ok, error, probe = False, "", {} try: probe = client_for_external_system(row).test_connection() ok = True except (ExternalSystemError, FactoryMesError) as exc: error = str(exc) with session_scope() as s: current = s.execute( select(ExternalSystem).where( ExternalSystem.external_system_id == system_id, ExternalSystem.user_id == user_id, ) ).scalar_one() current.status = "active" if ok else "invalid" if ok: current.last_verified_at = datetime.now(timezone.utc) current.config = {**(current.config or {}), **probe} return {"ok": ok, "error": error if not ok else "", **probe} def delete_external_system(user_id: UUID, system_id: UUID) -> bool: with session_scope() as s: row = s.execute( select(ExternalSystem).where( ExternalSystem.external_system_id == system_id, ExternalSystem.user_id == user_id, ) ).scalar_one_or_none() if row is None: return False definition = s.execute( select(ExternalSystemDefinition).where( ExternalSystemDefinition.definition_id == row.definition_id ) ).scalar_one() if definition.access_mode == "selected": # 用户断开只清凭据,保留管理员授予的可见权。 row.credentials = {} row.config = {} row.status = "pending" row.last_verified_at = None else: s.delete(row) return True def external_system_tools_available(user_id: UUID) -> bool: if not crypto_configured(): return False try: with session_scope() as s: return s.execute( select(ExternalSystem.external_system_id) .join( ExternalSystemDefinition, ExternalSystemDefinition.definition_id == ExternalSystem.definition_id, ) .where( ExternalSystem.user_id == user_id, ExternalSystem.status == "active", ExternalSystemDefinition.enabled.is_(True), ) .limit(1) ).scalar_one_or_none() is not None except Exception: return False