ForcePilot/backend/package/yuxi/channels/contract/errors/domain.py
Kris b88c0ae29e feat(channels): 批量新增多渠道网关限界上下文基础代码与契约
新增完整的 channels 限界上下文模块,包含契约层、领域核心层、应用服务、管道编排、插件体系、基础设施组合根等全层级代码,新增飞书与微信 iLink 渠道插件基础结构,补充各类 DTO、端口协议与领域服务实现。
2026-07-02 03:22:12 +08:00

576 lines
15 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.

"""领域错误类型定义。
定义 ``DomainError`` 抽象基类及其 24 个具体子类,表示领域规则违反或业务
状态异常。涵盖冲突、规则违反、渠道降级、Bot 循环预算、插件生命周期、
配对审批、DM 安全、限流、能力证明、配置热更新、Schema 初始化、账户生命
周期回调、内容审核、Agent 协作等场景。
"""
from __future__ import annotations
from typing import Any
from yuxi.channels.contract.errors.base import Error
class DomainError(Error):
"""领域错误抽象基类。
表示领域规则违反或业务状态异常。子类覆盖各类业务场景。
"""
error_code = "DOMAIN_ERROR"
class ConflictError(DomainError):
"""资源冲突错误。
资源状态冲突时抛出如重复创建、并发修改HTTP 409
"""
error_code = "CONFLICT"
def __init__(
self,
resource: str,
*,
trace_id: str | None = None,
) -> None:
super().__init__(f"Conflict on resource: {resource}", trace_id=trace_id)
self.resource = resource
def to_dict(self) -> dict[str, Any]:
data = super().to_dict()
data["resource"] = self.resource
return data
class RuleViolationError(DomainError):
"""领域规则违反错误。
业务规则被违反时抛出如会话合并规则不满足HTTP 422
"""
error_code = "RULE_VIOLATION"
def __init__(
self,
rule: str,
*,
trace_id: str | None = None,
) -> None:
super().__init__(f"Rule violated: {rule}", trace_id=trace_id)
self.rule = rule
def to_dict(self) -> dict[str, Any]:
data = super().to_dict()
data["rule"] = self.rule
return data
class ChannelDegradedError(DomainError):
"""渠道降级错误。
渠道插件失败触发优雅降级时抛出FR-36HTTP 503
"""
error_code = "CHANNEL_DEGRADED"
def __init__(
self,
channel: str,
*,
trace_id: str | None = None,
) -> None:
super().__init__(f"Channel degraded: {channel}", trace_id=trace_id)
self.channel = channel
def to_dict(self) -> dict[str, Any]:
data = super().to_dict()
data["channel"] = self.channel
return data
class BotLoopBudgetExceededError(DomainError):
"""Bot 循环预算超限错误。
Bot 回复频率超出预算限制时抛出,防止 Bot 之间形成无限循环FR-33HTTP 429
"""
error_code = "BOT_LOOP_BUDGET_EXCEEDED"
def __init__(
self,
budget: dict[str, Any],
*,
trace_id: str | None = None,
) -> None:
super().__init__("Bot loop budget exceeded", trace_id=trace_id)
self.budget = budget
def to_dict(self) -> dict[str, Any]:
data = super().to_dict()
data["budget"] = self.budget
return data
class PluginAlreadyRegisteredError(DomainError):
"""插件已注册错误。
同一插件 ID 重复注册时抛出FR-32HTTP 409
"""
error_code = "PLUGIN_ALREADY_REGISTERED"
def __init__(
self,
plugin_id: str,
*,
trace_id: str | None = None,
) -> None:
super().__init__(
f"Plugin already registered: {plugin_id}",
trace_id=trace_id,
)
self.plugin_id = plugin_id
def to_dict(self) -> dict[str, Any]:
data = super().to_dict()
data["plugin_id"] = self.plugin_id
return data
class PluginNotFoundError(DomainError):
"""插件不存在错误。
操作的插件不存在时抛出FR-32HTTP 404
"""
error_code = "PLUGIN_NOT_FOUND"
def __init__(
self,
plugin_id: str,
*,
trace_id: str | None = None,
) -> None:
super().__init__(f"Plugin not found: {plugin_id}", trace_id=trace_id)
self.plugin_id = plugin_id
def to_dict(self) -> dict[str, Any]:
data = super().to_dict()
data["plugin_id"] = self.plugin_id
return data
class PairingExpiredError(DomainError):
"""配对审批过期错误。
DM 配对审批记录已过期时抛出FR-33HTTP 410
"""
error_code = "PAIRING_EXPIRED"
def __init__(
self,
pairing_id: str,
*,
trace_id: str | None = None,
) -> None:
super().__init__(f"Pairing expired: {pairing_id}", trace_id=trace_id)
self.pairing_id = pairing_id
def to_dict(self) -> dict[str, Any]:
data = super().to_dict()
data["pairing_id"] = self.pairing_id
return data
class DmDeniedError(DomainError):
"""DM 被拒绝错误。
DM 安全策略拒绝消息时抛出FR-33HTTP 403
"""
error_code = "DM_DENIED"
def __init__(
self,
reason: str,
*,
trace_id: str | None = None,
) -> None:
super().__init__(f"DM denied: {reason}", trace_id=trace_id)
self.reason = reason
def to_dict(self) -> dict[str, Any]:
data = super().to_dict()
data["reason"] = self.reason
return data
class RateLimitError(DomainError):
"""限流错误。
请求频率超出限制时抛出FR-19HTTP 429携带建议重试等待时间。
"""
error_code = "RATE_LIMIT"
def __init__(
self,
resource: str,
retry_after_ms: int,
*,
trace_id: str | None = None,
) -> None:
super().__init__(
f"Rate limit exceeded on {resource}, retry after {retry_after_ms}ms",
trace_id=trace_id,
)
self.resource = resource
self.retry_after_ms = retry_after_ms
def to_dict(self) -> dict[str, Any]:
data = super().to_dict()
data["resource"] = self.resource
data["retry_after_ms"] = self.retry_after_ms
data["retry_after"] = max(1, self.retry_after_ms // 1000)
return data
class CapabilityNotProvenError(DomainError):
"""能力证明失败错误。
声明的能力在运行时证明失败时抛出FR-08/FR-21HTTP 422
"""
error_code = "CAPABILITY_NOT_PROVEN"
def __init__(
self,
capability: str,
*,
trace_id: str | None = None,
) -> None:
super().__init__(
f"Capability not proven: {capability}",
trace_id=trace_id,
)
self.capability = capability
def to_dict(self) -> dict[str, Any]:
data = super().to_dict()
data["capability"] = self.capability
return data
class ConfigRestartRequiredError(DomainError):
"""配置需重启错误。
``hybrid`` 模式下不可热更新的配置项变更时抛出FR-37HTTP 409
"""
error_code = "CONFIG_RESTART_REQUIRED"
def __init__(
self,
key: str,
*,
trace_id: str | None = None,
) -> None:
super().__init__(
f"Config restart required: {key}",
trace_id=trace_id,
)
self.key = key
def to_dict(self) -> dict[str, Any]:
data = super().to_dict()
data["key"] = self.key
return data
class ConfigNotHotReloadableError(DomainError):
"""配置不可热更新错误。
``hot`` 模式下不可热更新的配置项变更时抛出FR-37HTTP 422
"""
error_code = "CONFIG_NOT_HOT_RELOADABLE"
def __init__(
self,
key: str,
*,
trace_id: str | None = None,
) -> None:
super().__init__(
f"Config not hot reloadable: {key}",
trace_id=trace_id,
)
self.key = key
def to_dict(self) -> dict[str, Any]:
data = super().to_dict()
data["key"] = self.key
return data
class ConfigVersionConflictError(DomainError):
"""配置版本冲突错误。
并发修改配置导致版本冲突时抛出FR-37HTTP 409
"""
error_code = "CONFIG_VERSION_CONFLICT"
def __init__(
self,
expected: int,
actual: int,
*,
trace_id: str | None = None,
) -> None:
super().__init__(
f"Version conflict: expected {expected}, actual {actual}",
trace_id=trace_id,
)
self.expected = expected
self.actual = actual
def to_dict(self) -> dict[str, Any]:
data = super().to_dict()
data["expected"] = self.expected
data["actual"] = self.actual
return data
class ConfigValidationError(DomainError):
"""配置校验失败错误。
配置 schema 校验失败时抛出FR-37HTTP 400携带错误明细列表。
"""
error_code = "CONFIG_VALIDATION"
def __init__(
self,
errors: list[str],
*,
trace_id: str | None = None,
) -> None:
super().__init__(
f"Config validation failed: {', '.join(errors)}",
trace_id=trace_id,
)
self.errors = errors
def to_dict(self) -> dict[str, Any]:
data = super().to_dict()
data["errors"] = list(self.errors)
return data
class SchemaInitializationError(DomainError):
"""Schema 初始化失败错误。
渠道专属表 ``ensure_channel_schema()`` 初始化失败时抛出INV-I5HTTP 500
必须导致宿主启动失败。
"""
error_code = "SCHEMA_INIT_FAILED"
def __init__(
self,
reason: str,
*,
trace_id: str | None = None,
) -> None:
super().__init__(
f"Schema initialization failed: {reason}",
trace_id=trace_id,
)
self.reason = reason
def to_dict(self) -> dict[str, Any]:
data = super().to_dict()
data["reason"] = self.reason
return data
class ConfigRollbackError(DomainError):
"""配置回滚失败错误。
配置热更新失败后回滚也失败时抛出FR-37HTTP 500携带失败原因。
"""
error_code = "CONFIG_ROLLBACK_FAILED"
def __init__(
self,
key: str,
reason: str,
*,
trace_id: str | None = None,
) -> None:
super().__init__(
f"Config rollback failed for key: {key}",
trace_id=trace_id,
)
self.key = key
self.reason = reason
def to_dict(self) -> dict[str, Any]:
data = super().to_dict()
data["key"] = self.key
data["reason"] = self.reason
return data
class PluginFailedError(DomainError):
"""插件失败错误。
插件初始化、启动或运行时失败时抛出FR-32触发 FR-36 优雅降级。
"""
error_code = "PLUGIN_FAILED"
def __init__(
self,
plugin_id: str,
reason: str,
*,
trace_id: str | None = None,
) -> None:
super().__init__(
f"Plugin {plugin_id} failed: {reason}",
trace_id=trace_id,
)
self.plugin_id = plugin_id
self.reason = reason
def to_dict(self) -> dict[str, Any]:
data = super().to_dict()
data["plugin_id"] = self.plugin_id
data["reason"] = self.reason
return data
class PipelineConfigError(DomainError):
"""管道配置错误。
管道装配时校验失败抛出(如阶段 ``compensate`` 字段指向不存在的阶段 ID
属于编程错误,必须在管道构造期 fail-fast 暴露,避免运行期补偿阶段查找
静默失败HTTP 500
"""
error_code = "PIPELINE_CONFIG_ERROR"
def __init__(
self,
pipeline: str,
stage_id: str,
compensate: str,
*,
trace_id: str | None = None,
) -> None:
super().__init__(
f"Pipeline '{pipeline}' stage '{stage_id}' references unknown compensate stage '{compensate}'",
trace_id=trace_id,
)
self.pipeline = pipeline
self.stage_id = stage_id
self.compensate = compensate
def to_dict(self) -> dict[str, Any]:
data = super().to_dict()
data["pipeline"] = self.pipeline
data["stage_id"] = self.stage_id
data["compensate"] = self.compensate
return data
class LifecycleHookError(DomainError):
"""生命周期回调失败异常。
由 ``LifecycleAdapter`` 的四个回调方法(``afterAccountConfigWritten`` /
``beforeAccountDelete`` / ``onAccountEnabled`` / ``onAccountDisabled``
在回调失败时抛出。编排链路捕获后按"不阻塞主流程"策略处理:记录告警
审计,响应附 ``warning`` 字段,不回滚已落库的状态/配置。
``ChannelDegradedError`` / ``DependencyError`` / 超时等异常在
``AccountLifecycleService`` 中统一转换为本异常的等价表示(``LifecycleCallbackResult``
保留原始 traceback``exc_info`` 日志记录)。
字段:
hook: 回调方法名(如 ``afterAccountConfigWritten``)。
reason: 失败原因摘要。
"""
error_code = "LIFECYCLE_HOOK_ERROR"
def __init__(self, hook: str, reason: str, *, trace_id: str = "") -> None:
super().__init__(
f"lifecycle hook '{hook}' failed: {reason}",
trace_id=trace_id,
)
self.hook = hook
self.reason = reason
def to_dict(self) -> dict[str, Any]:
data = super().to_dict()
data["hook"] = self.hook
data["reason"] = self.reason
return data
class ContentReviewError(DomainError):
"""内容审核错误基类。
表示内容审核域的领域规则违反或业务状态异常。子类覆盖内容违规等场景。
"""
error_code = "CONTENT_REVIEW_ERROR"
class ContentViolationError(ContentReviewError):
"""内容违规错误(管道自动审核命中 block 时抛出)。
预留错误类型:本期不实现管道自动审核,本错误类不投入使用。
未来入站 / 出站管道自动审核命中 ``block`` 时抛出HTTP 422
``raiseOnControlFailure`` 的 error_code 映射表届时同步扩展收录
``CONTENT_VIOLATION``。本期 3 个端点CR-01/02/03不抛此错误
CR-01 预审核仅返回 ``verdict=block``CR-02/03 为纯查询。
"""
error_code = "CONTENT_VIOLATION"
class AgentCollaborationError(DomainError):
"""Agent 协作错误基类。
表示多 Agent 协作场景的领域规则违反或业务状态异常。子类覆盖
Agent 不可用、移交失败等具体场景。
"""
error_code = "AGENT_COLLABORATION_ERROR"
class AgentNotAvailableError(AgentCollaborationError):
"""目标 Agent 不可用错误。
协作目标 Agent 不存在或未启用时抛出HTTP 503
"""
error_code = "AGENT_NOT_AVAILABLE"
class AgentHandoffFailedError(AgentCollaborationError):
"""Agent 移交失败错误。
Agent 间移交操作失败时抛出HTTP 500
"""
error_code = "AGENT_HANDOFF_FAILED"