ForcePilot/backend/package/yuxi/storage/postgres/models_external.py
Kris 56022a9199 chore: 批量整理代码变更,修复细节与优化结构
- 修复 scheduler store 末尾多余逗号
- 将智能体名称从“语析”改为“Kris”
- 新增获取运行记录UID的权限校验接口
- 重构操作日志写入逻辑,使用独立会话避免事务污染
- 注册外部系统调度处理器
- 优化全局错误处理器,统一响应格式与序列化处理
- 新增调度任务运行日志的handler_name字段与索引
- 重构任务执行函数,新增payload覆盖与操作人审计参数
- 重写外部系统store,新增侧边栏折叠状态与持久化
- 优化会话、消息等模型的索引、约束与字段类型
- 新增多项配置项与环境变量支持
- 重构渠道相关模型,新增索引、约束与字段优化
- 新增渠道插件模型与内容审核模型的完善
2026-07-11 22:06:21 +08:00

2140 lines
88 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.

"""外部系统集成相关的 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
from typing import Any
from sqlalchemy import (
BigInteger,
Boolean,
CheckConstraint,
Column,
DateTime,
ForeignKey,
Index,
Integer,
LargeBinary,
String,
Text,
UniqueConstraint,
and_,
func,
select,
text,
)
from sqlalchemy import (
Enum as SAEnum,
)
from sqlalchemy.dialects.postgresql import JSON, JSONB
from sqlalchemy.orm import column_property
from yuxi.external_systems.framework.runtime.enums import (
ExecutionCaller,
ExecutionOperation,
ExecutionStatus,
HealthStatus,
)
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=True, default="", 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="重试策略adapter 层实现重试executor 层不重试)",
)
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为空时自包含运行",
)
source_type = Column(
String(64),
nullable=True,
index=True,
comment="工具来源类型(继承自所属系统的 source_type用于溯源",
)
# 审计字段
created_by = Column(String(64), nullable=False, default="system")
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(Boolean, nullable=False, default=False)
deleted_at = Column(DateTime, nullable=True, index=True)
__table_args__ = (
Index(
"ix_ext_tool_configs_system_id_enabled",
"system_id",
"enabled",
postgresql_where=text("is_deleted = false"),
),
Index(
"ix_ext_tool_configs_adapter_type_enabled",
"adapter_type",
"enabled",
postgresql_where=text("is_deleted = false"),
),
Index(
"ix_ext_tool_configs_category_enabled",
"category",
"enabled",
postgresql_where=text("is_deleted = false"),
),
)
def to_dict(self) -> dict[str, Any]:
"""序列化为字典;敏感字段的加解密由 persistence 层承接。
.. warning::
``auth_config`` / ``adapter_config`` 返回的是**密文**,仅供调试使用。
业务层应通过 ``orm_to_external_tool`` mapper 获取解密后的数据。
"""
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=False, default="system")
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(Boolean, nullable=False, default=False)
deleted_at = Column(DateTime, nullable=True, index=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, index=True, 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/invalid/pending",
)
status_message = Column(String(512), nullable=True, comment="状态说明,如解析失败原因")
# 审计字段
created_by = Column(String(64), nullable=False, default="system")
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(Boolean, nullable=False, default=False)
deleted_at = Column(DateTime, nullable=True, index=True)
__table_args__ = (
# status 取值约束(与 AssetStatus Literal 对齐)
CheckConstraint(
"status IN ('active', 'invalid', 'pending')",
name="ck_ext_adapter_assets_status",
),
# checksum 部分唯一索引:未软删除记录中 checksum 唯一,避免重复上传
Index(
"uq_ext_adapter_assets_checksum_active",
"checksum",
unique=True,
postgresql_where=text("is_deleted = false"),
),
# 复合索引:按适配器类型 + 状态 / 适配器类型 + 资产类型筛选(部分索引,仅覆盖未删除记录)
Index(
"ix_ext_adapter_assets_adapter_type_status",
"adapter_type",
"status",
postgresql_where=text("is_deleted = false"),
),
Index(
"ix_ext_adapter_assets_adapter_type_asset_type",
"adapter_type",
"asset_type",
postgresql_where=text("is_deleted = false"),
),
)
def to_dict(self) -> dict[str, Any]:
"""序列化为 API 响应字典;``content`` 为二进制原值,列表场景应使用 defer 排除。"""
return {
"id": self.id,
"adapter_type": self.adapter_type,
"asset_type": self.asset_type,
"name": self.name,
"size": self.size or 0,
"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"
__table_args__ = (
Index("ix_ext_systems_adapter_enabled_deleted", "adapter_type", "enabled", "is_deleted"),
Index("ix_ext_systems_category_enabled_deleted", "category", "enabled", "is_deleted"),
Index("ix_ext_systems_source_type_deleted", "source_type", "is_deleted"),
)
# 主键
id = Column(Integer, primary_key=True, autoincrement=True)
# 乐观锁(并发更新保护)
version = Column(Integer, nullable=False, default=1, comment="乐观锁版本号")
# 基础信息
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用于统一操作分派")
integration_key = Column(String(64), nullable=True, index=True, comment="厂商集成目录 key关联系统与集成元数据")
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="重试策略adapter 层实现重试executor 层不重试)",
)
# 审计字段
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(Boolean, nullable=False, default=False, index=True)
deleted_at = Column(DateTime, nullable=True, index=True)
def to_dict(self) -> dict[str, Any]:
return {
"id": self.id,
"version": self.version,
"slug": self.slug,
"name": self.name,
"description": self.description,
"category": self.category,
"adapter_type": self.adapter_type,
"source_type": self.source_type,
"integration_key": self.integration_key,
"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(Boolean, nullable=False, default=False, index=True)
deleted_at = Column(DateTime, nullable=True, index=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=and_(is_default.is_(True), is_deleted.is_(False)),
),
# 跨系统查询同 env_key 的环境时使用(复合唯一索引 (system_id, env_key) 不满足最左前缀)
Index("ix_ext_system_environments_env_key", "env_key"),
)
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 问题
# 过滤软删除记录,确保 env_count 与实际未删除环境数一致
ExternalSystem.env_count = column_property(
select(func.count(ExternalSystemEnvironment.id))
.where(ExternalSystemEnvironment.system_id == ExternalSystem.id)
.where(ExternalSystemEnvironment.is_deleted.is_(False))
.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(Boolean, nullable=False, default=False, index=True)
deleted_at = Column(DateTime, nullable=True, index=True)
__table_args__ = (
UniqueConstraint("system_id", "env_key", name="uq_ext_system_tokens_system_env"),
# 列表查询主路径:按系统 + 环境 + 失效状态过滤
Index(
"ix_ext_system_tokens_system_env_invalidated",
"system_id",
"env_key",
"is_invalidated",
postgresql_where=text("is_deleted = false"),
),
# 即将过期查询:按 expires_at 范围扫描,过滤未失效记录
Index(
"ix_ext_system_tokens_expires_at_active",
"expires_at",
postgresql_where=text("is_deleted = false AND is_invalidated = false"),
),
)
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="当前桶的近似 p99 延迟upsert 冲突时取 GREATEST 保留峰值,聚合时取各桶 MAX",
)
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(Boolean, nullable=False, default=False, index=True)
deleted_at = Column(DateTime, nullable=True, index=True)
__table_args__ = (
UniqueConstraint("system_id", "env_key", "bucket_at", name="uq_ext_system_metrics_bucket"),
Index(
"ix_ext_system_metrics_bucket",
"bucket_at",
postgresql_where=text("is_deleted = false"),
),
)
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 None
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(Boolean, nullable=False, default=False, index=True)
deleted_at = Column(DateTime, nullable=True, index=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 允许为空工具级路径tool.system_id is None不关联具体系统
# 避免用负数占位违反 FK 约束。查询时需处理 NULL 语义。
system_id = Column(
Integer,
ForeignKey("ext_systems.id", ondelete="CASCADE"),
nullable=True,
index=True,
comment="所属系统 ID工具级路径为 NULL",
)
env_key = Column(String(32), nullable=False, default="default", comment="环境标识")
tool_slug = Column(
String(128),
ForeignKey("ext_tool_configs.slug", ondelete="CASCADE", onupdate="CASCADE"),
nullable=False,
index=True,
comment="工具 slug",
)
# 调用方信息
caller = Column(
SAEnum(ExecutionCaller, name="execution_caller", native_enum=False),
nullable=False,
comment="调用方agent/user/workflow/simulate/sync_job/retry",
)
caller_id = Column(String(64), nullable=True, comment="调用方实体 ID")
# 执行状态与时间
status = Column(
SAEnum(ExecutionStatus, name="execution_status", native_enum=False),
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(
SAEnum(ExecutionOperation, name="execution_operation", native_enum=False),
nullable=False,
default=ExecutionOperation.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=False, default="system")
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(Boolean, nullable=False, default=False)
deleted_at = Column(DateTime, nullable=True, index=True)
__table_args__ = (
Index(
"ix_ext_tool_executions_system_started",
"system_id",
"started_at",
postgresql_where=text("is_deleted = false"),
),
Index(
"ix_ext_tool_executions_tool_started",
"tool_slug",
"started_at",
postgresql_where=text("is_deleted = false"),
),
Index(
"ix_ext_tool_executions_status_started",
"status",
"started_at",
postgresql_where=text("is_deleted = false"),
),
# GIN 索引:加速 tags JSONB 的 @> 包含查询tag_key/tag_value 过滤)
Index(
"ix_ext_tool_executions_tags_gin",
"tags",
postgresql_using="gin",
postgresql_ops={"tags": "jsonb_path_ops"},
),
)
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),
ForeignKey("ext_tool_executions.execution_id", ondelete="CASCADE"),
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=False, default="system")
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(Boolean, nullable=False, default=False)
deleted_at = Column(DateTime, nullable=True, index=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(
SAEnum(HealthStatus, name="health_status", native_enum=False),
nullable=False,
default=HealthStatus.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, 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(Boolean, nullable=False, default=False, index=True)
deleted_at = Column(DateTime, nullable=True, index=True)
__table_args__ = (
# 部分唯一索引:仅对未删除记录生效,避免软删除后 upsert 冲突(与 ext_system_environments 等表一致)
Index(
"uq_ext_system_health_checks_system_env",
"system_id",
"env_key",
unique=True,
postgresql_where=text("is_deleted = false"),
),
# 部分索引:查询均带 is_deleted=false 过滤,与其他 ext_* 表保持一致
Index(
"ix_ext_system_health_checks_status",
"health_status",
postgresql_where=text("is_deleted = false"),
),
Index(
"ix_ext_system_health_checks_checked_at",
"checked_at",
postgresql_where=text("is_deleted = false"),
),
# 复合索引按系统取最新记录get_latest_by_system 常用路径)
Index(
"ix_ext_system_health_checks_system_checked",
"system_id",
"checked_at",
postgresql_where=text("is_deleted = false"),
),
)
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 对外部系统事件的订阅关系:哪个系统/环境的什么事件、由哪个
handleragent/workflow处理以及外部系统返回的订阅 ID、验签密钥、
过期与续期信息。与 ExternalToolExecution 的"主动调用"链路互补,共同
覆盖双向集成场景。
"""
__tablename__ = "ext_webhook_subscriptions"
# 主键
id = Column(Integer, primary_key=True, autoincrement=True)
# 基础信息
slug = Column(String(128), nullable=False, 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(Boolean, nullable=False, default=False, index=True)
deleted_at = Column(DateTime, nullable=True, index=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"),
Index("ix_ext_webhook_subscriptions_last_received_at", "last_received_at"),
Index(
"ix_ext_webhook_subscriptions_renewal_scan",
"status",
"auto_renew",
"expires_at",
),
# 幂等键slug / callback_path 仅对未软删除记录唯一,避免软删除后无法重建同名订阅
Index(
"uq_ext_webhook_subscriptions_slug",
"slug",
unique=True,
postgresql_where=(is_deleted.is_(False)),
),
Index(
"uq_ext_webhook_subscriptions_callback_path",
"callback_path",
unique=True,
postgresql_where=(is_deleted.is_(False)),
),
CheckConstraint(
"status IN ('pending', 'active', 'paused', 'expired', 'disabled')",
name="ck_ext_webhook_subscriptions_status",
),
CheckConstraint(
"secret_algorithm IN ('hmac_sha256', 'hmac_sha1', 'md5')",
name="ck_ext_webhook_subscriptions_secret_algorithm",
),
CheckConstraint(
"target_handler_type IN ('agent', 'workflow', 'webhook_forward')",
name="ck_ext_webhook_subscriptions_target_handler_type",
),
)
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(Boolean, nullable=False, default=False, index=True)
deleted_at = Column(DateTime, nullable=True, index=True)
__table_args__ = (
# 幂等去重:仅对未软删除记录生效,避免软删除记录永久占用 external_event_id
Index(
"uq_ext_webhook_events_sub_ext_event",
"subscription_id",
"external_event_id",
unique=True,
postgresql_where=text("is_deleted = false"),
),
Index("ix_ext_webhook_events_received_status", "received_at", "processing_status"),
Index("ix_ext_webhook_events_sub_payload_hash", "subscription_id", "payload_hash"),
Index("ix_ext_webhook_events_sub_status_received", "subscription_id", "processing_status", "received_at"),
)
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, 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")
source_header_strategy = Column(
String(32),
nullable=False,
default="remaining",
comment="响应头解析策略remaining/used/limit/fraction_used_limit",
)
# 告警阈值
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(Boolean, nullable=False, default=False, index=True)
deleted_at = Column(DateTime, nullable=True, index=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"),
CheckConstraint(
"source IN ('response_header', 'manual', 'config')",
name="ck_ext_system_quota_usage_source",
),
CheckConstraint(
"quota_window IN ('daily', 'hourly', 'rolling_24h', 'minute', 'custom')",
name="ck_ext_system_quota_usage_quota_window",
),
CheckConstraint(
"source_header_strategy IN ('remaining', 'used', 'limit', 'fraction_used_limit')",
name="ck_ext_system_quota_usage_source_header_strategy",
),
CheckConstraint(
"warning_threshold >= 0 AND warning_threshold <= 100",
name="ck_ext_system_quota_usage_warning_threshold",
),
CheckConstraint(
"critical_threshold >= 0 AND critical_threshold <= 100",
name="ck_ext_system_quota_usage_critical_threshold",
),
CheckConstraint(
"window_start IS NULL OR window_end IS NULL OR window_start < window_end",
name="ck_ext_system_quota_usage_window_range",
),
)
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,
"source_header_strategy": self.source_header_strategy,
"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/webhook_event_failed",
)
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(Boolean, nullable=False, default=False, index=True)
deleted_at = Column(DateTime, nullable=True, index=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="最大重试次数")
current_retry_count = Column(
Integer,
nullable=False,
default=0,
comment="当前连续重试次数(失败时递增,成功/重置时清零)",
)
# 策略状态
status = Column(
String(32),
nullable=False,
default="active",
index=True,
comment="策略状态active/rotating/overdue/failed/disabled",
)
# 乐观锁(并发轮换保护)
version = Column(
Integer,
nullable=False,
default=1,
comment="乐观锁版本号,状态转换时 CAS 校验",
)
# 审计字段
created_by = Column(String(64), nullable=False, default="system")
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(Boolean, nullable=False, default=False, index=True)
deleted_at = Column(DateTime, nullable=True, index=True)
__table_args__ = (
# 仅对未软删除记录做唯一性约束
Index(
"uq_ext_secret_rotation_policies_system_env_secret",
"system_id",
"env_key",
"secret_type",
"secret_name",
unique=True,
postgresql_where=text("is_deleted = false"),
),
# 调度扫描常用过滤路径
Index(
"ix_ext_secret_rotation_policies_status_next_rotation",
"status",
"next_rotation_at",
),
Index(
"ix_ext_secret_rotation_policies_system_status_next_rotation",
"system_id",
"status",
"next_rotation_at",
),
CheckConstraint(
"rotation_mode IN ('auto', 'manual')",
name="ck_ext_secret_rotation_policies_rotation_mode",
),
CheckConstraint(
"status IN ('active', 'rotating', 'overdue', 'failed', 'disabled')",
name="ck_ext_secret_rotation_policies_status",
),
CheckConstraint(
"last_rotation_status IS NULL OR last_rotation_status IN ('success', 'failed', 'skipped')",
name="ck_ext_secret_rotation_policies_last_rotation_status",
),
CheckConstraint(
"secret_type IN ('client_secret', 'api_key', 'cert', 'password', 'connection_string')",
name="ck_ext_secret_rotation_policies_secret_type",
),
CheckConstraint(
"rotation_interval_days > 0",
name="ck_ext_secret_rotation_policies_rotation_interval_days",
),
CheckConstraint(
"notify_before_days >= 0",
name="ck_ext_secret_rotation_policies_notify_before_days",
),
CheckConstraint(
"max_rotation_retries >= 0",
name="ck_ext_secret_rotation_policies_max_rotation_retries",
),
CheckConstraint(
"current_retry_count >= 0",
name="ck_ext_secret_rotation_policies_current_retry_count",
),
CheckConstraint(
"rotation_count >= 0",
name="ck_ext_secret_rotation_policies_rotation_count",
),
)
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,
"current_retry_count": self.current_retry_count,
"status": self.status,
"version": self.version,
"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 ExternalSecret(Base):
"""外部系统具名密钥元数据表。
作为 ``ExternalSystem.secret_refs`` JSONB 中密钥 key 的引用索引,
提供引用完整性校验与软删除保护,不存储密钥值本身(仍由 secret_refs 托管)。
创建密钥轮换策略时若引用不存在则自动注册。
"""
__tablename__ = "ext_secrets"
# 主键
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="密钥展示名称")
secret_ref_key = Column(
String(128),
nullable=False,
comment="对应 ExternalSystem.secret_refs 中的 key",
)
# 状态
status = Column(
String(32),
nullable=False,
default="active",
comment="状态active/inactive",
)
# 乐观锁
version = Column(Integer, nullable=False, default=1, comment="乐观锁版本号")
# 审计字段
created_by = Column(String(64), nullable=False, default="system")
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(Boolean, nullable=False, default=False, index=True)
deleted_at = Column(DateTime, nullable=True, index=True)
__table_args__ = (
# 同一系统/环境下 secret_ref_key 唯一(未删除)
Index(
"uq_ext_secrets_system_env_ref",
"system_id",
"env_key",
"secret_ref_key",
unique=True,
postgresql_where=text("is_deleted = false"),
),
CheckConstraint(
"secret_type IN ('client_secret', 'api_key', 'cert', 'password', 'connection_string')",
name="ck_ext_secrets_secret_type",
),
CheckConstraint(
"status IN ('active', 'inactive')",
name="ck_ext_secrets_status",
),
)
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_key": self.secret_ref_key,
"status": self.status,
"version": self.version,
"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 ExternalSecretRotationRecord(Base):
"""密钥轮换执行历史记录。
记录每次轮换尝试(成功/失败/跳过),用于审计、排障与合规追溯。
"""
__tablename__ = "ext_secret_rotation_records"
# 主键
id = Column(Integer, primary_key=True, autoincrement=True)
# 关联关系
policy_id = Column(
Integer,
ForeignKey("ext_secret_rotation_policies.id", ondelete="CASCADE"),
nullable=False,
index=True,
comment="所属轮换策略 ID",
)
secret_id = Column(
Integer,
ForeignKey("ext_secrets.id", ondelete="SET NULL"),
nullable=True,
index=True,
comment="轮换的密钥元数据 ID",
)
# 执行信息
attempted_at = Column(DateTime, nullable=False, default=utc_now_naive, comment="尝试时间")
status = Column(
String(32),
nullable=False,
comment="执行结果success/failed/skipped",
)
mode = Column(
String(32),
nullable=False,
comment="执行模式auto/manual",
)
error = Column(Text, nullable=True, comment="失败时的错误信息")
performed_by = Column(String(64), nullable=True, comment="执行人 UID手动时填写")
old_secret_version = Column(String(64), nullable=True, comment="旧密钥版本标识")
new_secret_version = Column(String(64), nullable=True, comment="新密钥版本标识")
# 审计字段
created_by = Column(String(64), nullable=False, default="system")
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(Boolean, nullable=False, default=False, index=True)
deleted_at = Column(DateTime, nullable=True, index=True)
__table_args__ = (
# 按策略查询历史常用路径
Index(
"ix_ext_secret_rotation_records_policy_attempted",
"policy_id",
"attempted_at",
),
CheckConstraint(
"status IN ('success', 'failed', 'skipped')",
name="ck_ext_secret_rotation_records_status",
),
CheckConstraint(
"mode IN ('auto', 'manual')",
name="ck_ext_secret_rotation_records_mode",
),
)
def to_dict(self) -> dict[str, Any]:
"""序列化为 API 响应字典。"""
return {
"id": self.id,
"policy_id": self.policy_id,
"secret_id": self.secret_id,
"attempted_at": format_utc_datetime(self.attempted_at),
"status": self.status,
"mode": self.mode,
"error": self.error,
"performed_by": self.performed_by,
"old_secret_version": self.old_secret_version,
"new_secret_version": self.new_secret_version,
"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):
"""外部工具访问控制规则。
基于 principaluser/role/department/agent+ tool + env 的细粒度访问
控制。effect 为 allow/denypriority 越高优先级越高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="SET NULL", onupdate="CASCADE"),
nullable=True,
index=True,
comment="工具 slug为空表示系统下所有工具",
)
system_id = Column(
Integer,
ForeignKey("ext_systems.id", ondelete="SET NULL"),
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=False, default="system")
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(Boolean, nullable=False, default=False)
deleted_at = Column(DateTime, nullable=True, index=True)
__table_args__ = (
# 防止重复创建相同语义规则(未软删除记录间唯一)
Index(
"uq_ext_tool_access_rules_scope",
"system_id",
"tool_slug",
"principal_type",
"principal_id",
"env_key",
"effect",
unique=True,
postgresql_where=text("is_deleted = false"),
postgresql_nulls_not_distinct=True,
),
# 列表页常用过滤路径
Index(
"ix_ext_tool_access_rules_system_env",
"system_id",
"env_key",
postgresql_where=text("is_deleted = false"),
),
Index(
"ix_ext_tool_access_rules_tool_env",
"tool_slug",
"env_key",
postgresql_where=text("is_deleted = false"),
),
# 评估查询常用路径:先按主体定位,再按启用/过期过滤
Index(
"ix_ext_tool_access_rules_eval_principal",
"principal_type",
"principal_id",
"enabled",
"expires_at",
postgresql_where=text("is_deleted = false"),
),
CheckConstraint(
"principal_type IN ('user', 'role', 'department', 'agent', 'service_account')",
name="ck_ext_tool_access_rules_principal_type",
),
CheckConstraint(
"effect IN ('allow', 'deny')",
name="ck_ext_tool_access_rules_effect",
),
CheckConstraint(
"priority >= 0",
name="ck_ext_tool_access_rules_priority",
),
)
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, comment="运行计划 cron 表达式,为空表示手动运行")
# 上次运行结果
last_run_at = Column(DateTime, nullable=True, comment="上次运行时间")
last_run_status = Column(String(32), nullable=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=False, default="system")
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(Boolean, nullable=False, default=False)
deleted_at = Column(DateTime, nullable=True, index=True)
__table_args__ = (
Index(
"ix_ext_tool_test_cases_tool_enabled",
"tool_slug",
"enabled",
postgresql_where=text("is_deleted = false"),
),
# 列表按运行状态/期望状态/环境筛选(仅未删除记录)
Index(
"ix_ext_tool_test_cases_last_run_status",
"last_run_status",
postgresql_where=text("is_deleted = false"),
),
Index(
"ix_ext_tool_test_cases_expected_status",
"expected_status",
postgresql_where=text("is_deleted = false"),
),
Index(
"ix_ext_tool_test_cases_env_key",
"env_key",
postgresql_where=text("is_deleted = false"),
),
# 列表按 cron 表达式精确筛选(仅未删除记录)
Index(
"ix_ext_tool_test_cases_schedule_cron",
"schedule_cron",
postgresql_where=text("is_deleted = false and schedule_cron is not null"),
),
# 调度器查询:已启用且配置了 cron 的用例
Index(
"ix_ext_tool_test_cases_scheduled",
"enabled",
"schedule_cron",
postgresql_where=text("is_deleted = false and schedule_cron is not null"),
),
# 告警扫描:开启告警且连续失败次数大于 0 的用例
Index(
"ix_ext_tool_test_cases_failing",
"consecutive_failures",
"alert_on_failure",
postgresql_where=text("is_deleted = false and alert_on_failure = true and consecutive_failures > 0"),
),
)
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(Boolean, nullable=False, default=False, index=True)
deleted_at = Column(DateTime, nullable=True, index=True)
__table_args__ = (
Index(
"ix_ext_notification_channels_type_enabled",
"channel_type",
"enabled",
postgresql_where=text("is_deleted = false"),
),
Index("ix_ext_notification_channels_name", "name"),
CheckConstraint(
"channel_type IN ('email', 'webhook', 'slack', 'feishu', 'dingtalk')",
name="ck_ext_notification_channels_channel_type",
),
)
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),
}