- ① 后端删除 app/ai_provider/(38 文件,含 anthropic/openai 格式、billing、audio、embedding)、services/ai_provider_admin.py、core/config/model_config.py、tests/ai_provider/(21 文件) - ② 连带退役 application/agent/(旧 Agent 循环/steer/follow-up/确认注册表)、application/skill_executor.py、app/skills/(python skills 孤儿)、endpoints/audio.py、application/imports.py(ProviderManager 消费方);settings 端点收敛为生成偏好 - ③ runs.py 修剪:AgentRunRegistry/steer/follow-up/confirmations 端点退役,保留 list/get/events/cancel/stream;safety 确认改为直接落库裁决(pending→approved/denied) - ④ 前端删除:models 页、ModelGatewayView、gateway.ts、GatewayPanel(玄关)、ModelGatewayPanel/ModelStrategyForm、audio.ts/useVoiceInput、网关类型与 mock;设置页收敛为外观/语言/通知 - ⑤ 配置收敛:config/models|providers|pricing.example.yaml 归档 shenshi/config/;.env.example 删 provider keys/MODEL_ENDPOINT_POOL/MODEL_CONFIG_PATH,收敛 SHENSHI_* - ⑥ 验收:rg 审计灵台 Provider SDK import 与 ai_provider 引用归零;后端 528 passed(2 个预存失败经 HEAD 复跑验证与本手术无关)、前端 type-check+201 passed、神识 go vet+test、BFF 4 passed
179 lines
6.4 KiB
Python
179 lines
6.4 KiB
Python
"""API 层依赖注入。
|
|
|
|
从 ``app.state`` 取 lifespan 装配的共享运行时(ToolRuntime / ConfirmationStore / EventStore)。
|
|
CRUD 类路由直接调用 ``services`` 函数(函数内部自管 ``AsyncSessionLocal``),不在此注入 session。
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hmac
|
|
|
|
from fastapi import Depends, HTTPException, Request
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.application.tool_runtime.runtime import ToolRuntime
|
|
from app.core.config import settings
|
|
from app.db.session import get_session
|
|
from app.modules.runtime.events import EventStore
|
|
from app.modules.safety.confirmation import ConfirmationStore
|
|
from app.schemas.auth import CurrentUser as CurrentUserSchema
|
|
from app.services.auth import decode_token, get_user_by_id, get_user_permissions
|
|
|
|
|
|
def _unavailable(message: str) -> HTTPException:
|
|
return HTTPException(
|
|
status_code=503,
|
|
detail={"error": {"code": "service_unavailable", "message": message}},
|
|
)
|
|
|
|
|
|
def get_tool_runtime(request: Request) -> ToolRuntime:
|
|
runtime: ToolRuntime | None = getattr(request.app.state, "tool_runtime", None)
|
|
if runtime is None:
|
|
raise _unavailable("工具运行时未就绪(沙箱未配置或未启动)")
|
|
return runtime
|
|
|
|
|
|
def get_optional_tool_runtime(request: Request) -> ToolRuntime | None:
|
|
"""宽松版:tool_runtime 未就绪时返回 None(不抛 503),供聚合视图降级呈现。"""
|
|
return getattr(request.app.state, "tool_runtime", None)
|
|
|
|
|
|
def get_confirmation_store(request: Request) -> ConfirmationStore:
|
|
store = getattr(request.app.state, "confirmation_store", None)
|
|
if store is None:
|
|
raise _unavailable("确认存储未就绪")
|
|
return store
|
|
|
|
|
|
def get_event_store(request: Request) -> EventStore:
|
|
store = getattr(request.app.state, "event_store", None)
|
|
if store is None:
|
|
raise _unavailable("事件存储未就绪")
|
|
return store
|
|
|
|
|
|
async def require_admin(
|
|
request: Request,
|
|
db: AsyncSession = Depends(get_session),
|
|
) -> None:
|
|
"""管理端点鉴权:JWT + is_admin 优先,ADMIN_TOKEN fallback。
|
|
|
|
JWT 模式下:
|
|
- 有效 JWT 且 is_admin=True → 放行
|
|
- 有效 JWT 且 is_admin=False → 403
|
|
- 无效 JWT → 回退 ADMIN_TOKEN 校验
|
|
ADMIN_TOKEN 模式下(原 v1 逻辑):
|
|
- 同现有逻辑,常数时间比较
|
|
- ADMIN_TOKEN 未配置且无 JWT → 503 fail-closed
|
|
"""
|
|
# 0) fail-closed:两种鉴权均未配置时直接拒绝
|
|
if not settings.jwt_secret and not settings.admin_token:
|
|
raise HTTPException(
|
|
status_code=503,
|
|
detail={"error": {"code": "admin_not_configured", "message": "管理端点未启用。"}},
|
|
)
|
|
|
|
credential = request.headers.get("Authorization", "")
|
|
if not credential.startswith("Bearer "):
|
|
raise HTTPException(
|
|
status_code=401,
|
|
detail={"error": {"code": "unauthorized", "message": "缺少管理员凭证。"}},
|
|
)
|
|
presented = credential[len("Bearer ") :]
|
|
|
|
# 1) 尝试 JWT 模式(如果 jwt_secret 已配置)
|
|
if settings.jwt_secret:
|
|
try:
|
|
payload = decode_token(presented)
|
|
if payload.get("type") == "access":
|
|
user_id = payload.get("sub")
|
|
user = await get_user_by_id(db, user_id)
|
|
if user is None or not user.is_active:
|
|
raise HTTPException(
|
|
status_code=401,
|
|
detail={
|
|
"error": {"code": "unauthorized", "message": "用户不存在或已禁用。"}
|
|
},
|
|
)
|
|
if not user.is_admin:
|
|
raise HTTPException(
|
|
status_code=403,
|
|
detail={
|
|
"error": {"code": "forbidden", "message": "仅管理员可访问此接口。"}
|
|
},
|
|
)
|
|
return # JWT 模式鉴权通过
|
|
except HTTPException:
|
|
raise
|
|
except Exception:
|
|
pass # JWT 无效,回退 ADMIN_TOKEN 校验
|
|
|
|
# 2) Fallback: ADMIN_TOKEN 校验(原 v1 逻辑)
|
|
token = settings.admin_token
|
|
if not token:
|
|
raise HTTPException(
|
|
status_code=503,
|
|
detail={"error": {"code": "admin_not_configured", "message": "管理端点未启用。"}},
|
|
)
|
|
if not hmac.compare_digest(presented, token):
|
|
raise HTTPException(
|
|
status_code=401,
|
|
detail={"error": {"code": "unauthorized", "message": "管理员凭证无效。"}},
|
|
)
|
|
|
|
|
|
async def get_current_user(
|
|
request: Request,
|
|
db: AsyncSession = Depends(get_session),
|
|
) -> CurrentUserSchema:
|
|
"""从 Authorization: Bearer <access_token> 解析当前用户。
|
|
|
|
JWT 未配置(jwt_secret 为空)时 503 fail-closed。
|
|
"""
|
|
if not settings.jwt_secret:
|
|
raise HTTPException(
|
|
status_code=503,
|
|
detail={
|
|
"error": {
|
|
"code": "service_unavailable",
|
|
"message": "认证服务未配置(JWT_SECRET 为空)。",
|
|
}
|
|
},
|
|
)
|
|
credential = request.headers.get("Authorization", "")
|
|
if not credential.startswith("Bearer "):
|
|
raise HTTPException(
|
|
status_code=401,
|
|
detail={"error": {"code": "unauthorized", "message": "缺少认证凭证。"}},
|
|
)
|
|
token = credential[len("Bearer ") :]
|
|
try:
|
|
payload = decode_token(token)
|
|
except Exception:
|
|
raise HTTPException(
|
|
status_code=401,
|
|
detail={"error": {"code": "unauthorized", "message": "Token 无效或已过期。"}},
|
|
) from None
|
|
if payload.get("type") != "access":
|
|
raise HTTPException(
|
|
status_code=401,
|
|
detail={"error": {"code": "unauthorized", "message": "Token 类型不匹配。"}},
|
|
)
|
|
user_id = payload.get("sub")
|
|
user = await get_user_by_id(db, user_id)
|
|
if user is None or not user.is_active:
|
|
raise HTTPException(
|
|
status_code=401,
|
|
detail={"error": {"code": "unauthorized", "message": "用户不存在或已禁用。"}},
|
|
)
|
|
perms = await get_user_permissions(db, user.id)
|
|
return CurrentUserSchema(
|
|
user_id=user.id,
|
|
username=user.username,
|
|
email=user.email,
|
|
owner_scope=user.id,
|
|
is_admin=user.is_admin,
|
|
permissions=perms,
|
|
)
|