Files
group_fqcd_jr/app/core/errors.py
T

212 lines
6.7 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.
"""HTTP 错误码唯一口径。
权威来源:`docs/05-接口文档.md` §3.5(HTTP 状态码)、§3.6(核心错误码)以及各接口小节
补充的 `SSE_NOT_ACCEPTABLE`、`FEEDBACK_ALREADY_EXISTS`、`INVALID_CURSOR` 等码。
实现不得再自造码;新增场景必须先落到文档已有码上。
`retryable=None` 表示按 `status_code >= 500` 推导,与文档表中标注的个例
(如 `RESOURCE_VERSION_CONFLICT` 为 409 但可重试)用 `retryable=False/True` 显式覆盖。
"""
class AgentError(Exception):
"""基类。未分类内部错误按文档映射到 `AGENT_INTERNAL_ERROR` / 500。"""
code = "AGENT_INTERNAL_ERROR"
status_code = 500
# 文档 §3.6 的“是否可重试”列:500 标注“视情况”,基类按不可重试处理。
retryable: bool | None = False
def __init__(self, message: str) -> None:
super().__init__(message)
self.message = message
# None 表示未声明:按 status_code >= 500 推导,与文档主表一致。
declared = type(self).retryable
self.retryable = self.status_code >= 500 if declared is None else declared
@property
def is_retryable(self) -> bool:
"""固化为布尔,供错误信封(文档 §3.4)直接序列化。"""
return bool(self.retryable)
# --- 输入、鉴权与权限 -------------------------------------------------------
class ValidationAgentError(AgentError):
"""已解析请求不满足字段或业务输入约束(文档 §3.5 的 422)。"""
code = "AGENT_INPUT_INVALID"
status_code = 422
class UnauthorizedAgentError(AgentError):
"""Token 缺失或无效(文档 §3.6 `AUTHENTICATION_REQUIRED`)。"""
code = "AUTHENTICATION_REQUIRED"
status_code = 401
class ForbiddenAgentError(AgentError):
"""角色、入口或数据范围不允许(文档 §3.6 `AGENT_PERMISSION_DENIED`)。"""
code = "AGENT_PERMISSION_DENIED"
status_code = 403
class OnboardingRequiredError(ForbiddenAgentError):
"""开户前置条件未满足,沿用文档登记的权限错误码。"""
class AgentPermissionDeniedError(ForbiddenAgentError):
"""`AGENT_PERMISSION_DENIED` 的语义化别名,供新代码使用。"""
# --- 404:必须按资源语义细分,禁止使用通用 404 ------------------------------
class ResourceNotFoundError(AgentError):
"""通用 404 基类,只用于继承与 `except`,禁止直接抛出。
文档 §3.6 没有与实现无关的通用 404 码,因此这里取主表中最接近“资源被隐藏”
语义的 `SESSION_NOT_FOUND`;`tests/unit/core/test_errors.py` 用 AST 校验
所有抛错点都使用语义正确的子类,绝不会落到这个兜底值上。
"""
code = "SESSION_NOT_FOUND"
status_code = 404
class SessionNotFoundError(ResourceNotFoundError):
code = "SESSION_NOT_FOUND"
class SessionNotAccessibleError(ResourceNotFoundError):
"""当前身份不能访问会话;按文档 §4.2 资源隐藏规则同样返回 404。"""
code = "SESSION_NOT_ACCESSIBLE"
class AgentTypeNotFoundError(ValidationAgentError):
code = "AGENT_TYPE_NOT_FOUND"
status_code = 404
class RunNotFoundError(ResourceNotFoundError):
"""运行不存在或不可见;越权与不存在必须返回同一码,不泄露存在性。"""
code = "RUN_NOT_FOUND"
class GenericResourceNotFoundError(ResourceNotFoundError):
"""已对当前身份隐藏的非会话、非运行资源(消息、工单、客户、管理面资源等)。
文档 §3.6 未给这类通用 404 单独定义码;按同一资源隐藏语义复用
`SESSION_NOT_FOUND`,避免实现再造新码。
"""
code = "SESSION_NOT_FOUND"
class ReferenceNotFoundError(GenericResourceNotFoundError):
"""知识引用不存在或不可解析时的语义化别名。"""
# --- 409:幂等、版本与运行状态 ----------------------------------------------
class ConflictAgentError(AgentError):
"""通用 409 基类,只用于继承与 `except`,禁止直接抛出。
文档 §3.5 把非法状态转换归 409,主表里对应的码是 `RUN_NOT_CANCELLABLE`;
具体抛错点必须使用下面的语义子类。
"""
code = "RUN_NOT_CANCELLABLE"
status_code = 409
class IdempotencyConflictError(ConflictAgentError):
code = "IDEMPOTENCY_CONFLICT"
class ResourceVersionConflictError(ConflictAgentError):
"""`If-Match` 版本过期(文档 §5.3);文档标注可重试。"""
code = "RESOURCE_VERSION_CONFLICT"
retryable = True
class RunNotCancellableError(ConflictAgentError):
"""运行已成功、失败或进入最终提交事务(文档 §6.4)。"""
code = "RUN_NOT_CANCELLABLE"
# 注意:文档 §3.6 主表列有 `RUN_CANCELLED`,但 §6.4 把它明确定义为
# `request_idempotency` 的**状态标识**(`status='failed'` + `error_code='RUN_CANCELLED'`),
# 而不是 HTTP 响应错误码;同一运行重复取消必须幂等返回同一状态。因此这里
# **不提供** `RUN_CANCELLED` 的 HTTP 异常类,避免实现把它当成客户端错误返回。
class RunLeaseLostError(ConflictAgentError):
"""Worker 租约失效或不能覆盖运行终态。"""
code = "RUN_NOT_CANCELLABLE"
class InvalidStateError(ConflictAgentError):
"""非法状态转换且不属于运行取消/版本冲突场景(文档 §3.5 归 409)。"""
code = "RUN_NOT_CANCELLABLE"
class ResourceAlreadyExistsError(ConflictAgentError):
"""唯一性约束冲突(文档 §3.5 归 409)。"""
code = "IDEMPOTENCY_CONFLICT"
# --- 400、429、503、504 -----------------------------------------------------
class InvalidCursorError(AgentError):
code = "INVALID_CURSOR"
status_code = 400
class RateLimitedError(AgentError):
code = "RATE_LIMITED"
status_code = 429
retryable = True
class RecoverableAgentError(AgentError):
"""必需依赖不可用(文档 §3.6 `DEPENDENCY_UNAVAILABLE`,可重试)。"""
code = "DEPENDENCY_UNAVAILABLE"
status_code = 503
retryable = True
class DependencyUnavailableError(RecoverableAgentError):
code = "DEPENDENCY_UNAVAILABLE"
class UpstreamTimeoutError(RecoverableAgentError):
"""上游超过时间预算(文档 §3.6 `UPSTREAM_TIMEOUT`)。"""
code = "UPSTREAM_TIMEOUT"
status_code = 504
# --- SSE 与反馈 -------------------------------------------------------------
class SseNotAcceptableError(AgentError):
"""`Accept` 不接受 `text/event-stream`(文档 §2 运行事件接口)。"""
code = "SSE_NOT_ACCEPTABLE"
status_code = 406
class FeedbackAlreadyExistsError(ConflictAgentError):
"""同一用户对同一消息重复提交且内容不同(文档 §7.5)。"""
code = "FEEDBACK_ALREADY_EXISTS"