新增完整的 channels 限界上下文模块,包含契约层、领域核心层、应用服务、管道编排、插件体系、基础设施组合根等全层级代码,新增飞书与微信 iLink 渠道插件基础结构,补充各类 DTO、端口协议与领域服务实现。
576 lines
15 KiB
Python
576 lines
15 KiB
Python
"""领域错误类型定义。
|
||
|
||
定义 ``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-36,HTTP 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-33,HTTP 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-32,HTTP 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-32,HTTP 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-33,HTTP 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-33,HTTP 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-19,HTTP 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-21,HTTP 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-37,HTTP 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-37,HTTP 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-37,HTTP 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-37,HTTP 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-I5,HTTP 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-37,HTTP 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"
|