Files
group_fqcd_jr/app/core/errors.py
T
lzf_0626 6516ccb385 feat: 第二版——接口契约对齐 docs/05,修复静默故障与数据库基线
相对第一版 46fc976 的完整变更。组员迁移对照表见 docs/20。

一、对外契约对齐 docs/05(破坏性,共 4 处,组员需按 docs/20 调整)
1) 配置发布端点改为文档规定的复数资源名:submit→validations、
   approve→reviews(需 body decision)、activate→activations、
   rollback→rollbacks;第一版这 4 个动词式路径 docs/05 从未定义过。
2) 错误码由 8 个笼统码改为 15 个具体语义码(FORBIDDEN→AGENT_PERMISSION_DENIED、
   UNAUTHORIZED→AUTHENTICATION_REQUIRED、CONFLICT→RESOURCE_VERSION_CONFLICT、
   RESOURCE_NOT_FOUND→RUN_NOT_FOUND/SESSION_NOT_FOUND 等),
   输入类错误状态码 400→422。
3) POST /api/v1/agent-runs 与 GET /api/v1/agent-runs/{run_id} 统一为
   {data, meta} 信封(data 内字段名与语义未变)。
4) 错误响应体统一为 {error:{code,message,retryable,field_errors}, meta:{trace_id}},
   不再返回 FastAPI 默认的 {"detail": ...}。

二、数据库基线与约束
新增 39 张表的基线迁移(链根)与联合唯一键纠偏(4 张表、删 8 增 4,幂等收敛);
撤下 config_release 的双人复核 CHECK(应用层已允许自审,审核节点保留,
自审如实写入 reviewer_id);记忆 active key 生成列与唯一键;
activate 开始记录 supersedes_release_id 使版本链可追溯。
docs/00 基线未修改,未重命名或删除任何表与字段。

三、修复会静默出错或无报错的缺陷
- 跑完集成测试后平台会静默失去生效配置:清理只删自己创建的版本,却没有恢复被它
  顶成 superseded 的原生效版本,且审计一并删除因而完全无痕,表现为所有工具被拒
  但没有任何报错。已修清理逻辑并加恢复。
- Worker 单轮异常导致进程退出;记忆抽取调用方的“事务已开始”异常;
  召回缓存丢失 degraded 标记;连接时区未生效导致 created_at/updated_at 差 8 小时;
  .env 与 os.getenv 密钥来源分裂导致“没有可用的已批准模型端点”。
- 记忆信号识别漏判与跨键误命中;SSE 未带 Accept 的协商行为。

四、功能补齐
记忆链路 P1/P2/P3(抽取、受控词表、召回与缓存、生命周期级联及投影事件)、
fin_* 场内交易只读 ORM 层、agent_intent_config 状态流转并在运行期真正生效、
限流(Redis 固定窗口、故障一律放行)、游标校验、trace_id 中间件、
示例业务 Agent fund_query_demo 与一键端到端验证脚本,以及审计/指纹/迁移状态工具。

五、文档与验证
新增 docs/19(业务 Agent 接入实操)、docs/20(第一版迁移指南)与 docs/evidence 证据;
docs/01/02/06/08/09/17 同步实现现状。

验证结果:ruff 通过、mypy 103 文件无错、unit+contract 447 passed、
integration 29 passed、acceptance_check --production 7 PASS、
demo_agent_e2e 9/9 PASS(含失败关闭反证)。
2026-09-10 15:55:54 +08:00

208 lines
6.6 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 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"