ForcePilot/backend/package/yuxi/storage/postgres/models_external.py
Kris 33acf3b61e refactor(postgres models): remove unnecessary indexes and add new fields and indexes
1. 移除 ExternalSystemHealthCheck、ExternalSystemQuotaUsage、ExternalToolTestCase 中的冗余索引
2. 为 ChannelUserIdentity 新增 pending_review 字段并添加GIN索引加速查询
3. 为 ChannelAuditLog 添加 target_account 字段索引
4. 为 ChannelOutboxEntry 新增 delivered_parts 字段并添加高频查询列索引
2026-07-09 04:23:35 +08:00

1551 lines
68 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
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, 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 对外部系统事件的订阅关系:哪个系统/环境的什么事件、由哪个
handleragent/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, 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):
"""外部工具访问控制规则。
基于 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="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, 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),
}