ForcePilot/backend/package/yuxi/external_systems/integrations/registry.py
Kris 00a5577aac chore: 整理并合并近期多模块迭代变更
本次提交包含了大量跨模块的功能增强、bug修复与代码优化:
1.  清理冗余空行与导入格式,统一代码风格
2.  新增集成超时异常类,替换原生TimeoutError避免穿透核心层
3.  扩展审计日志DTO与用例,新增变更字段追踪能力
4.  完善各类仓储接口,新增批量操作、时间范围过滤支持
5.  重构健康检查返回格式,统一使用status字段替代冗余的reachable/healthy
6.  扩展仪表盘与各类服务端口,新增待办统计、系统dashboard等能力
7.  优化批量操作与事务处理,新增保存点支持隔离失败操作
8.  修复配额管理阈值计算逻辑,支持自定义告警阈值
9.  统一认证类型错误提示,优化插件注册校验逻辑
10. 扩展SOAP适配器预览能力,兼容bytes类型输入
11. 新增批量恢复、批量硬删除回收站资源的接口与DTO
12. 优化测试用例断言逻辑,兼容前端操作符别名与字段名差异
2026-07-11 21:38:08 +08:00

147 lines
5.9 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.

"""IntegrationRegistry厂商集成目录注册表。
实现 ``core/contracts.py::IntegrationRegistry`` 端口(结构化子类型,无需显式继承)。
维护 ``key -> IntegrationMetadata`` 映射,应用启动时由各集成包 ``__init__.py``
通过 ``register()`` 自注册,运行时只读。元数据为内存态,不持久化到数据库。
``clear()`` 仅供测试隔离使用。
键冲突策略:同一 ``key`` 重复注册且元数据实例不同时抛 ``DomainValidationError``
与 framework 层 ``register_protocol_auth_plugin`` / ``http_error_extractor_registry``
等注册表策略一致,避免静默覆盖。
"""
from __future__ import annotations
from yuxi.external_systems.exceptions import DomainValidationError, EntityNotFoundError
from yuxi.external_systems.integrations.schemas import IntegrationMetadata
class IntegrationRegistry:
"""厂商集成目录注册表,实现 core/contracts.py::IntegrationRegistry 端口。
实现 Protocol 的 3 个查询方法list_integrations / get_integration /
list_adapter_types并额外提供 register / clear 供框架内部使用
(与 framework/registry/adapter_registry.py::AdapterRegistry 模式一致)。
"""
_integrations: dict[str, IntegrationMetadata] = {}
@classmethod
def register(cls, metadata: IntegrationMetadata) -> None:
"""注册厂商集成元数据。键冲突抛 DomainValidationError。"""
existing = cls._integrations.get(metadata.key)
if existing is not None and existing is not metadata:
raise DomainValidationError(
f"厂商集成元数据冲突: key={metadata.key} "
f"已注册 {existing.display_name},尝试注册 {metadata.display_name}"
)
cls._integrations[metadata.key] = metadata
@classmethod
def filter_integrations(
cls,
*,
adapter_type: str | None = None,
tags: list[str] | None = None,
keyword: str | None = None,
) -> list[IntegrationMetadata]:
"""按条件过滤并稳定排序厂商集成(不分页)。
list_integrations 与 count_integrations 的共用底层方法,
保证过滤逻辑单一来源避免两处实现漂移。overview / categories
端点需要全量过滤结果时也直接调用本方法。
- adapter_type: 精确匹配 metadata.adapter_type
- tags: OR 关系(命中任一标签即返回)
- keyword: 模糊匹配 key / display_name / description大小写不敏感
- 按 key 升序稳定排序,保证分页结果可重现
"""
items = list(cls._integrations.values())
if adapter_type is not None:
items = [it for it in items if it.adapter_type == adapter_type]
if tags:
tag_set = set(tags)
items = [it for it in items if tag_set.intersection(it.tags or ())]
if keyword:
kw = keyword.lower()
items = [
it
for it in items
if kw in it.key.lower() or kw in (it.display_name or "").lower() or kw in (it.description or "").lower()
]
items.sort(key=lambda it: it.key)
return items
@classmethod
def list_integrations(
cls,
*,
adapter_type: str | None = None,
tags: list[str] | None = None,
keyword: str | None = None,
limit: int = 50,
offset: int = 0,
) -> list[IntegrationMetadata]:
"""查询厂商集成目录,支持按适配器类型/标签/关键词过滤与分页。
分页基于 filter_integrations 的全量结果切片,全量总数请用
count_integrations 获取list 返回的 len(items) 仅为当前页条数)。
"""
filtered = cls.filter_integrations(adapter_type=adapter_type, tags=tags, keyword=keyword)
if offset < 0:
offset = 0
if limit < 0:
limit = 0
return filtered[offset : offset + limit]
@classmethod
def count_integrations(
cls,
*,
adapter_type: str | None = None,
tags: list[str] | None = None,
keyword: str | None = None,
) -> int:
"""统计满足筛选条件的厂商集成全量总数(不分页)。
与 list_integrations 共用 filter_integrations保证 total 与
分页结果一致。供 Router 层返回真实全量 total避免前端分页页数错误。
"""
return len(cls.filter_integrations(adapter_type=adapter_type, tags=tags, keyword=keyword))
@classmethod
def get_integration(cls, key: str) -> IntegrationMetadata | None:
"""按 key 获取厂商集成元数据,未找到返回 None。"""
return cls._integrations.get(key)
@classmethod
def get_integration_or_raise(cls, key: str) -> IntegrationMetadata:
"""按 key 获取厂商集成元数据,未找到抛 ``EntityNotFoundError`` (404)。
供 Router 的详情类端点使用,避免每个端点重复 None 判断与抛异常逻辑。
"""
metadata = cls._integrations.get(key)
if metadata is None:
raise EntityNotFoundError(f"厂商集成 {key} 不存在")
return metadata
@classmethod
def list_adapter_types(cls) -> list[str]:
"""列出所有已注册厂商集成涉及的适配器类型(去重,按字母升序排序)。
与 ``filter_integrations``(按 key 排序)和 ``list_operations``(按字母
排序)保持一致的排序契约:所有 list_* 方法均返回排序后的结果,
调用方无需重复排序。
"""
seen: dict[str, None] = {}
for it in cls._integrations.values():
if it.adapter_type not in seen:
seen[it.adapter_type] = None
return sorted(seen.keys())
@classmethod
def clear(cls) -> None:
"""清空注册表,仅供测试使用。"""
cls._integrations.clear()