Files
hunjing/backend/app/core/errors.py
T
chenxin 684261f4b4 M5:百工与司命——能力注册表、授契行契请命、禁域裁决与观察标证
- 百工域 domains/baigong:capabilities(统一能力契约:执行类型 builtin/qiankun/mcp/http + 四维风险标注 + 来源版本)、mandates(授契 grant / 行契 charter 预算时间窗,撤销立即生效)、tool_calls(净化摘要 + 观察标证);请命复用 pending_confirmations 加 expires_at/brief_json(超时=拒绝执行 HJ-1302,六要素说明)
- 司命裁决器 siming.py:四维取最高档(能力/数据/后果/情境);禁域为代码常量不可配置(递归删根/格式化/写裸设备/fork 炸弹、file:// 协议、密钥材料外发)——专项测试断言恶意标注与篡改对象仍拒 HJ-1301
- 执行链:禁域→裁决(HJ-1303 未授契拒且双留痕)→请命→执行(乾坤经 SandboxClient HMAC;MCP 经 call_tool;内置无副作用)→净化标证(观察非事实,敏感遮蔽+体量截断)→行动账
- MCP 登记/启停/刷新自动派生与撤下 capabilities(博闻登记接入点);内置能力种子(echo/datetime/qiankun.shell/web.fetch)
- 端点:/capabilities /mandates /petitions /tool-calls + /tools 兼容投影;前端百工页四区(能力目录四维标注/授契行契/请命批准拒绝/行动账观察标证)
- 退役:modules/safety|skills|tool_runtime|agent、application/tool_runtime、services/skills|safety、tool_policies/skills/model_endpoint_overrides 表(迁移 006)、前端 SkillManager/ToolsConsole/旧 tools 页
- 验收(test_baigong_siming 四用例):未授契被拒留痕、请命超时不执行、禁域不可关闭、结果为观察不入忆;后端 454、前端 220、BFF 4、神识 5 包全绿(2 个预存环境失败不变)
2026-08-27 19:17:21 +08:00

133 lines
4.8 KiB
Python

"""统一错误响应与异常处理。"""
from contextvars import ContextVar
from typing import Any
from fastapi import FastAPI, Request, status
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from starlette.exceptions import HTTPException as StarletteHTTPException
from app.core.ids import new_trace_id
from app.core.logging import log_with_context
from app.core.security import redact_value
class SafetyError(Exception):
"""司命决策异常(错误码映射契约字典 §15)。"""
def __init__(self, code: str, message: str) -> None:
super().__init__(message)
self.code = code
self.message = message
def __str__(self) -> str:
return self.message
trace_id_var: ContextVar[str | None] = ContextVar("trace_id", default=None)
ERROR_HTTP_STATUS: dict[str, int] = {
"invalid_request": status.HTTP_400_BAD_REQUEST,
"unauthorized": status.HTTP_401_UNAUTHORIZED,
"permission_denied": status.HTTP_403_FORBIDDEN,
"requires_confirmation": status.HTTP_403_FORBIDDEN,
"not_found": status.HTTP_404_NOT_FOUND,
"conflict": status.HTTP_409_CONFLICT,
"payload_too_large": status.HTTP_413_CONTENT_TOO_LARGE,
"unsupported_media_type": status.HTTP_415_UNSUPPORTED_MEDIA_TYPE,
"validation_failed": status.HTTP_422_UNPROCESSABLE_CONTENT,
"context_overflow": status.HTTP_400_BAD_REQUEST,
"run_timeout": status.HTTP_504_GATEWAY_TIMEOUT,
"model_error": status.HTTP_502_BAD_GATEWAY,
"model_rate_limited": status.HTTP_429_TOO_MANY_REQUESTS,
"model_timeout": status.HTTP_504_GATEWAY_TIMEOUT,
"model_config_error": status.HTTP_500_INTERNAL_SERVER_ERROR,
"model_unavailable": status.HTTP_503_SERVICE_UNAVAILABLE,
"model_provider_key_missing": status.HTTP_500_INTERNAL_SERVER_ERROR,
"tool_error": status.HTTP_502_BAD_GATEWAY,
"safety_rejected": status.HTTP_403_FORBIDDEN,
"rate_limited": status.HTTP_429_TOO_MANY_REQUESTS,
"internal_error": status.HTTP_500_INTERNAL_SERVER_ERROR,
}
class AppError(Exception):
def __init__(
self,
code: str,
message: str,
*,
status_code: int | None = None,
details: dict[str, Any] | None = None,
) -> None:
self.code = code
self.message = message
self.status_code = status_code or ERROR_HTTP_STATUS.get(code, status.HTTP_400_BAD_REQUEST)
self.details = details or {}
def current_trace_id() -> str:
trace_id = trace_id_var.get()
if trace_id is None:
trace_id = new_trace_id()
trace_id_var.set(trace_id)
return trace_id
def error_response(error: AppError) -> JSONResponse:
return JSONResponse(
status_code=error.status_code,
content={
"error": {
"code": error.code,
"message": error.message,
"trace_id": current_trace_id(),
"details": redact_value(error.details),
}
},
)
def register_exception_handlers(app: FastAPI) -> None:
@app.exception_handler(AppError)
async def handle_app_error(_request: Request, exc: AppError) -> JSONResponse:
return error_response(exc)
@app.exception_handler(SafetyError)
async def handle_safety_error(_request: Request, exc: SafetyError) -> JSONResponse:
# 司命异常自带契约错误码(如 permission_denied → 403),按错误码映射 HTTP 状态。
return error_response(AppError(exc.code, exc.message))
@app.exception_handler(RequestValidationError)
async def handle_validation_error(
_request: Request, exc: RequestValidationError
) -> JSONResponse:
return error_response(
AppError(
"validation_failed",
"请求字段校验失败。",
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
details={"errors": exc.errors()},
)
)
@app.exception_handler(StarletteHTTPException)
async def handle_http_error(_request: Request, exc: StarletteHTTPException) -> JSONResponse:
code = "not_found" if exc.status_code == status.HTTP_404_NOT_FOUND else "invalid_request"
return error_response(AppError(code, str(exc.detail), status_code=exc.status_code))
@app.exception_handler(Exception)
async def handle_unexpected_error(_request: Request, exc: Exception) -> JSONResponse:
log_with_context(
"未处理异常",
level="ERROR",
trace_id=current_trace_id(),
error_type=exc.__class__.__name__,
)
return error_response(
AppError(
"internal_error",
"服务端内部错误。",
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
details={"type": exc.__class__.__name__},
)
)