"""外部系统集成相关的 PostgreSQL 数据模型。 表名统一以 `ext_` 为前缀;字段类型复用 `yuxi.storage.postgres.base` 中的 `Base` 与 `JSON_VALUE`。所有表统一包含审计字段(created_by/updated_by/ created_at/updated_at/is_deleted/deleted_at),支持软删除与操作追溯。 """ from __future__ import annotations import base64 from typing import Any from sqlalchemy import ( BigInteger, Boolean, Column, DateTime, ForeignKey, Index, Integer, LargeBinary, String, Text, UniqueConstraint, func, select, ) from sqlalchemy.dialects.postgresql import JSON, JSONB from sqlalchemy.orm import column_property from yuxi.storage.postgres.models_business import Base from yuxi.utils.datetime_utils import format_utc_datetime, utc_now_naive JSON_VALUE = JSON().with_variant(JSONB, "postgresql") class ExternalToolConfig(Base): """外部系统工具配置(通用模型,HTTP 为首个适配器实现)。""" __tablename__ = "ext_tool_configs" # 主键 id = Column(Integer, primary_key=True, autoincrement=True) # 基础信息 slug = Column(String(128), nullable=False, unique=True, index=True, comment="工具唯一标识") name = Column(String(128), nullable=False, comment="展示名称") description = Column(Text, nullable=False, comment="工具描述(给 LLM 看)") category = Column(String(64), nullable=False, default="default", comment="展示分类") adapter_type = Column(String(32), nullable=False, default="http", comment="适配器类型:http/grpc/...") enabled = Column(Boolean, nullable=False, default=True, comment="是否启用") # 通用执行配置 timeout = Column(Integer, nullable=False, default=30, comment="超时时间(秒)") retry_policy = Column(JSON_VALUE, nullable=False, default=dict, comment="重试策略") auth_type = Column(String(32), nullable=False, default="none", comment="认证类型") auth_config = Column(JSON_VALUE, nullable=True, comment="认证配置(敏感字段加密存储)") # 适配器专属配置(首期由 HTTP 适配器使用) adapter_config = Column(JSON_VALUE, nullable=False, default=dict, comment="适配器专属配置") # 关联关系 system_id = Column( Integer, ForeignKey("ext_systems.id", ondelete="SET NULL"), nullable=True, index=True, comment="所属系统 ID(为空时自包含运行)", ) # 审计字段 created_by = Column(String(64), nullable=True) updated_by = Column(String(64), nullable=True) created_at = Column(DateTime, nullable=False, default=utc_now_naive) updated_at = Column(DateTime, nullable=False, default=utc_now_naive, onupdate=utc_now_naive) is_deleted = Column(Integer, nullable=False, default=0, index=True) deleted_at = Column(DateTime, nullable=True) __table_args__ = ( Index("ix_ext_tool_configs_system_id_enabled", "system_id", "enabled"), Index("ix_ext_tool_configs_adapter_type_enabled", "adapter_type", "enabled"), Index("ix_ext_tool_configs_category_enabled", "category", "enabled"), ) def to_dict(self) -> dict[str, Any]: """序列化为字典;敏感字段的加解密由 persistence 层承接。""" return { "id": self.id, "slug": self.slug, "name": self.name, "description": self.description, "category": self.category, "adapter_type": self.adapter_type, "enabled": self.enabled, "timeout": self.timeout, "retry_policy": self.retry_policy or {}, "auth_type": self.auth_type, "auth_config": self.auth_config or {}, "adapter_config": self.adapter_config or {}, "system_id": self.system_id, "created_by": self.created_by, "updated_by": self.updated_by, "created_at": format_utc_datetime(self.created_at), "updated_at": format_utc_datetime(self.updated_at), } class ExternalToolConfigVersion(Base): """外部工具配置版本历史;每次保存时记录完整快照。""" __tablename__ = "ext_tool_config_versions" # 主键 id = Column(Integer, primary_key=True, autoincrement=True) # 关联关系 tool_slug = Column( String(128), ForeignKey("ext_tool_configs.slug", ondelete="CASCADE", onupdate="CASCADE"), nullable=False, index=True, comment="工具 slug", ) # 版本信息 version_id = Column(String(64), nullable=False, comment="展示版本号,如 2026-06-15-001") snapshot = Column(JSON_VALUE, nullable=False, comment="完整工具配置快照(敏感字段已加密)") # 审计字段 created_by = Column(String(64), nullable=True) updated_by = Column(String(64), nullable=True) created_at = Column(DateTime, nullable=False, default=utc_now_naive) updated_at = Column(DateTime, nullable=False, default=utc_now_naive, onupdate=utc_now_naive) is_deleted = Column(Integer, nullable=False, default=0, index=True) deleted_at = Column(DateTime, nullable=True) __table_args__ = (UniqueConstraint("tool_slug", "version_id", name="uq_ext_tool_config_versions_tool_version"),) def to_dict(self) -> dict[str, Any]: """序列化为 API 响应字典;snapshot 中的敏感字段保持加密状态,由服务层按需解密。""" return { "id": self.id, "tool_slug": self.tool_slug, "version_id": self.version_id, "snapshot": self.snapshot or {}, "created_by": self.created_by, "updated_by": self.updated_by, "created_at": format_utc_datetime(self.created_at), "updated_at": format_utc_datetime(self.updated_at), } class ExternalAdapterAsset(Base): """外部适配器资产(如 proto/reflection/wsdl/openapi 等)。""" __tablename__ = "ext_adapter_assets" # 主键 id = Column(Integer, primary_key=True, autoincrement=True) # 资产基础信息 adapter_type = Column(String(32), nullable=False, index=True, comment="适配器类型:http/grpc/soap/...") asset_type = Column(String(32), nullable=False, comment="资产类型:proto/reflection/wsdl/openapi/...") name = Column(String(256), nullable=False, comment="资产名称(通常取自上传文件名)") content = Column(LargeBinary, nullable=False, comment="资产原始内容(二进制)") size = Column(Integer, default=0, comment="内容字节数") checksum = Column(String(64), nullable=True, comment="内容 SHA-256 校验和") # 状态信息 status = Column( String(32), nullable=False, default="active", index=True, comment="资产状态:active/parse_failed/inactive", ) status_message = Column(String(512), nullable=True, comment="状态说明,如解析失败原因") # 审计字段 created_by = Column(String(64), nullable=True) updated_by = Column(String(64), nullable=True) created_at = Column(DateTime, nullable=False, default=utc_now_naive, index=True) updated_at = Column(DateTime, nullable=False, default=utc_now_naive, onupdate=utc_now_naive) is_deleted = Column(Integer, nullable=False, default=0, index=True) deleted_at = Column(DateTime, nullable=True) def to_dict(self) -> dict[str, Any]: """序列化为字典;content 以 base64 编码返回,便于 JSON 传输。""" content_b64 = base64.b64encode(self.content).decode("utf-8") if self.content else "" return { "id": self.id, "adapter_type": self.adapter_type, "asset_type": self.asset_type, "name": self.name, "content": content_b64, "size": self.size, "checksum": self.checksum, "status": self.status, "status_message": self.status_message, "created_by": self.created_by, "updated_by": self.updated_by, "created_at": format_utc_datetime(self.created_at), "updated_at": format_utc_datetime(self.updated_at), } def to_list_dict(self) -> dict[str, Any]: """序列化为列表项字典;不包含 content 字段,避免大体积响应。""" return { "id": self.id, "adapter_type": self.adapter_type, "asset_type": self.asset_type, "name": self.name, "size": self.size, "checksum": self.checksum, "status": self.status, "status_message": self.status_message, "created_by": self.created_by, "updated_by": self.updated_by, "created_at": format_utc_datetime(self.created_at), "updated_at": format_utc_datetime(self.updated_at), } class ExternalSystem(Base): """外部系统一级实体。""" __tablename__ = "ext_systems" # 主键 id = Column(Integer, primary_key=True, autoincrement=True) # 基础信息 slug = Column(String(128), nullable=False, unique=True, index=True, comment="系统唯一标识") name = Column(String(128), nullable=False, comment="展示名称") description = Column(Text, nullable=False, comment="系统描述") category = Column(String(64), nullable=False, default="default", comment="展示分类") adapter_type = Column(String(32), nullable=False, comment="适配器类型") source_type = Column(String(64), nullable=True, comment="渠道 source_type,用于统一操作分派") enabled = Column(Boolean, nullable=False, default=True, comment="是否启用") # 连接与认证配置 connection_config = Column(JSON_VALUE, nullable=False, default=dict, comment="系统级连接配置") auth_type = Column(String(32), nullable=False, default="none", comment="认证类型") auth_config = Column(JSON_VALUE, nullable=True, comment="认证配置(敏感字段加密存储)") secret_refs = Column(JSON_VALUE, nullable=False, default=dict, comment="具名 secret 引用") # 运维治理配置 rate_limit = Column(JSON_VALUE, nullable=False, default=dict, comment="限流配置") circuit_breaker = Column(JSON_VALUE, nullable=False, default=dict, comment="熔断配置") pool_config = Column(JSON_VALUE, nullable=False, default=dict, comment="连接池配置") observability = Column(JSON_VALUE, nullable=False, default=dict, comment="可观测性配置") # 通用执行配置 timeout = Column(Integer, nullable=False, default=30, comment="超时时间(秒)") retry_policy = Column(JSON_VALUE, nullable=False, default=dict, comment="重试策略") # 审计字段 created_by = Column(String(64), nullable=True) updated_by = Column(String(64), nullable=True) created_at = Column(DateTime, nullable=False, default=utc_now_naive) updated_at = Column(DateTime, nullable=False, default=utc_now_naive, onupdate=utc_now_naive) is_deleted = Column(Integer, nullable=False, default=0, index=True) deleted_at = Column(DateTime, nullable=True) def to_dict(self) -> dict[str, Any]: return { "id": self.id, "slug": self.slug, "name": self.name, "description": self.description, "category": self.category, "adapter_type": self.adapter_type, "source_type": self.source_type, "enabled": self.enabled, "connection_config": self.connection_config or {}, "auth_type": self.auth_type, "auth_config": self.auth_config or {}, "secret_refs": self.secret_refs or {}, "rate_limit": self.rate_limit or {}, "circuit_breaker": self.circuit_breaker or {}, "pool_config": self.pool_config or {}, "observability": self.observability or {}, "timeout": self.timeout, "retry_policy": self.retry_policy or {}, "env_count": self.env_count or 0, "created_by": self.created_by, "updated_by": self.updated_by, "created_at": format_utc_datetime(self.created_at), "updated_at": format_utc_datetime(self.updated_at), } class ExternalSystemEnvironment(Base): """外部系统环境配置(dev / staging / prod)。""" __tablename__ = "ext_system_environments" # 主键 id = Column(Integer, primary_key=True, autoincrement=True) # 关联关系 system_id = Column( Integer, ForeignKey("ext_systems.id", ondelete="CASCADE"), nullable=False, index=True, comment="所属系统 ID", ) # 环境基础信息 env_key = Column(String(32), nullable=False, comment="环境标识") name = Column(String(128), nullable=False, comment="环境名称") is_default = Column(Boolean, nullable=False, default=False, comment="是否默认环境") enabled = Column(Boolean, nullable=False, default=True, comment="是否启用") # 环境级配置 connection_config = Column(JSON_VALUE, nullable=False, default=dict, comment="环境级连接配置") auth_config = Column(JSON_VALUE, nullable=True, comment="环境级认证配置(加密存储)") # 审计字段 created_by = Column(String(64), nullable=True) updated_by = Column(String(64), nullable=True) created_at = Column(DateTime, nullable=False, default=utc_now_naive) updated_at = Column(DateTime, nullable=False, default=utc_now_naive, onupdate=utc_now_naive) is_deleted = Column(Integer, nullable=False, default=0, index=True) deleted_at = Column(DateTime, nullable=True) __table_args__ = ( UniqueConstraint("system_id", "env_key", name="uq_ext_system_environments_system_env"), # 每个外部系统只能有一个默认环境 Index( "uq_ext_system_environments_default", "system_id", unique=True, postgresql_where=is_default.is_(True), ), ) def to_dict(self) -> dict[str, Any]: return { "id": self.id, "system_id": self.system_id, "env_key": self.env_key, "name": self.name, "connection_config": self.connection_config or {}, "auth_config": self.auth_config or {}, "is_default": self.is_default, "enabled": self.enabled, "created_by": self.created_by, "updated_by": self.updated_by, "created_at": format_utc_datetime(self.created_at), "updated_at": format_utc_datetime(self.updated_at), } # 外部系统环境数:使用关联子查询,避免列表查询时出现 N+1 问题 ExternalSystem.env_count = column_property( select(func.count(ExternalSystemEnvironment.id)) .where(ExternalSystemEnvironment.system_id == ExternalSystem.id) .correlate(ExternalSystem) .scalar_subquery() ) class ExternalSystemToken(Base): """外部系统 Token 持久化。""" __tablename__ = "ext_system_tokens" # 主键 id = Column(Integer, primary_key=True, autoincrement=True) # 关联关系 system_id = Column( Integer, ForeignKey("ext_systems.id", ondelete="CASCADE"), nullable=False, comment="所属系统 ID", ) # Token 基础信息 env_key = Column(String(32), nullable=False, default="default", comment="环境标识") token_type = Column(String(32), nullable=False, comment="Token 类型") access_token = Column(Text, nullable=False, comment="Access Token(加密存储)") refresh_token = Column(Text, nullable=True, comment="Refresh Token(加密存储)") expires_at = Column(DateTime, nullable=True, comment="过期时间") extra = Column(JSON_VALUE, nullable=False, default=dict, comment="额外信息") # 状态信息 is_invalidated = Column(Boolean, nullable=False, default=False, comment="是否已主动失效") # 审计字段 created_by = Column(String(64), nullable=True) updated_by = Column(String(64), nullable=True) created_at = Column(DateTime, nullable=False, default=utc_now_naive) updated_at = Column(DateTime, nullable=False, default=utc_now_naive, onupdate=utc_now_naive) is_deleted = Column(Integer, nullable=False, default=0, index=True) deleted_at = Column(DateTime, nullable=True) __table_args__ = (UniqueConstraint("system_id", "env_key", name="uq_ext_system_tokens_system_env"),) def to_dict(self) -> dict[str, Any]: """序列化为字典;不返回 Token 明文,仅返回是否存在。 Token 值加密存储于 ``access_token`` / ``refresh_token``,模型层 ``to_dict`` 不直接暴露密文或明文;需要明文的场景由服务层 (如 ``token_service.get_token_detail``)按权限解密后返回。 """ return { "id": self.id, "system_id": self.system_id, "env_key": self.env_key, "token_type": self.token_type, "has_access_token": bool(self.access_token), "has_refresh_token": bool(self.refresh_token), "expires_at": format_utc_datetime(self.expires_at) if self.expires_at else None, "extra": self.extra or {}, "is_invalidated": self.is_invalidated, "created_by": self.created_by, "updated_by": self.updated_by, "created_at": format_utc_datetime(self.created_at), "updated_at": format_utc_datetime(self.updated_at), } class ExternalSystemMetric(Base): """外部系统调用指标(按时间桶聚合)。""" __tablename__ = "ext_system_metrics" # 主键 id = Column(BigInteger, primary_key=True, autoincrement=True) # 关联关系 system_id = Column( Integer, ForeignKey("ext_systems.id", ondelete="CASCADE"), nullable=False, comment="所属系统 ID", ) # 桶维度 env_key = Column(String(32), nullable=False, default="default", comment="环境标识") bucket_at = Column(DateTime, nullable=False, comment="时间桶起始时间") # 调用计数 total_calls = Column(Integer, nullable=False, default=0) success_calls = Column(Integer, nullable=False, default=0) failed_calls = Column(Integer, nullable=False, default=0) auth_failed_calls = Column(Integer, nullable=False, default=0) ssrf_blocked_calls = Column(Integer, nullable=False, default=0) timeout_calls = Column(Integer, nullable=False, default=0) # 延迟统计 p99_latency_ms = Column(Integer, nullable=True, comment="当前桶的近似峰值延迟") latency_sum_ms = Column( BigInteger, nullable=False, default=0, comment="当前桶调用延迟总和(毫秒),用于原子累加计算真实平均值" ) latency_count = Column(Integer, nullable=False, default=0, comment="当前桶参与统计的调用次数") # Token 与限流统计 token_refresh_count = Column(Integer, nullable=False, default=0) token_refresh_failed = Column(Integer, nullable=False, default=0) throttled_count = Column(Integer, nullable=False, default=0) circuit_open_count = Column(Integer, nullable=False, default=0) # 审计字段 created_by = Column(String(64), nullable=True) updated_by = Column(String(64), nullable=True) created_at = Column(DateTime, nullable=False, default=utc_now_naive) updated_at = Column(DateTime, nullable=False, default=utc_now_naive, onupdate=utc_now_naive) is_deleted = Column(Integer, nullable=False, default=0, index=True) deleted_at = Column(DateTime, nullable=True) __table_args__ = (UniqueConstraint("system_id", "env_key", "bucket_at", name="uq_ext_system_metrics_bucket"),) def to_dict(self) -> dict[str, Any]: """序列化为 API 响应字典;avg_latency_ms 由 latency_sum_ms/latency_count 计算,避免存储不一致。""" latency_count = self.latency_count or 0 latency_sum_ms = self.latency_sum_ms or 0 avg_latency_ms = round(latency_sum_ms / latency_count, 2) if latency_count > 0 else 0.0 return { "id": self.id, "system_id": self.system_id, "env_key": self.env_key, "bucket_at": format_utc_datetime(self.bucket_at), "total_calls": self.total_calls, "success_calls": self.success_calls, "failed_calls": self.failed_calls, "auth_failed_calls": self.auth_failed_calls, "ssrf_blocked_calls": self.ssrf_blocked_calls, "timeout_calls": self.timeout_calls, "p99_latency_ms": self.p99_latency_ms, "latency_sum_ms": latency_sum_ms, "latency_count": latency_count, "avg_latency_ms": avg_latency_ms, "token_refresh_count": self.token_refresh_count, "token_refresh_failed": self.token_refresh_failed, "throttled_count": self.throttled_count, "circuit_open_count": self.circuit_open_count, "created_by": self.created_by, "updated_by": self.updated_by, "created_at": format_utc_datetime(self.created_at), "updated_at": format_utc_datetime(self.updated_at), } class ExternalSystemAuditLog(Base): """外部系统审计日志。 记录系统级变更、Token 失效、缓存清理等运维事件;detail 中不应包含密钥、 Token、密码等敏感信息,由调用方在写入前完成脱敏。 """ __tablename__ = "ext_system_audit_logs" # 主键 id = Column(BigInteger, primary_key=True, autoincrement=True) # 关联关系 system_id = Column( Integer, ForeignKey("ext_systems.id", ondelete="SET NULL"), nullable=True, index=True, comment="所属系统 ID", ) # 事件基础信息 env_key = Column(String(32), nullable=True, comment="环境标识") event_type = Column(String(64), nullable=False, index=True, comment="事件类型") action_type = Column( String(64), nullable=True, index=True, comment="操作类型:CREATE/UPDATE/DELETE/ROTATE_SECRET/INVALIDATE_TOKEN/EXPORT/BULK_OPERATION/AUTH/EXECUTE/OTHER", ) resource = Column(String(256), nullable=True, comment="操作对象,如系统:crm_prod") user = Column(String(64), nullable=True, comment="操作人") # 事件详情 detail = Column(JSON_VALUE, nullable=False, default=dict, comment="事件详情(已脱敏)") snapshot_before = Column(JSON_VALUE, nullable=True, comment="变更前快照(已脱敏)") snapshot_after = Column(JSON_VALUE, nullable=True, comment="变更后快照(已脱敏)") description = Column(Text, nullable=True, comment="操作备注/描述") # 审计字段 created_by = Column(String(64), nullable=True) updated_by = Column(String(64), nullable=True) created_at = Column(DateTime, nullable=False, default=utc_now_naive, index=True) updated_at = Column(DateTime, nullable=False, default=utc_now_naive, onupdate=utc_now_naive) is_deleted = Column(Integer, nullable=False, default=0, index=True) deleted_at = Column(DateTime, nullable=True) __table_args__ = ( Index("ix_ext_system_audit_logs_system_id_created_at", "system_id", "created_at"), Index("ix_ext_system_audit_logs_action_type_created_at", "action_type", "created_at"), Index("ix_ext_system_audit_logs_user_created_at", "user", "created_at"), Index("ix_ext_system_audit_logs_env_key_created_at", "env_key", "created_at"), ) def to_dict(self) -> dict[str, Any]: """序列化为 API 响应字典。""" return { "id": self.id, "system_id": self.system_id, "env_key": self.env_key, "event_type": self.event_type, "action_type": self.action_type, "resource": self.resource, "user": self.user, "detail": self.detail or {}, "snapshot_before": self.snapshot_before, "snapshot_after": self.snapshot_after, "description": self.description, "created_by": self.created_by, "updated_by": self.updated_by, "created_at": format_utc_datetime(self.created_at), "updated_at": format_utc_datetime(self.updated_at), } class ExternalToolExecution(Base): """外部系统工具单次执行记录。""" __tablename__ = "ext_tool_executions" # 主键 id = Column(BigInteger, primary_key=True, autoincrement=True) # 执行标识 execution_id = Column(String(64), nullable=False, unique=True, index=True, comment="单次执行唯一标识") trace_id = Column(String(64), nullable=False, index=True, comment="调用链路唯一标识") correlation_id = Column(String(64), nullable=True, index=True, comment="业务关联 ID,如 Agent 运行 ID") # 关联关系 system_id = Column(Integer, nullable=False, index=True, comment="所属系统 ID") env_key = Column(String(32), nullable=False, default="default", comment="环境标识") tool_slug = Column(String(128), nullable=False, index=True, comment="工具 slug") # 调用方信息 caller = Column(String(32), nullable=False, comment="调用方:agent/user/workflow/simulate/sync_job/retry") caller_id = Column(String(64), nullable=True, comment="调用方实体 ID") # 执行状态与时间 status = Column( String(32), nullable=False, index=True, comment="pending/running/success/failed/timeout/throttled/cancelled" ) started_at = Column(DateTime, nullable=False, index=True, comment="开始时间") ended_at = Column(DateTime, nullable=True, comment="结束时间") duration_ms = Column(Integer, nullable=True, comment="耗时毫秒") # 操作类型 operation = Column(String(32), nullable=False, default="execute", comment="execute/health_check/test/simulate") # 请求/响应摘要(已脱敏) request_summary = Column(JSON_VALUE, nullable=False, default=dict, comment="请求摘要(已脱敏)") response_summary = Column(JSON_VALUE, nullable=True, comment="响应摘要(已脱敏)") error = Column(JSON_VALUE, nullable=True, comment="错误信息") # 重试与标签 retry_of = Column(String(64), nullable=True, index=True, comment="重试来源 execution_id") retry_count = Column(Integer, nullable=False, default=0, comment="第几次重试") tags = Column(JSON_VALUE, nullable=False, default=dict, comment="业务标签") # 审计字段 created_by = Column(String(64), nullable=True) updated_by = Column(String(64), nullable=True) created_at = Column(DateTime, nullable=False, default=utc_now_naive) updated_at = Column(DateTime, nullable=False, default=utc_now_naive, onupdate=utc_now_naive) is_deleted = Column(Integer, nullable=False, default=0, index=True) deleted_at = Column(DateTime, nullable=True) __table_args__ = ( Index("ix_ext_tool_executions_system_started", "system_id", "started_at"), Index("ix_ext_tool_executions_tool_started", "tool_slug", "started_at"), Index("ix_ext_tool_executions_status_started", "status", "started_at"), ) def to_dict(self, *, tool_name: str | None = None) -> dict[str, Any]: """序列化为 API 响应字典。 为前端列表/详情展示提供与字段名语义一致的别名: - execution_time -> started_at - environment -> env_key - tool_name 默认回退到 tool_slug,可由服务层传入真实工具名覆盖。 """ return { "execution_id": self.execution_id, "trace_id": self.trace_id, "correlation_id": self.correlation_id, "system_id": self.system_id, "env_key": self.env_key, "environment": self.env_key, "tool_slug": self.tool_slug, "tool_name": tool_name if tool_name is not None else self.tool_slug, "caller": self.caller, "caller_id": self.caller_id, "status": self.status, "operation": self.operation, "started_at": format_utc_datetime(self.started_at), "execution_time": format_utc_datetime(self.started_at), "ended_at": format_utc_datetime(self.ended_at) if self.ended_at else None, "duration_ms": self.duration_ms, "request_summary": self.request_summary or {}, "response_summary": self.response_summary, "error": self.error, "retry_of": self.retry_of, "retry_count": self.retry_count, "tags": self.tags or {}, "created_by": self.created_by, "updated_by": self.updated_by, "created_at": format_utc_datetime(self.created_at), "updated_at": format_utc_datetime(self.updated_at), } class ExternalToolExecutionTrace(Base): """外部系统工具执行调用链路。""" __tablename__ = "ext_execution_traces" # 主键 id = Column(BigInteger, primary_key=True, autoincrement=True) # 关联关系 execution_id = Column(String(64), nullable=False, unique=True, comment="关联执行记录") trace_id = Column(String(64), nullable=False, index=True, comment="调用链路 ID") # 链路数据 spans = Column(JSON_VALUE, nullable=False, comment="完整 Span 树") total_duration_ms = Column(Integer, nullable=True, comment="总耗时") # 审计字段 created_by = Column(String(64), nullable=True) updated_by = Column(String(64), nullable=True) created_at = Column(DateTime, nullable=False, default=utc_now_naive) updated_at = Column(DateTime, nullable=False, default=utc_now_naive, onupdate=utc_now_naive) is_deleted = Column(Integer, nullable=False, default=0, index=True) deleted_at = Column(DateTime, nullable=True) def to_dict(self) -> dict[str, Any]: """序列化为 API 响应字典。""" return { "execution_id": self.execution_id, "trace_id": self.trace_id, "total_duration_ms": self.total_duration_ms, "spans": self.spans or [], "created_by": self.created_by, "updated_by": self.updated_by, "created_at": format_utc_datetime(self.created_at), "updated_at": format_utc_datetime(self.updated_at), } class ExternalSystemHealthCheck(Base): """外部系统健康检查历史记录。 每个外部系统保留最近一次健康检查结果,用于全局健康检查页的一览展示。 """ __tablename__ = "ext_system_health_checks" # 主键 id = Column(BigInteger, primary_key=True, autoincrement=True) # 关联关系 system_id = Column( Integer, ForeignKey("ext_systems.id", ondelete="CASCADE"), nullable=False, index=True, comment="所属系统 ID", ) env_key = Column(String(32), nullable=False, default="default", comment="环境标识") # 探测结果 health_status = Column( String(32), nullable=False, default="not_checked", comment="健康状态:healthy/auth_failure/connectivity_issue/not_checked", ) duration_ms = Column(Integer, nullable=True, comment="探测耗时毫秒") detail = Column(Text, nullable=True, comment="探测结果说明") error_code = Column(String(64), nullable=True, comment="错误码") error_message = Column(Text, nullable=True, comment="错误信息") checked_at = Column(DateTime, nullable=True, index=True, comment="探测时间") # 审计字段 created_by = Column(String(64), nullable=True) updated_by = Column(String(64), nullable=True) created_at = Column(DateTime, nullable=False, default=utc_now_naive) updated_at = Column(DateTime, nullable=False, default=utc_now_naive, onupdate=utc_now_naive) is_deleted = Column(Integer, nullable=False, default=0, index=True) deleted_at = Column(DateTime, nullable=True) __table_args__ = ( UniqueConstraint("system_id", "env_key", name="uq_ext_system_health_checks_system_env"), Index("ix_ext_system_health_checks_status", "health_status"), Index("ix_ext_system_health_checks_checked_at", "checked_at"), ) def to_dict(self) -> dict[str, Any]: """序列化为 API 响应字典。""" return { "id": self.id, "system_id": self.system_id, "env_key": self.env_key, "health_status": self.health_status, "duration_ms": self.duration_ms, "detail": self.detail, "error_code": self.error_code, "error_message": self.error_message, "checked_at": format_utc_datetime(self.checked_at) if self.checked_at else None, "created_by": self.created_by, "updated_by": self.updated_by, "created_at": format_utc_datetime(self.created_at), "updated_at": format_utc_datetime(self.updated_at), } class ExternalWebhookSubscription(Base): """外部系统事件订阅(Webhook 入站)。 承载 Yuxi 对外部系统事件的订阅关系:哪个系统/环境的什么事件、由哪个 handler(agent/workflow)处理,以及外部系统返回的订阅 ID、验签密钥、 过期与续期信息。与 ExternalToolExecution 的"主动调用"链路互补,共同 覆盖双向集成场景。 """ __tablename__ = "ext_webhook_subscriptions" # 主键 id = Column(Integer, primary_key=True, autoincrement=True) # 基础信息 slug = Column(String(128), nullable=False, unique=True, index=True, comment="订阅唯一标识,用于回调路径") name = Column(String(128), nullable=False, comment="展示名称") description = Column(Text, nullable=False, default="", comment="订阅描述") # 关联关系 system_id = Column( Integer, ForeignKey("ext_systems.id", ondelete="CASCADE"), nullable=False, index=True, comment="所属外部系统 ID", ) env_key = Column(String(32), nullable=False, default="default", comment="环境标识") # 事件订阅信息 source_event_type = Column( String(128), nullable=False, index=True, comment="外部事件类型,如 salesforce.account.updated" ) subscription_id = Column(String(256), nullable=True, index=True, comment="外部系统返回的订阅 ID") # 处理方信息 target_handler_type = Column(String(32), nullable=False, comment="处理方类型:agent/workflow/webhook_forward") target_handler_id = Column(String(64), nullable=True, comment="处理方实体 ID") # 回调与验签 callback_path = Column(String(256), nullable=False, comment="Yuxi 接收路径,如 /api/external/webhooks/{slug}") secret = Column(Text, nullable=True, comment="验签密钥(加密存储,to_dict 不暴露明文)") secret_algorithm = Column(String(32), nullable=False, default="hmac_sha256", comment="验签算法") # 过滤与转换规则 filter_rules = Column(JSON_VALUE, nullable=False, default=dict, comment="事件过滤规则") transform_rules = Column(JSON_VALUE, nullable=False, default=dict, comment="事件转换规则") # 状态信息 status = Column( String(32), nullable=False, default="pending", comment="订阅状态:pending/active/paused/expired/disabled", ) enabled = Column(Boolean, nullable=False, default=True, comment="是否启用") # 过期与续期 expires_at = Column(DateTime, nullable=True, index=True, comment="外部系统订阅过期时间") auto_renew = Column(Boolean, nullable=False, default=True, comment="是否自动续期") last_renewed_at = Column(DateTime, nullable=True, comment="上次续期时间") last_renew_error = Column(Text, nullable=True, comment="上次续期错误信息") renewal_attempts = Column(Integer, nullable=False, default=0, comment="续期连续失败次数") # 接收统计 last_received_at = Column(DateTime, nullable=True, comment="最近一次收到事件时间") received_count = Column(BigInteger, nullable=False, default=0, comment="累计收到事件数") retention_days = Column(Integer, nullable=False, default=30, comment="事件保留天数,超期由清理任务删除") # 审计字段 created_by = Column(String(64), nullable=True) updated_by = Column(String(64), nullable=True) created_at = Column(DateTime, nullable=False, default=utc_now_naive) updated_at = Column(DateTime, nullable=False, default=utc_now_naive, onupdate=utc_now_naive) is_deleted = Column(Integer, nullable=False, default=0, index=True) deleted_at = Column(DateTime, nullable=True) __table_args__ = ( Index("ix_ext_webhook_subscriptions_system_env_event", "system_id", "env_key", "source_event_type"), Index("ix_ext_webhook_subscriptions_status_enabled", "status", "enabled"), ) def to_dict(self) -> dict[str, Any]: """序列化为 API 响应字典;secret 仅返回是否存在,明文由服务层按权限解密。""" return { "id": self.id, "slug": self.slug, "name": self.name, "description": self.description, "system_id": self.system_id, "env_key": self.env_key, "source_event_type": self.source_event_type, "subscription_id": self.subscription_id, "target_handler_type": self.target_handler_type, "target_handler_id": self.target_handler_id, "callback_path": self.callback_path, "has_secret": bool(self.secret), "secret_algorithm": self.secret_algorithm, "filter_rules": self.filter_rules or {}, "transform_rules": self.transform_rules or {}, "status": self.status, "enabled": self.enabled, "expires_at": format_utc_datetime(self.expires_at) if self.expires_at else None, "auto_renew": self.auto_renew, "last_renewed_at": format_utc_datetime(self.last_renewed_at) if self.last_renewed_at else None, "last_renew_error": self.last_renew_error, "renewal_attempts": self.renewal_attempts, "last_received_at": format_utc_datetime(self.last_received_at) if self.last_received_at else None, "received_count": self.received_count, "retention_days": self.retention_days, "created_by": self.created_by, "updated_by": self.updated_by, "created_at": format_utc_datetime(self.created_at), "updated_at": format_utc_datetime(self.updated_at), } class ExternalWebhookEvent(Base): """Webhook 事件落地。 外部系统推送的原始事件持久化,支持基于 external_event_id 的幂等去重、 重放、关联到智能体运行。payload/headers 由调用方在写入前完成脱敏, 不得包含密钥、Token 等敏感信息。 """ __tablename__ = "ext_webhook_events" # 主键 id = Column(BigInteger, primary_key=True, autoincrement=True) # 关联关系 subscription_id = Column( Integer, ForeignKey("ext_webhook_subscriptions.id", ondelete="CASCADE"), nullable=False, index=True, comment="所属订阅 ID", ) subscription_slug = Column(String(128), nullable=False, index=True, comment="订阅 slug(冗余,便于查询)") # 事件基础信息 external_event_id = Column(String(256), nullable=True, index=True, comment="外部系统事件 ID,用于幂等去重") event_type = Column(String(128), nullable=False, index=True, comment="事件类型") payload = Column(JSON_VALUE, nullable=False, comment="事件载荷(已脱敏)") payload_hash = Column( String(64), nullable=True, index=True, comment="payload SHA-256,用于无 external_event_id 时的去重" ) headers = Column(JSON_VALUE, nullable=False, default=dict, comment="请求头(已脱敏)") signature = Column(String(512), nullable=True, comment="请求签名") signature_verified = Column(Boolean, nullable=False, default=False, comment="是否验签通过") # 接收信息 received_at = Column(DateTime, nullable=False, index=True, comment="收到时间") source_ip = Column(String(64), nullable=True, comment="来源 IP") # 处理状态 processing_status = Column( String(32), nullable=False, default="pending", index=True, comment="处理状态:pending/processing/processed/failed/ignored/duplicate", ) processing_attempts = Column(Integer, nullable=False, default=0, comment="处理尝试次数") last_processed_at = Column(DateTime, nullable=True, comment="上次处理时间") processing_error = Column(Text, nullable=True, comment="处理错误信息") # 触发关联 correlation_id = Column(String(64), nullable=True, index=True, comment="业务关联 ID,如触发的 agent_run_id") triggered_handler_type = Column(String(32), nullable=True, comment="实际触发的处理方类型") triggered_handler_id = Column(String(64), nullable=True, comment="实际触发的处理方 ID") triggered_execution_id = Column(String(64), nullable=True, index=True, comment="触发的执行记录 ID") # 审计字段 created_by = Column(String(64), nullable=True) updated_by = Column(String(64), nullable=True) created_at = Column(DateTime, nullable=False, default=utc_now_naive) updated_at = Column(DateTime, nullable=False, default=utc_now_naive, onupdate=utc_now_naive) is_deleted = Column(Integer, nullable=False, default=0, index=True) deleted_at = Column(DateTime, nullable=True) __table_args__ = ( UniqueConstraint("subscription_id", "external_event_id", name="uq_ext_webhook_events_sub_ext_event"), Index("ix_ext_webhook_events_received_status", "received_at", "processing_status"), ) def to_dict(self) -> dict[str, Any]: """序列化为 API 响应字典。""" return { "id": self.id, "subscription_id": self.subscription_id, "subscription_slug": self.subscription_slug, "external_event_id": self.external_event_id, "event_type": self.event_type, "payload": self.payload or {}, "payload_hash": self.payload_hash, "headers": self.headers or {}, "signature": self.signature, "signature_verified": self.signature_verified, "received_at": format_utc_datetime(self.received_at), "source_ip": self.source_ip, "processing_status": self.processing_status, "processing_attempts": self.processing_attempts, "last_processed_at": format_utc_datetime(self.last_processed_at) if self.last_processed_at else None, "processing_error": self.processing_error, "correlation_id": self.correlation_id, "triggered_handler_type": self.triggered_handler_type, "triggered_handler_id": self.triggered_handler_id, "triggered_execution_id": self.triggered_execution_id, "created_by": self.created_by, "updated_by": self.updated_by, "created_at": format_utc_datetime(self.created_at), "updated_at": format_utc_datetime(self.updated_at), } class ExternalSystemQuotaUsage(Base): """外部系统 API 配额跟踪。 按时间窗口跟踪外部系统 API 配额使用情况(如 Salesforce 24h 限额、 Microsoft Graph 每日限额)。配额值可从响应头解析或手动配置,用于 调用前预检与接近上限时降级。 """ __tablename__ = "ext_system_quota_usage" # 主键 id = Column(Integer, primary_key=True, autoincrement=True) # 关联关系 system_id = Column( Integer, ForeignKey("ext_systems.id", ondelete="CASCADE"), nullable=False, index=True, comment="所属外部系统 ID", ) env_key = Column(String(32), nullable=False, default="default", comment="环境标识") # 配额定义 quota_key = Column(String(128), nullable=False, comment="配额项标识,如 salesforce.api_calls.24h") quota_name = Column(String(128), nullable=False, comment="配额展示名称") quota_window = Column(String(32), nullable=False, comment="窗口类型:daily/hourly/rolling_24h/minute/custom") window_start = Column(DateTime, nullable=True, comment="窗口起始时间") window_end = Column(DateTime, nullable=True, index=True, comment="窗口结束时间(重置时间)") # 配额数值 limit_value = Column(BigInteger, nullable=False, default=0, comment="配额上限") used_value = Column(BigInteger, nullable=False, default=0, comment="已用值") unit = Column(String(32), nullable=False, default="calls", comment="配额单位:calls/bytes/records") # 配额来源 source = Column(String(32), nullable=False, default="config", comment="配额来源:response_header/manual/config") source_header_name = Column(String(128), nullable=True, comment="响应头字段名,如 X-RateLimit-Remaining") # 告警阈值 warning_threshold = Column(Integer, nullable=False, default=80, comment="告警阈值百分比(0-100)") critical_threshold = Column(Integer, nullable=False, default=95, comment="严重告警阈值百分比(0-100)") # 时间戳 last_updated_at = Column(DateTime, nullable=True, comment="配额数据最后更新时间") last_response_at = Column(DateTime, nullable=True, comment="最后一次响应时间") # 审计字段 created_by = Column(String(64), nullable=True) updated_by = Column(String(64), nullable=True) created_at = Column(DateTime, nullable=False, default=utc_now_naive) updated_at = Column(DateTime, nullable=False, default=utc_now_naive, onupdate=utc_now_naive) is_deleted = Column(Integer, nullable=False, default=0, index=True) deleted_at = Column(DateTime, nullable=True) __table_args__ = ( UniqueConstraint("system_id", "env_key", "quota_key", name="uq_ext_system_quota_usage_system_env_key"), Index("ix_ext_system_quota_usage_window_end", "window_end"), ) def to_dict(self) -> dict[str, Any]: """序列化为 API 响应字典;usage_ratio 由 used/limit 计算,避免存储不一致。""" limit_value = self.limit_value or 0 used_value = self.used_value or 0 usage_ratio = round(used_value / limit_value, 4) if limit_value > 0 else 0.0 return { "id": self.id, "system_id": self.system_id, "env_key": self.env_key, "quota_key": self.quota_key, "quota_name": self.quota_name, "quota_window": self.quota_window, "window_start": format_utc_datetime(self.window_start) if self.window_start else None, "window_end": format_utc_datetime(self.window_end) if self.window_end else None, "limit_value": limit_value, "used_value": used_value, "remaining_value": max(limit_value - used_value, 0), "usage_ratio": usage_ratio, "unit": self.unit, "source": self.source, "source_header_name": self.source_header_name, "warning_threshold": self.warning_threshold, "critical_threshold": self.critical_threshold, "last_updated_at": format_utc_datetime(self.last_updated_at) if self.last_updated_at else None, "last_response_at": format_utc_datetime(self.last_response_at) if self.last_response_at else None, "created_by": self.created_by, "updated_by": self.updated_by, "created_at": format_utc_datetime(self.created_at), "updated_at": format_utc_datetime(self.updated_at), } class ExternalSystemAlert(Base): """外部系统告警事件。 记录健康检查失败、熔断打开、Token 连续刷新失败、配额接近上限、连续 超时等运维事件。同一 dedup_key 在 firing 状态下唯一,避免重复告警; resolved 后允许再次触发。用于前端运维大盘展示与通知驱动。 """ __tablename__ = "ext_system_alerts" # 主键 id = Column(BigInteger, primary_key=True, autoincrement=True) # 关联关系 system_id = Column( Integer, ForeignKey("ext_systems.id", ondelete="SET NULL"), nullable=True, index=True, comment="所属外部系统 ID(系统删除时置空,保留告警历史)", ) env_key = Column(String(32), nullable=True, comment="环境标识") # 告警分类与状态 alert_type = Column( String(64), nullable=False, index=True, comment="告警类型:health_check_failed/circuit_open/token_refresh_failed/quota_near_limit/continuous_timeout/webhook_expired", ) severity = Column(String(32), nullable=False, default="warning", comment="严重程度:info/warning/critical/fatal") status = Column( String(32), nullable=False, default="firing", index=True, comment="告警状态:firing/acknowledged/resolved/suppressed", ) # 告警内容 title = Column(String(256), nullable=False, comment="告警标题") description = Column(Text, nullable=True, comment="告警描述") detail = Column(JSON_VALUE, nullable=False, default=dict, comment="告警详情") # 关联资源 resource_type = Column(String(32), nullable=True, comment="关联资源类型:system/tool/webhook/sync_job/quota") resource_id = Column(String(128), nullable=True, comment="关联资源 ID") related_execution_id = Column(String(64), nullable=True, comment="关联执行记录 ID") related_trace_id = Column(String(64), nullable=True, comment="关联链路 ID") metric_snapshot = Column(JSON_VALUE, nullable=True, comment="触发时的指标快照") # 去重与时间 dedup_key = Column(String(256), nullable=True, index=True, comment="去重键,相同 key 的 firing 告警合并") triggered_at = Column(DateTime, nullable=False, index=True, comment="告警触发时间") resolved_at = Column(DateTime, nullable=True, comment="告警恢复时间") # 确认与恢复 acknowledged_by = Column(String(64), nullable=True, comment="确认人") acknowledged_at = Column(DateTime, nullable=True, comment="确认时间") resolution_note = Column(Text, nullable=True, comment="恢复备注") # 通知状态 notification_status = Column( String(32), nullable=False, default="pending", comment="通知状态:pending/sent/failed/skipped" ) notification_sent_at = Column(DateTime, nullable=True, comment="通知发送时间") notification_channels = Column(JSON_VALUE, nullable=False, default=dict, comment="通知渠道状态") # 审计字段 created_by = Column(String(64), nullable=True) updated_by = Column(String(64), nullable=True) created_at = Column(DateTime, nullable=False, default=utc_now_naive) updated_at = Column(DateTime, nullable=False, default=utc_now_naive, onupdate=utc_now_naive) is_deleted = Column(Integer, nullable=False, default=0, index=True) deleted_at = Column(DateTime, nullable=True) __table_args__ = ( Index("ix_ext_system_alerts_status_severity_triggered", "status", "severity", "triggered_at"), Index("ix_ext_system_alerts_system_status", "system_id", "status"), Index("ix_ext_system_alerts_alert_type_triggered", "alert_type", "triggered_at"), # 同一 dedup_key 在 firing 状态下唯一,避免重复告警 Index( "uq_ext_system_alerts_dedup_firing", "dedup_key", unique=True, postgresql_where="status = 'firing'", ), ) def to_dict(self) -> dict[str, Any]: """序列化为 API 响应字典。""" return { "id": self.id, "system_id": self.system_id, "env_key": self.env_key, "alert_type": self.alert_type, "severity": self.severity, "status": self.status, "title": self.title, "description": self.description, "detail": self.detail or {}, "resource_type": self.resource_type, "resource_id": self.resource_id, "related_execution_id": self.related_execution_id, "related_trace_id": self.related_trace_id, "metric_snapshot": self.metric_snapshot, "dedup_key": self.dedup_key, "triggered_at": format_utc_datetime(self.triggered_at), "resolved_at": format_utc_datetime(self.resolved_at) if self.resolved_at else None, "acknowledged_by": self.acknowledged_by, "acknowledged_at": format_utc_datetime(self.acknowledged_at) if self.acknowledged_at else None, "resolution_note": self.resolution_note, "notification_status": self.notification_status, "notification_sent_at": format_utc_datetime(self.notification_sent_at) if self.notification_sent_at else None, "notification_channels": self.notification_channels or {}, "created_by": self.created_by, "updated_by": self.updated_by, "created_at": format_utc_datetime(self.created_at), "updated_at": format_utc_datetime(self.updated_at), } class ExternalSecretRotationPolicy(Base): """外部系统密钥轮换策略。 管理外部系统密钥(OAuth client secret、API key、mTLS 证书等)的轮换 周期与状态。secret_ref 指向 ExternalSystem.secret_refs 中的具名 secret; auto 模式由适配器执行轮换,manual 模式仅提醒。用于满足企业安全合规 要求,避免密钥过期导致的生产事故。 """ __tablename__ = "ext_secret_rotation_policies" # 主键 id = Column(Integer, primary_key=True, autoincrement=True) # 关联关系 system_id = Column( Integer, ForeignKey("ext_systems.id", ondelete="CASCADE"), nullable=False, index=True, comment="所属外部系统 ID", ) env_key = Column(String(32), nullable=False, default="default", comment="环境标识") # 密钥定义 secret_type = Column( String(32), nullable=False, comment="密钥类型:client_secret/api_key/cert/password/connection_string" ) secret_name = Column(String(128), nullable=False, comment="密钥名称,如 salesforce_consumer_secret") secret_ref = Column(String(128), nullable=False, comment="引用 ExternalSystem.secret_refs 中的 key") # 轮换策略 rotation_mode = Column(String(32), nullable=False, default="manual", comment="轮换模式:auto/manual") rotation_interval_days = Column(Integer, nullable=False, default=90, comment="轮换周期(天)") notify_before_days = Column(Integer, nullable=False, default=7, comment="提前通知天数") notify_channels = Column(JSON_VALUE, nullable=False, default=dict, comment="通知渠道配置") # 轮换状态 last_rotated_at = Column(DateTime, nullable=True, comment="上次成功轮换时间(仅成功时更新)") next_rotation_at = Column(DateTime, nullable=True, index=True, comment="下次轮换时间") last_rotation_at = Column(DateTime, nullable=True, comment="上次轮换尝试时间(含失败,每次尝试均更新)") last_rotation_status = Column(String(32), nullable=True, comment="上次轮换结果:success/failed/skipped") last_rotation_error = Column(Text, nullable=True, comment="上次轮换错误信息") rotation_count = Column(Integer, nullable=False, default=0, comment="成功轮换次数") max_rotation_retries = Column(Integer, nullable=False, default=3, comment="最大重试次数") # 策略状态 status = Column( String(32), nullable=False, default="active", index=True, comment="策略状态:active/rotating/overdue/failed/disabled", ) # 审计字段 created_by = Column(String(64), nullable=True) updated_by = Column(String(64), nullable=True) created_at = Column(DateTime, nullable=False, default=utc_now_naive) updated_at = Column(DateTime, nullable=False, default=utc_now_naive, onupdate=utc_now_naive) is_deleted = Column(Integer, nullable=False, default=0, index=True) deleted_at = Column(DateTime, nullable=True) __table_args__ = ( UniqueConstraint( "system_id", "env_key", "secret_type", "secret_name", name="uq_ext_secret_rotation_policies_system_env_secret", ), ) def to_dict(self) -> dict[str, Any]: """序列化为 API 响应字典。""" return { "id": self.id, "system_id": self.system_id, "env_key": self.env_key, "secret_type": self.secret_type, "secret_name": self.secret_name, "secret_ref": self.secret_ref, "rotation_mode": self.rotation_mode, "rotation_interval_days": self.rotation_interval_days, "notify_before_days": self.notify_before_days, "notify_channels": self.notify_channels or {}, "last_rotated_at": format_utc_datetime(self.last_rotated_at) if self.last_rotated_at else None, "next_rotation_at": format_utc_datetime(self.next_rotation_at) if self.next_rotation_at else None, "last_rotation_at": format_utc_datetime(self.last_rotation_at) if self.last_rotation_at else None, "last_rotation_status": self.last_rotation_status, "last_rotation_error": self.last_rotation_error, "rotation_count": self.rotation_count, "max_rotation_retries": self.max_rotation_retries, "status": self.status, "created_by": self.created_by, "updated_by": self.updated_by, "created_at": format_utc_datetime(self.created_at), "updated_at": format_utc_datetime(self.updated_at), } class ExternalToolAccessRule(Base): """外部工具访问控制规则。 基于 principal(user/role/department/agent)+ tool + env 的细粒度访问 控制。effect 为 allow/deny,priority 越高优先级越高(deny 默认优先)。 conditions 承载时间窗口、只读约束、调用上限等复杂条件。用于多部门 共用平台时的合规管控。 """ __tablename__ = "ext_tool_access_rules" # 主键 id = Column(Integer, primary_key=True, autoincrement=True) # 关联关系 tool_slug = Column( String(128), ForeignKey("ext_tool_configs.slug", ondelete="CASCADE", onupdate="CASCADE"), nullable=True, index=True, comment="工具 slug(为空表示系统下所有工具)", ) system_id = Column( Integer, ForeignKey("ext_systems.id", ondelete="CASCADE"), nullable=True, index=True, comment="所属系统 ID(为空表示全局规则)", ) # 主体信息 principal_type = Column( String(32), nullable=False, index=True, comment="主体类型:user/role/department/agent/service_account" ) principal_id = Column(String(64), nullable=False, index=True, comment="主体 ID") principal_name = Column(String(128), nullable=True, comment="主体名称(冗余,便于展示)") # 约束条件 env_key = Column(String(32), nullable=True, comment="约束到特定环境,为空表示所有环境") effect = Column(String(16), nullable=False, default="allow", comment="规则效果:allow/deny") priority = Column(Integer, nullable=False, default=0, comment="优先级,数字越大优先级越高") conditions = Column( JSON_VALUE, nullable=False, default=dict, comment="附加条件,如 {time_window, read_only, max_calls_per_day}" ) expires_at = Column(DateTime, nullable=True, index=True, comment="规则过期时间,为空表示永久") # 状态信息 enabled = Column(Boolean, nullable=False, default=True, comment="是否启用") description = Column(Text, nullable=True, comment="规则说明") # 审计字段 created_by = Column(String(64), nullable=True) updated_by = Column(String(64), nullable=True) created_at = Column(DateTime, nullable=False, default=utc_now_naive) updated_at = Column(DateTime, nullable=False, default=utc_now_naive, onupdate=utc_now_naive) is_deleted = Column(Integer, nullable=False, default=0, index=True) deleted_at = Column(DateTime, nullable=True) __table_args__ = ( Index("ix_ext_tool_access_rules_tool_enabled", "tool_slug", "enabled"), Index("ix_ext_tool_access_rules_principal", "principal_type", "principal_id"), Index("ix_ext_tool_access_rules_system_enabled", "system_id", "enabled"), ) def to_dict(self) -> dict[str, Any]: """序列化为 API 响应字典。""" return { "id": self.id, "tool_slug": self.tool_slug, "system_id": self.system_id, "principal_type": self.principal_type, "principal_id": self.principal_id, "principal_name": self.principal_name, "env_key": self.env_key, "effect": self.effect, "priority": self.priority, "conditions": self.conditions or {}, "expires_at": format_utc_datetime(self.expires_at) if self.expires_at else None, "enabled": self.enabled, "description": self.description, "created_by": self.created_by, "updated_by": self.updated_by, "created_at": format_utc_datetime(self.created_at), "updated_at": format_utc_datetime(self.updated_at), } class ExternalToolTestCase(Base): """外部工具回归测试用例。 为关键工具配置请求 fixture 与期望断言,定期运行以监控外部系统 API 变更导致的静默失败(如返回 200 但结构变化)。支持 jsonpath/status_code /response_time 等多种断言类型;连续失败达到阈值时触发告警。 """ __tablename__ = "ext_tool_test_cases" # 主键 id = Column(Integer, primary_key=True, autoincrement=True) # 关联关系 tool_slug = Column( String(128), ForeignKey("ext_tool_configs.slug", ondelete="CASCADE", onupdate="CASCADE"), nullable=False, index=True, comment="被测工具 slug", ) # 用例基础信息 name = Column(String(128), nullable=False, comment="用例名称") description = Column(Text, nullable=True, comment="用例说明") env_key = Column(String(32), nullable=True, comment="测试环境,为空使用默认环境") # 请求与断言 request_fixture = Column(JSON_VALUE, nullable=False, comment="请求参数 fixture") expected_status = Column(String(32), nullable=True, comment="期望状态:success/failed/具体状态码") assertions = Column( JSON_VALUE, nullable=False, default=list, comment="断言列表,如 [{type: jsonpath, path, op, value}]" ) timeout = Column(Integer, nullable=False, default=30, comment="超时时间(秒)") # 调度与启用 enabled = Column(Boolean, nullable=False, default=True, comment="是否启用") schedule_cron = Column(String(64), nullable=True, index=True, comment="运行计划 cron 表达式,为空表示手动运行") # 上次运行结果 last_run_at = Column(DateTime, nullable=True, comment="上次运行时间") last_run_status = Column(String(32), nullable=True, index=True, comment="上次运行结果:passed/failed/error/skipped") last_run_duration_ms = Column(Integer, nullable=True, comment="上次运行耗时毫秒") last_run_response = Column(JSON_VALUE, nullable=True, comment="上次运行响应(已脱敏)") last_run_error = Column(Text, nullable=True, comment="上次运行错误信息") last_run_assertions_result = Column(JSON_VALUE, nullable=True, comment="上次运行断言明细") # 告警配置 consecutive_failures = Column(Integer, nullable=False, default=0, comment="连续失败次数") alert_on_failure = Column(Boolean, nullable=False, default=True, comment="失败是否触发告警") alert_threshold = Column(Integer, nullable=False, default=3, comment="连续失败几次后告警") # 审计字段 created_by = Column(String(64), nullable=True) updated_by = Column(String(64), nullable=True) created_at = Column(DateTime, nullable=False, default=utc_now_naive) updated_at = Column(DateTime, nullable=False, default=utc_now_naive, onupdate=utc_now_naive) is_deleted = Column(Integer, nullable=False, default=0, index=True) deleted_at = Column(DateTime, nullable=True) __table_args__ = ( Index("ix_ext_tool_test_cases_tool_enabled", "tool_slug", "enabled"), Index("ix_ext_tool_test_cases_last_run_status", "last_run_status"), ) def to_dict(self) -> dict[str, Any]: """序列化为 API 响应字典。""" return { "id": self.id, "tool_slug": self.tool_slug, "name": self.name, "description": self.description, "env_key": self.env_key, "request_fixture": self.request_fixture or {}, "expected_status": self.expected_status, "assertions": self.assertions or [], "timeout": self.timeout, "enabled": self.enabled, "schedule_cron": self.schedule_cron, "last_run_at": format_utc_datetime(self.last_run_at) if self.last_run_at else None, "last_run_status": self.last_run_status, "last_run_duration_ms": self.last_run_duration_ms, "last_run_response": self.last_run_response, "last_run_error": self.last_run_error, "last_run_assertions_result": self.last_run_assertions_result, "consecutive_failures": self.consecutive_failures, "alert_on_failure": self.alert_on_failure, "alert_threshold": self.alert_threshold, "created_by": self.created_by, "updated_by": self.updated_by, "created_at": format_utc_datetime(self.created_at), "updated_at": format_utc_datetime(self.updated_at), } class ExternalNotificationChannel(Base): """外部系统通知渠道配置。 管理告警通知渠道(Slack webhook、Email SMTP、企业机器人等)的配置, 供 ``NotificationService`` 在发送告警通知时按 ``channel_type`` 分派。 ``config`` 为渠道特定配置 dict,可能含敏感字段(webhook URL、SMTP 密码 等),由 persistence 边界加解密、use_cases mapper 脱敏。 """ __tablename__ = "ext_notification_channels" # 主键 id = Column(BigInteger, primary_key=True, autoincrement=True) # 渠道基础信息 channel_type = Column( String(64), nullable=False, index=True, comment="渠道类型:slack/email/feishu/dingtalk/webhook/..." ) name = Column(String(128), nullable=False, comment="展示名称") description = Column(Text, nullable=True, comment="渠道描述") # 渠道配置(敏感字段加密存储) config = Column(JSON_VALUE, nullable=False, default=dict, comment="渠道配置(敏感字段加密存储)") # 启用状态 enabled = Column(Boolean, nullable=False, default=True, comment="是否启用") # 审计字段 created_by = Column(String(64), nullable=True) updated_by = Column(String(64), nullable=True) created_at = Column(DateTime, nullable=False, default=utc_now_naive) updated_at = Column(DateTime, nullable=False, default=utc_now_naive, onupdate=utc_now_naive) is_deleted = Column(Integer, nullable=False, default=0, index=True) deleted_at = Column(DateTime, nullable=True) __table_args__ = ( Index("ix_ext_notification_channels_type_enabled", "channel_type", "enabled"), Index("ix_ext_notification_channels_name", "name"), ) def to_dict(self) -> dict[str, Any]: """序列化为 API 响应字典;config 不返回明文,仅返回是否存在。""" return { "id": self.id, "channel_type": self.channel_type, "name": self.name, "description": self.description, "has_config": bool(self.config), "enabled": self.enabled, "created_by": self.created_by, "updated_by": self.updated_by, "created_at": format_utc_datetime(self.created_at), "updated_at": format_utc_datetime(self.updated_at), }