refactor(external_systems): 优化时间参数处理与新增异常能力
1. 将工具、webhook的时间DTO字段从字符串改为datetime类型 2. 移除工具服务中的手动ISO时间解析逻辑 3. 新增CapabilityNotSupportedError异常与get_integration_or_raise方法 4. 格式化代码行内表达式简化写法
This commit is contained in:
parent
00092c818e
commit
1965b9f742
@ -33,7 +33,8 @@ adapters),放在根目录便于统一导入路径。
|
||||
├── SecretResolutionError (400) # 密钥解析失败
|
||||
├── AuthError (403) # 认证失败
|
||||
├── AccessDeniedError (403) # 访问规则拒绝
|
||||
└── WebhookError (400) # Webhook 验签/处理失败
|
||||
├── WebhookError (400) # Webhook 验签/处理失败
|
||||
└── CapabilityNotSupportedError (501) # 适配器未实现可选能力
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@ -448,3 +449,46 @@ class WebhookError(FrameworkError):
|
||||
|
||||
status_code = 400
|
||||
error_code = "WEBHOOK_ERROR"
|
||||
|
||||
|
||||
class CapabilityNotSupportedError(FrameworkError):
|
||||
"""适配器未实现某项可选能力。
|
||||
|
||||
当适配器存在但不实现某项可选能力(如 ``validate_config``)时抛出,
|
||||
由 Router 捕获 Python 内置 ``NotImplementedError`` 后翻译为本异常,
|
||||
交由 ``unified_error_handler`` 统一映射为 HTTP 501,避免原生异常
|
||||
被 ``unhandled_exception_handler`` 兜底为 500。
|
||||
|
||||
与 ``IntegrationOperationNotRegisteredError`` 的区别:本异常表示
|
||||
适配器实例不支持该能力方法;后者表示厂商集成的操作未注册。
|
||||
"""
|
||||
|
||||
status_code = 501
|
||||
error_code = "CAPABILITY_NOT_SUPPORTED"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
capability: str,
|
||||
*,
|
||||
adapter_type: str | None = None,
|
||||
details: dict[str, Any] | None = None,
|
||||
trace_id: str | None = None,
|
||||
) -> None:
|
||||
"""初始化能力未支持异常。
|
||||
|
||||
参数:
|
||||
capability: 未实现的能力名称(如 ``validate_config``)。
|
||||
adapter_type: 适配器类型,写入 ``details`` 供客户端参考。
|
||||
details: 业务字段字典,与上述参数合并。
|
||||
trace_id: 调用链路追踪 ID,用于跨链路关联。
|
||||
"""
|
||||
merged: dict[str, Any] = dict(details or {})
|
||||
if adapter_type is not None:
|
||||
merged["adapter_type"] = adapter_type
|
||||
merged["capability"] = capability
|
||||
adapter_desc = f" {adapter_type}" if adapter_type else ""
|
||||
super().__init__(
|
||||
f"适配器{adapter_desc}未实现能力: {capability}",
|
||||
details=merged,
|
||||
trace_id=trace_id,
|
||||
)
|
||||
|
||||
@ -13,7 +13,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from yuxi.external_systems.exceptions import DomainValidationError
|
||||
from yuxi.external_systems.exceptions import DomainValidationError, EntityNotFoundError
|
||||
from yuxi.external_systems.integrations.schemas import IntegrationMetadata
|
||||
|
||||
|
||||
@ -84,9 +84,7 @@ class IntegrationRegistry:
|
||||
分页基于 filter_integrations 的全量结果切片,全量总数请用
|
||||
count_integrations 获取(list 返回的 len(items) 仅为当前页条数)。
|
||||
"""
|
||||
filtered = cls.filter_integrations(
|
||||
adapter_type=adapter_type, tags=tags, keyword=keyword
|
||||
)
|
||||
filtered = cls.filter_integrations(adapter_type=adapter_type, tags=tags, keyword=keyword)
|
||||
if offset < 0:
|
||||
offset = 0
|
||||
if limit < 0:
|
||||
@ -106,17 +104,24 @@ class IntegrationRegistry:
|
||||
与 list_integrations 共用 filter_integrations,保证 total 与
|
||||
分页结果一致。供 Router 层返回真实全量 total,避免前端分页页数错误。
|
||||
"""
|
||||
return len(
|
||||
cls.filter_integrations(
|
||||
adapter_type=adapter_type, tags=tags, keyword=keyword
|
||||
)
|
||||
)
|
||||
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]:
|
||||
"""列出所有已注册厂商集成涉及的适配器类型(去重)。"""
|
||||
|
||||
@ -11,6 +11,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
@ -506,8 +507,8 @@ class GetToolExecutionTrendInput(BaseModel):
|
||||
model_config = ConfigDict(frozen=True)
|
||||
|
||||
tool_id: int
|
||||
start_time: str | None = None
|
||||
end_time: str | None = None
|
||||
start_time: datetime | None = None
|
||||
end_time: datetime | None = None
|
||||
interval: str = "day"
|
||||
env_key: str | None = None
|
||||
|
||||
|
||||
@ -7,6 +7,7 @@ dataclass 对齐。
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
@ -124,8 +125,8 @@ class ListWebhookEventsInput(BaseModel):
|
||||
triggered_execution_id: str | None = None
|
||||
signature_verified: bool | None = None
|
||||
source_ip: str | None = None
|
||||
start: Any | None = None
|
||||
end: Any | None = None
|
||||
start: datetime | None = None
|
||||
end: datetime | None = None
|
||||
|
||||
|
||||
class GetEventInput(BaseModel):
|
||||
|
||||
@ -25,7 +25,6 @@ tool_router 扩展端点(版本对比 / 版本回滚 / 参数模板 / 批量
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from yuxi.external_systems.core.contracts import (
|
||||
@ -384,11 +383,7 @@ class ToolService(ToolServicePort):
|
||||
adapter = self._registry.get_adapter(tool.adapter_type)
|
||||
# 系统级工具预解析 context 供 adapter 构造 schema(如 gRPC asset 加载 proto);
|
||||
# 工具级工具 context 为 None(契约允许)。
|
||||
context = (
|
||||
await self._context_builder.build(tool, env)
|
||||
if tool.system_id is not None
|
||||
else None
|
||||
)
|
||||
context = await self._context_builder.build(tool, env) if tool.system_id is not None else None
|
||||
schema_tool = await adapter.build_tool(tool, context)
|
||||
env_dict = env or {}
|
||||
|
||||
@ -994,7 +989,8 @@ class ToolService(ToolServicePort):
|
||||
async with self._uow:
|
||||
# 2. 回滚前创建当前配置的新版本快照(记录回滚前的状态)
|
||||
new_version_id = await self._record_version_before_update(
|
||||
tool, input_dto.updated_by,
|
||||
tool,
|
||||
input_dto.updated_by,
|
||||
)
|
||||
|
||||
# 3. 将目标版本明文 snapshot 写回 ext_tool_configs
|
||||
@ -1198,12 +1194,12 @@ class ToolService(ToolServicePort):
|
||||
details={"interval": input_dto.interval},
|
||||
)
|
||||
|
||||
start = self._parse_iso_datetime(input_dto.start_time)
|
||||
end = self._parse_iso_datetime(input_dto.end_time)
|
||||
start = input_dto.start_time
|
||||
end = input_dto.end_time
|
||||
if start is not None and end is not None and start > end:
|
||||
raise DomainValidationError(
|
||||
"start_time 不能晚于 end_time",
|
||||
details={"start_time": input_dto.start_time, "end_time": input_dto.end_time},
|
||||
details={"start_time": start.isoformat(), "end_time": end.isoformat()},
|
||||
)
|
||||
|
||||
rows = await self._repos.execution.aggregate_trend_by_tool(
|
||||
@ -1269,21 +1265,3 @@ class ToolService(ToolServicePort):
|
||||
|
||||
items = [to_asset_output(a) for a in assets]
|
||||
return GetToolAssetsOutput(items=items)
|
||||
|
||||
# ========== 内部辅助(扩展端点) ==========
|
||||
|
||||
@staticmethod
|
||||
def _parse_iso_datetime(value: str | None) -> datetime | None:
|
||||
"""解析 ISO 8601 时间字符串,返回 ``datetime`` 或 ``None``。
|
||||
|
||||
解析失败时抛 ``DomainValidationError``,不静默回退。
|
||||
"""
|
||||
if value is None:
|
||||
return None
|
||||
try:
|
||||
return datetime.fromisoformat(value.replace("Z", "+00:00"))
|
||||
except ValueError as exc:
|
||||
raise DomainValidationError(
|
||||
f"时间格式非法: {value}(需 ISO 8601 格式)",
|
||||
details={"value": value},
|
||||
) from exc
|
||||
|
||||
Loading…
Reference in New Issue
Block a user