2026-01-30 12:43:08 +08:00
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
2026-03-21 12:57:09 +08:00
|
|
|
|
import ipaddress
|
2025-12-10 20:52:44 +08:00
|
|
|
|
import time
|
2026-03-31 19:19:04 +08:00
|
|
|
|
from dataclasses import dataclass
|
2026-03-09 22:57:23 +08:00
|
|
|
|
from datetime import datetime, timezone
|
2025-12-10 20:52:44 +08:00
|
|
|
|
from enum import Enum
|
2026-01-30 03:10:21 +08:00
|
|
|
|
from typing import TYPE_CHECKING, Any
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
from fastapi import HTTPException, Request
|
2026-03-09 22:57:23 +08:00
|
|
|
|
from fastapi.concurrency import run_in_threadpool
|
2026-03-09 10:38:14 +08:00
|
|
|
|
from fastapi.responses import JSONResponse
|
2026-03-02 18:02:21 +08:00
|
|
|
|
from sqlalchemy.exc import SQLAlchemyError
|
2025-12-18 01:59:40 +08:00
|
|
|
|
from sqlalchemy.orm import Session
|
2026-03-09 10:38:14 +08:00
|
|
|
|
from starlette.requests import ClientDisconnect
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
2026-01-06 16:29:03 +08:00
|
|
|
|
from src.config.settings import config
|
2026-01-07 14:55:07 +08:00
|
|
|
|
from src.core.enums import UserRole
|
2026-03-08 00:05:48 +08:00
|
|
|
|
from src.core.exceptions import BalanceInsufficientException
|
2025-12-10 20:52:44 +08:00
|
|
|
|
from src.core.logger import logger
|
2026-03-09 22:57:23 +08:00
|
|
|
|
from src.database.database import create_session
|
2026-01-07 14:55:07 +08:00
|
|
|
|
from src.models.database import ApiKey, AuditEventType, User
|
2025-12-10 20:52:44 +08:00
|
|
|
|
from src.services.auth.service import AuthService
|
2026-03-17 16:34:09 +08:00
|
|
|
|
from src.services.auth.session_service import SessionService
|
2026-03-15 14:22:59 +08:00
|
|
|
|
from src.services.rate_limit.user_rpm_limiter import SYSTEM_RPM_CONFIG_KEY, get_user_rpm_limiter
|
2025-12-10 20:52:44 +08:00
|
|
|
|
from src.services.system.audit import AuditService
|
2026-03-15 14:22:59 +08:00
|
|
|
|
from src.services.system.config import SystemConfigService
|
2025-12-10 20:52:44 +08:00
|
|
|
|
from src.services.usage.service import UsageService
|
2026-03-08 00:05:48 +08:00
|
|
|
|
from src.services.wallet import WalletService
|
2026-02-05 14:22:11 +08:00
|
|
|
|
from src.utils.perf import PerfRecorder
|
2026-03-31 19:19:04 +08:00
|
|
|
|
from src.utils.request_utils import get_request_identity_metadata, update_request_state
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
2026-01-07 14:55:07 +08:00
|
|
|
|
if TYPE_CHECKING:
|
|
|
|
|
|
from src.models.database import ManagementToken
|
|
|
|
|
|
|
2025-12-10 20:52:44 +08:00
|
|
|
|
from .adapter import ApiAdapter, ApiMode
|
|
|
|
|
|
from .context import ApiRequestContext
|
|
|
|
|
|
|
2026-01-28 23:07:58 +08:00
|
|
|
|
# 高频轮询端点,抑制其 debug 日志以减少噪音
|
|
|
|
|
|
QUIET_POLLING_PATHS: set[str] = {
|
|
|
|
|
|
"/api/admin/usage/active",
|
|
|
|
|
|
"/api/admin/usage/records",
|
|
|
|
|
|
"/api/admin/usage/stats",
|
|
|
|
|
|
"/api/admin/usage/aggregation/stats",
|
|
|
|
|
|
"/api/admin/health/status",
|
2026-03-11 15:05:11 +08:00
|
|
|
|
"/api/wallet/today-cost",
|
2026-01-28 23:07:58 +08:00
|
|
|
|
}
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
2026-03-21 12:57:09 +08:00
|
|
|
|
TRUSTED_GATEWAY_HEADER = "x-aether-gateway"
|
refactor: 拆分 Rust gateway/executor 大文件为模块目录结构,拆分 Python gateway.py 为子模块
Rust 侧:
- executor.rs 拆分为 executor/ 目录 (plan_builders, stream, sync, submission)
- 新增 kiro_stream/, local_finalize/, local_stream/ 模块目录
- 新增 video_tasks.rs
- 测试文件 ai_execute/files/video 拆分为子目录
- handlers/headers/control/constants 扩展支持新模块
Python 侧:
- gateway.py 拆分为 24 个子模块 (routes, shared, contract, chat, cli, video, files, finalize, reporting 等)
- 新增 antigravity/gemini_cli/kiro 的 rust_http 适配层
- upstream_fetcher 增加 Rust sidecar 支持
- executor_plan/candidate/pipeline 适配调整
测试:
- 对应拆分 test_internal_gateway_routes 为子目录
- 新增 rust_http 相关测试
2026-03-23 17:19:15 +08:00
|
|
|
|
TRUSTED_GATEWAY_EXECUTION_PATH_HEADER = "x-aether-execution-path"
|
2026-03-21 12:57:09 +08:00
|
|
|
|
TRUSTED_AUTH_USER_ID_HEADER = "x-aether-auth-user-id"
|
|
|
|
|
|
TRUSTED_AUTH_API_KEY_ID_HEADER = "x-aether-auth-api-key-id"
|
|
|
|
|
|
TRUSTED_AUTH_BALANCE_HEADER = "x-aether-auth-balance-remaining"
|
|
|
|
|
|
TRUSTED_AUTH_ACCESS_ALLOWED_HEADER = "x-aether-auth-access-allowed"
|
2026-03-31 19:19:04 +08:00
|
|
|
|
TRUSTED_RATE_LIMIT_PREFLIGHT_HEADER = "x-aether-rate-limit-preflight"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass(slots=True)
|
|
|
|
|
|
class _PipelinePerfState:
|
|
|
|
|
|
request: Request
|
|
|
|
|
|
labels: dict[str, str]
|
|
|
|
|
|
sampled: bool
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def create(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
request: Request,
|
|
|
|
|
|
*,
|
|
|
|
|
|
adapter: ApiAdapter,
|
|
|
|
|
|
mode: ApiMode,
|
|
|
|
|
|
) -> _PipelinePerfState:
|
|
|
|
|
|
labels = {
|
|
|
|
|
|
"mode": getattr(mode, "value", str(mode)),
|
|
|
|
|
|
"adapter": adapter.name,
|
|
|
|
|
|
}
|
|
|
|
|
|
sampled = PerfRecorder.should_store_sample()
|
|
|
|
|
|
if sampled:
|
|
|
|
|
|
setattr(request.state, "perf_sampled", True)
|
|
|
|
|
|
setattr(
|
|
|
|
|
|
request.state,
|
|
|
|
|
|
"perf_metrics",
|
|
|
|
|
|
{"pipeline": {}, "sample_rate": getattr(config, "perf_store_sample_rate", 1.0)},
|
|
|
|
|
|
)
|
|
|
|
|
|
return cls(request=request, labels=labels, sampled=sampled)
|
|
|
|
|
|
|
|
|
|
|
|
def record_ms(self, key: str, duration: float | None) -> None:
|
|
|
|
|
|
if duration is None:
|
|
|
|
|
|
return
|
|
|
|
|
|
perf_metrics = getattr(self.request.state, "perf_metrics", None)
|
|
|
|
|
|
if not isinstance(perf_metrics, dict):
|
|
|
|
|
|
return
|
|
|
|
|
|
bucket = perf_metrics.setdefault("pipeline", {})
|
|
|
|
|
|
bucket[key] = int(duration * 1000)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass(slots=True)
|
|
|
|
|
|
class _PipelineAuthState:
|
|
|
|
|
|
user: User | None
|
|
|
|
|
|
api_key: ApiKey | None
|
|
|
|
|
|
management_token: ManagementToken | None
|
2026-03-21 12:57:09 +08:00
|
|
|
|
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
class ApiRequestPipeline:
|
2026-03-08 00:05:48 +08:00
|
|
|
|
"""负责统一执行认证、余额校验、上下文构建等通用逻辑的管道。"""
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
def __init__(
|
|
|
|
|
|
self,
|
|
|
|
|
|
auth_service: AuthService = AuthService,
|
|
|
|
|
|
usage_service: UsageService = UsageService,
|
|
|
|
|
|
audit_service: AuditService = AuditService,
|
|
|
|
|
|
):
|
|
|
|
|
|
self.auth_service = auth_service
|
|
|
|
|
|
self.usage_service = usage_service
|
|
|
|
|
|
self.audit_service = audit_service
|
|
|
|
|
|
|
2026-03-18 00:12:00 +08:00
|
|
|
|
def _commit_session_touch(self, db: Session, *, scope: str) -> None:
|
|
|
|
|
|
"""Persist session last_seen updates immediately to avoid holding row locks.
|
|
|
|
|
|
|
|
|
|
|
|
Admin usage views can execute heavy read queries after authentication.
|
|
|
|
|
|
If the request later stalls, leaving the session touch inside the request
|
|
|
|
|
|
transaction can block all subsequent requests that update the same
|
|
|
|
|
|
`user_sessions` row. Commit the touch in its own short transaction so
|
|
|
|
|
|
later long-running reads cannot keep the session row locked.
|
|
|
|
|
|
"""
|
|
|
|
|
|
original_expire_on_commit = getattr(db, "expire_on_commit", None)
|
|
|
|
|
|
try:
|
|
|
|
|
|
if original_expire_on_commit is not None:
|
|
|
|
|
|
db.expire_on_commit = False
|
|
|
|
|
|
db.commit()
|
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
|
try:
|
|
|
|
|
|
db.rollback()
|
|
|
|
|
|
except Exception as rollback_exc:
|
|
|
|
|
|
logger.debug("[Pipeline] {} session touch rollback failed: {}", scope, rollback_exc)
|
|
|
|
|
|
logger.warning("[Pipeline] failed to persist {} session touch: {}", scope, exc)
|
|
|
|
|
|
finally:
|
|
|
|
|
|
if original_expire_on_commit is not None:
|
|
|
|
|
|
db.expire_on_commit = original_expire_on_commit
|
|
|
|
|
|
|
2026-03-31 19:19:04 +08:00
|
|
|
|
def _touch_authenticated_session(
|
|
|
|
|
|
self,
|
|
|
|
|
|
request: Request,
|
|
|
|
|
|
db: Session,
|
|
|
|
|
|
*,
|
|
|
|
|
|
user_id: str,
|
|
|
|
|
|
session_id: str,
|
|
|
|
|
|
scope: str,
|
|
|
|
|
|
) -> Any:
|
|
|
|
|
|
"""Resolve and touch the authenticated user session via shared request identity."""
|
|
|
|
|
|
|
|
|
|
|
|
client_device_id = SessionService.extract_client_device_id(request)
|
|
|
|
|
|
session = SessionService.get_active_session(db, session_id, user_id)
|
|
|
|
|
|
if not session:
|
|
|
|
|
|
raise HTTPException(status_code=401, detail="登录会话已失效,请重新登录")
|
|
|
|
|
|
|
|
|
|
|
|
SessionService.assert_session_device_matches(session, client_device_id)
|
|
|
|
|
|
identity = get_request_identity_metadata(request)
|
|
|
|
|
|
session_touched = SessionService.touch_session(
|
|
|
|
|
|
session,
|
|
|
|
|
|
client_ip=identity.client_ip,
|
|
|
|
|
|
user_agent=identity.user_agent,
|
|
|
|
|
|
)
|
|
|
|
|
|
if session_touched:
|
|
|
|
|
|
self._commit_session_touch(db, scope=scope)
|
|
|
|
|
|
return session
|
|
|
|
|
|
|
2025-12-10 20:52:44 +08:00
|
|
|
|
async def run(
|
|
|
|
|
|
self,
|
|
|
|
|
|
adapter: ApiAdapter,
|
|
|
|
|
|
http_request: Request,
|
|
|
|
|
|
db: Session,
|
|
|
|
|
|
*,
|
|
|
|
|
|
mode: ApiMode = ApiMode.STANDARD,
|
2026-01-30 03:10:21 +08:00
|
|
|
|
api_format_hint: str | None = None,
|
|
|
|
|
|
path_params: dict[str, Any] | None = None,
|
2026-01-30 14:30:57 +08:00
|
|
|
|
) -> Any:
|
2026-03-31 19:19:04 +08:00
|
|
|
|
perf_state = _PipelinePerfState.create(http_request, adapter=adapter, mode=mode)
|
2026-01-28 23:07:58 +08:00
|
|
|
|
is_quiet = http_request.url.path in QUIET_POLLING_PATHS
|
refactor: 拆分 Rust gateway/executor 大文件为模块目录结构,拆分 Python gateway.py 为子模块
Rust 侧:
- executor.rs 拆分为 executor/ 目录 (plan_builders, stream, sync, submission)
- 新增 kiro_stream/, local_finalize/, local_stream/ 模块目录
- 新增 video_tasks.rs
- 测试文件 ai_execute/files/video 拆分为子目录
- handlers/headers/control/constants 扩展支持新模块
Python 侧:
- gateway.py 拆分为 24 个子模块 (routes, shared, contract, chat, cli, video, files, finalize, reporting 等)
- 新增 antigravity/gemini_cli/kiro 的 rust_http 适配层
- upstream_fetcher 增加 Rust sidecar 支持
- executor_plan/candidate/pipeline 适配调整
测试:
- 对应拆分 test_internal_gateway_routes 为子目录
- 新增 rust_http 相关测试
2026-03-23 17:19:15 +08:00
|
|
|
|
gateway_execution_path = self._capture_trusted_gateway_execution_path(http_request)
|
2026-03-31 19:19:04 +08:00
|
|
|
|
self._log_pipeline_entry(
|
|
|
|
|
|
http_request,
|
|
|
|
|
|
adapter,
|
|
|
|
|
|
mode,
|
|
|
|
|
|
gateway_execution_path=gateway_execution_path,
|
|
|
|
|
|
quiet=is_quiet,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
auth_state = await self._authenticate_request_legacy(
|
|
|
|
|
|
http_request,
|
|
|
|
|
|
db,
|
|
|
|
|
|
adapter,
|
|
|
|
|
|
mode=mode,
|
|
|
|
|
|
quiet=is_quiet,
|
|
|
|
|
|
perf_state=perf_state,
|
|
|
|
|
|
)
|
|
|
|
|
|
await self._apply_legacy_request_guards(http_request, db, mode=mode, auth_state=auth_state)
|
|
|
|
|
|
|
|
|
|
|
|
context_or_response = await self._build_pipeline_context_legacy(
|
|
|
|
|
|
adapter,
|
|
|
|
|
|
http_request,
|
|
|
|
|
|
db,
|
|
|
|
|
|
mode=mode,
|
|
|
|
|
|
api_format_hint=api_format_hint,
|
|
|
|
|
|
path_params=path_params,
|
|
|
|
|
|
auth_state=auth_state,
|
|
|
|
|
|
quiet=is_quiet,
|
|
|
|
|
|
perf_state=perf_state,
|
|
|
|
|
|
)
|
|
|
|
|
|
if isinstance(context_or_response, JSONResponse):
|
|
|
|
|
|
return context_or_response
|
|
|
|
|
|
|
|
|
|
|
|
return await self._dispatch_adapter_request_legacy(
|
|
|
|
|
|
adapter,
|
|
|
|
|
|
context_or_response,
|
|
|
|
|
|
perf_state=perf_state,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def _log_pipeline_entry(
|
|
|
|
|
|
self,
|
|
|
|
|
|
request: Request,
|
|
|
|
|
|
adapter: ApiAdapter,
|
|
|
|
|
|
mode: ApiMode,
|
|
|
|
|
|
*,
|
|
|
|
|
|
gateway_execution_path: str | None,
|
|
|
|
|
|
quiet: bool,
|
|
|
|
|
|
) -> None:
|
|
|
|
|
|
if quiet:
|
|
|
|
|
|
return
|
|
|
|
|
|
logger.debug(
|
|
|
|
|
|
"[Pipeline] {} {} | adapter={}, mode={}, gateway_path={}",
|
|
|
|
|
|
request.method,
|
|
|
|
|
|
request.url.path,
|
|
|
|
|
|
adapter.__class__.__name__,
|
|
|
|
|
|
mode,
|
|
|
|
|
|
gateway_execution_path or "-",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
async def _authenticate_request_legacy(
|
|
|
|
|
|
self,
|
|
|
|
|
|
request: Request,
|
|
|
|
|
|
db: Session,
|
|
|
|
|
|
adapter: ApiAdapter,
|
|
|
|
|
|
*,
|
|
|
|
|
|
mode: ApiMode,
|
|
|
|
|
|
quiet: bool,
|
|
|
|
|
|
perf_state: _PipelinePerfState,
|
|
|
|
|
|
) -> _PipelineAuthState:
|
|
|
|
|
|
"""Legacy Python auth/session entrypoint.
|
|
|
|
|
|
|
|
|
|
|
|
Rust bridge can currently short-circuit only the client API-key path via
|
|
|
|
|
|
trusted gateway headers. JWT/session and management-token flows still
|
|
|
|
|
|
terminate in Python.
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
auth_start = PerfRecorder.start(force=perf_state.sampled)
|
2026-02-05 14:22:11 +08:00
|
|
|
|
try:
|
|
|
|
|
|
if mode == ApiMode.ADMIN:
|
2026-03-31 19:19:04 +08:00
|
|
|
|
user, management_token = await self._authenticate_admin(request, db)
|
|
|
|
|
|
return _PipelineAuthState(user=user, api_key=None, management_token=management_token)
|
|
|
|
|
|
if mode == ApiMode.USER:
|
|
|
|
|
|
user, management_token = await self._authenticate_user(request, db)
|
|
|
|
|
|
return _PipelineAuthState(user=user, api_key=None, management_token=management_token)
|
|
|
|
|
|
if mode == ApiMode.PUBLIC:
|
|
|
|
|
|
return _PipelineAuthState(user=None, api_key=None, management_token=None)
|
|
|
|
|
|
if mode == ApiMode.MANAGEMENT:
|
|
|
|
|
|
user, management_token = await self._authenticate_management(request, db)
|
|
|
|
|
|
return _PipelineAuthState(user=user, api_key=None, management_token=management_token)
|
|
|
|
|
|
|
|
|
|
|
|
user, api_key = await self._authenticate_client(
|
|
|
|
|
|
request,
|
|
|
|
|
|
db,
|
|
|
|
|
|
adapter,
|
|
|
|
|
|
quiet=quiet,
|
|
|
|
|
|
)
|
|
|
|
|
|
return _PipelineAuthState(user=user, api_key=api_key, management_token=None)
|
2026-02-05 14:22:11 +08:00
|
|
|
|
finally:
|
2026-03-31 19:19:04 +08:00
|
|
|
|
auth_duration = PerfRecorder.stop(
|
|
|
|
|
|
auth_start,
|
|
|
|
|
|
"pipeline_auth",
|
|
|
|
|
|
labels=perf_state.labels,
|
|
|
|
|
|
)
|
|
|
|
|
|
perf_state.record_ms("auth_ms", auth_duration)
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
2026-03-31 19:19:04 +08:00
|
|
|
|
async def _apply_legacy_request_guards(
|
|
|
|
|
|
self,
|
|
|
|
|
|
request: Request,
|
|
|
|
|
|
db: Session,
|
|
|
|
|
|
*,
|
|
|
|
|
|
mode: ApiMode,
|
|
|
|
|
|
auth_state: _PipelineAuthState,
|
|
|
|
|
|
) -> None:
|
|
|
|
|
|
"""Python-side request guards kept for shared limiter/config coupling."""
|
2026-03-15 14:22:59 +08:00
|
|
|
|
|
2026-03-31 19:19:04 +08:00
|
|
|
|
if mode not in {ApiMode.STANDARD, ApiMode.PROXY}:
|
|
|
|
|
|
return
|
|
|
|
|
|
if not auth_state.user or not auth_state.api_key:
|
|
|
|
|
|
return
|
|
|
|
|
|
if self._trusted_gateway_completed_rate_limit_preflight(request):
|
|
|
|
|
|
return
|
|
|
|
|
|
await self._check_user_rate_limit(request, db, auth_state.user, auth_state.api_key)
|
|
|
|
|
|
|
|
|
|
|
|
async def _build_pipeline_context_legacy(
|
|
|
|
|
|
self,
|
|
|
|
|
|
adapter: ApiAdapter,
|
|
|
|
|
|
request: Request,
|
|
|
|
|
|
db: Session,
|
|
|
|
|
|
*,
|
|
|
|
|
|
mode: ApiMode,
|
|
|
|
|
|
api_format_hint: str | None,
|
|
|
|
|
|
path_params: dict[str, Any] | None,
|
|
|
|
|
|
auth_state: _PipelineAuthState,
|
|
|
|
|
|
quiet: bool,
|
|
|
|
|
|
perf_state: _PipelinePerfState,
|
|
|
|
|
|
) -> ApiRequestContext | JSONResponse:
|
|
|
|
|
|
"""Legacy Python context boundary.
|
|
|
|
|
|
|
|
|
|
|
|
Request body reads, FastAPI request.state hydration, and context assembly
|
|
|
|
|
|
still rely on the Python request object and remain in-process for now.
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
raw_body = await self._read_request_body_for_context_legacy(
|
|
|
|
|
|
request,
|
|
|
|
|
|
adapter,
|
|
|
|
|
|
perf_state=perf_state,
|
2026-03-09 22:57:23 +08:00
|
|
|
|
)
|
2026-03-31 19:19:04 +08:00
|
|
|
|
if isinstance(raw_body, JSONResponse):
|
|
|
|
|
|
return raw_body
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
2026-03-31 19:19:04 +08:00
|
|
|
|
resolved_api_format_hint = self._resolve_api_format_hint(adapter, api_format_hint)
|
|
|
|
|
|
context_start = PerfRecorder.start(force=perf_state.sampled)
|
2025-12-10 20:52:44 +08:00
|
|
|
|
context = ApiRequestContext.build(
|
2026-03-31 19:19:04 +08:00
|
|
|
|
request=request,
|
2025-12-10 20:52:44 +08:00
|
|
|
|
db=db,
|
2026-03-31 19:19:04 +08:00
|
|
|
|
user=auth_state.user,
|
|
|
|
|
|
api_key=auth_state.api_key,
|
2025-12-10 20:52:44 +08:00
|
|
|
|
raw_body=raw_body,
|
|
|
|
|
|
mode=mode.value,
|
2026-03-31 19:19:04 +08:00
|
|
|
|
api_format_hint=resolved_api_format_hint,
|
2025-12-10 20:52:44 +08:00
|
|
|
|
path_params=path_params,
|
|
|
|
|
|
)
|
2026-02-05 14:22:11 +08:00
|
|
|
|
context_duration = PerfRecorder.stop(
|
2026-03-31 19:19:04 +08:00
|
|
|
|
context_start,
|
|
|
|
|
|
"pipeline_context_build",
|
|
|
|
|
|
labels=perf_state.labels,
|
2026-02-05 14:22:11 +08:00
|
|
|
|
)
|
2026-03-31 19:19:04 +08:00
|
|
|
|
perf_state.record_ms("context_build_ms", context_duration)
|
|
|
|
|
|
await self._apply_context_runtime_state_legacy(
|
|
|
|
|
|
context,
|
|
|
|
|
|
mode=mode,
|
|
|
|
|
|
auth_state=auth_state,
|
|
|
|
|
|
quiet=quiet,
|
|
|
|
|
|
)
|
|
|
|
|
|
return context
|
|
|
|
|
|
|
|
|
|
|
|
async def _read_request_body_for_context_legacy(
|
|
|
|
|
|
self,
|
|
|
|
|
|
request: Request,
|
|
|
|
|
|
adapter: ApiAdapter,
|
|
|
|
|
|
*,
|
|
|
|
|
|
perf_state: _PipelinePerfState,
|
|
|
|
|
|
) -> bytes | None | JSONResponse:
|
|
|
|
|
|
should_eager_read_body = request.method in {"POST", "PUT", "PATCH"} and getattr(
|
|
|
|
|
|
adapter, "eager_request_body", True
|
|
|
|
|
|
)
|
|
|
|
|
|
if not should_eager_read_body:
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
import asyncio
|
|
|
|
|
|
|
|
|
|
|
|
body_start = PerfRecorder.start(force=perf_state.sampled)
|
|
|
|
|
|
body_size = 0
|
|
|
|
|
|
try:
|
|
|
|
|
|
raw_body = await asyncio.wait_for(request.body(), timeout=config.request_body_timeout)
|
|
|
|
|
|
body_size = len(raw_body) if raw_body is not None else 0
|
|
|
|
|
|
return raw_body
|
|
|
|
|
|
finally:
|
|
|
|
|
|
body_duration = PerfRecorder.stop(
|
|
|
|
|
|
body_start,
|
|
|
|
|
|
"pipeline_body_read",
|
|
|
|
|
|
labels=perf_state.labels,
|
|
|
|
|
|
log_hint=f"size={body_size}",
|
|
|
|
|
|
)
|
|
|
|
|
|
perf_state.record_ms("body_read_ms", body_duration)
|
|
|
|
|
|
if perf_state.sampled:
|
|
|
|
|
|
perf_metrics = getattr(request.state, "perf_metrics", None)
|
|
|
|
|
|
if isinstance(perf_metrics, dict):
|
|
|
|
|
|
perf_metrics.setdefault("pipeline", {})["body_bytes"] = int(body_size)
|
|
|
|
|
|
except TimeoutError:
|
|
|
|
|
|
timeout_sec = int(config.request_body_timeout)
|
|
|
|
|
|
logger.error("读取请求体超时({}s),可能客户端未发送完整请求体", timeout_sec)
|
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
|
status_code=408,
|
|
|
|
|
|
detail=f"Request timeout: body not received within {timeout_sec} seconds",
|
|
|
|
|
|
)
|
|
|
|
|
|
except ClientDisconnect:
|
|
|
|
|
|
logger.warning(
|
|
|
|
|
|
"[Pipeline] 客户端在读取请求体期间断开连接: {} {}",
|
|
|
|
|
|
request.method,
|
|
|
|
|
|
request.url.path,
|
|
|
|
|
|
)
|
|
|
|
|
|
return JSONResponse(
|
|
|
|
|
|
status_code=499,
|
|
|
|
|
|
content={"error": "client_disconnected", "message": "Client closed request"},
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
async def _apply_context_runtime_state_legacy(
|
|
|
|
|
|
self,
|
|
|
|
|
|
context: ApiRequestContext,
|
|
|
|
|
|
*,
|
|
|
|
|
|
mode: ApiMode,
|
|
|
|
|
|
auth_state: _PipelineAuthState,
|
|
|
|
|
|
quiet: bool,
|
|
|
|
|
|
) -> None:
|
|
|
|
|
|
if auth_state.management_token:
|
|
|
|
|
|
context.management_token = auth_state.management_token
|
|
|
|
|
|
context.quiet_logging = quiet
|
|
|
|
|
|
|
|
|
|
|
|
if mode not in {ApiMode.STANDARD, ApiMode.PROXY, ApiMode.USER} or not auth_state.user:
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
if context.prefetched_balance_remaining is not None:
|
|
|
|
|
|
remaining = context.prefetched_balance_remaining
|
|
|
|
|
|
else:
|
|
|
|
|
|
remaining = await self._calculate_balance_remaining_async(
|
|
|
|
|
|
auth_state.user,
|
|
|
|
|
|
api_key=auth_state.api_key,
|
|
|
|
|
|
)
|
|
|
|
|
|
context.balance_remaining = remaining
|
|
|
|
|
|
|
|
|
|
|
|
async def _dispatch_adapter_request_legacy(
|
|
|
|
|
|
self,
|
|
|
|
|
|
adapter: ApiAdapter,
|
|
|
|
|
|
context: ApiRequestContext,
|
|
|
|
|
|
*,
|
|
|
|
|
|
perf_state: _PipelinePerfState,
|
|
|
|
|
|
) -> Any:
|
|
|
|
|
|
"""Legacy Python adapter dispatch/audit boundary."""
|
|
|
|
|
|
|
|
|
|
|
|
authorize_start = PerfRecorder.start(force=perf_state.sampled)
|
2026-02-05 14:22:11 +08:00
|
|
|
|
try:
|
|
|
|
|
|
authorize_result = adapter.authorize(context)
|
|
|
|
|
|
if hasattr(authorize_result, "__await__"):
|
|
|
|
|
|
await authorize_result
|
|
|
|
|
|
finally:
|
|
|
|
|
|
authorize_duration = PerfRecorder.stop(
|
2026-03-31 19:19:04 +08:00
|
|
|
|
authorize_start,
|
|
|
|
|
|
"pipeline_authorize",
|
|
|
|
|
|
labels=perf_state.labels,
|
2026-02-05 14:22:11 +08:00
|
|
|
|
)
|
2026-03-31 19:19:04 +08:00
|
|
|
|
perf_state.record_ms("authorize_ms", authorize_duration)
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
2026-03-31 19:19:04 +08:00
|
|
|
|
handle_start = PerfRecorder.start(force=perf_state.sampled)
|
2025-12-10 20:52:44 +08:00
|
|
|
|
try:
|
|
|
|
|
|
response = await adapter.handle(context)
|
2026-03-31 19:19:04 +08:00
|
|
|
|
handle_duration = PerfRecorder.stop(
|
|
|
|
|
|
handle_start,
|
|
|
|
|
|
"pipeline_handle",
|
|
|
|
|
|
labels=perf_state.labels,
|
|
|
|
|
|
)
|
|
|
|
|
|
perf_state.record_ms("handle_ms", handle_duration)
|
2025-12-10 20:52:44 +08:00
|
|
|
|
status_code = getattr(response, "status_code", None)
|
|
|
|
|
|
self._record_audit_event(context, adapter, success=True, status_code=status_code)
|
|
|
|
|
|
return response
|
|
|
|
|
|
except HTTPException as exc:
|
2026-03-31 19:19:04 +08:00
|
|
|
|
handle_duration = PerfRecorder.stop(
|
|
|
|
|
|
handle_start,
|
|
|
|
|
|
"pipeline_handle",
|
|
|
|
|
|
labels=perf_state.labels,
|
|
|
|
|
|
)
|
|
|
|
|
|
perf_state.record_ms("handle_ms", handle_duration)
|
2025-12-10 20:52:44 +08:00
|
|
|
|
err_detail = exc.detail if isinstance(exc.detail, str) else str(exc.detail)
|
|
|
|
|
|
self._record_audit_event(
|
|
|
|
|
|
context,
|
|
|
|
|
|
adapter,
|
|
|
|
|
|
success=False,
|
|
|
|
|
|
status_code=exc.status_code,
|
|
|
|
|
|
error=err_detail,
|
|
|
|
|
|
)
|
|
|
|
|
|
raise
|
2026-03-09 10:38:14 +08:00
|
|
|
|
except ClientDisconnect:
|
2026-03-31 19:19:04 +08:00
|
|
|
|
handle_duration = PerfRecorder.stop(
|
|
|
|
|
|
handle_start,
|
|
|
|
|
|
"pipeline_handle",
|
|
|
|
|
|
labels=perf_state.labels,
|
|
|
|
|
|
)
|
|
|
|
|
|
perf_state.record_ms("handle_ms", handle_duration)
|
2026-03-09 10:38:14 +08:00
|
|
|
|
logger.warning(
|
|
|
|
|
|
"[Pipeline] 客户端在处理期间断开连接: {} {}",
|
2026-03-31 19:19:04 +08:00
|
|
|
|
context.request_method,
|
|
|
|
|
|
context.request_path,
|
2026-03-09 10:38:14 +08:00
|
|
|
|
)
|
|
|
|
|
|
self._record_audit_event(
|
|
|
|
|
|
context,
|
|
|
|
|
|
adapter,
|
|
|
|
|
|
success=False,
|
|
|
|
|
|
status_code=499,
|
|
|
|
|
|
error="client_disconnected",
|
|
|
|
|
|
)
|
|
|
|
|
|
return JSONResponse(
|
|
|
|
|
|
status_code=499,
|
|
|
|
|
|
content={"error": "client_disconnected", "message": "Client closed request"},
|
|
|
|
|
|
)
|
2025-12-10 20:52:44 +08:00
|
|
|
|
except Exception as exc:
|
2026-03-31 19:19:04 +08:00
|
|
|
|
handle_duration = PerfRecorder.stop(
|
|
|
|
|
|
handle_start,
|
|
|
|
|
|
"pipeline_handle",
|
|
|
|
|
|
labels=perf_state.labels,
|
|
|
|
|
|
)
|
|
|
|
|
|
perf_state.record_ms("handle_ms", handle_duration)
|
2026-03-02 18:02:21 +08:00
|
|
|
|
if isinstance(exc, SQLAlchemyError):
|
|
|
|
|
|
# SQL 执行失败后事务会进入 aborted 状态;先回滚,避免审计写入二次报错。
|
|
|
|
|
|
try:
|
|
|
|
|
|
context.db.rollback()
|
|
|
|
|
|
except Exception as rollback_exc:
|
2026-03-09 22:57:23 +08:00
|
|
|
|
logger.debug("[Pipeline] 回滚失败(可忽略): {}", rollback_exc)
|
2025-12-10 20:52:44 +08:00
|
|
|
|
self._record_audit_event(
|
|
|
|
|
|
context,
|
|
|
|
|
|
adapter,
|
|
|
|
|
|
success=False,
|
|
|
|
|
|
status_code=500,
|
|
|
|
|
|
error=str(exc),
|
|
|
|
|
|
)
|
|
|
|
|
|
raise
|
|
|
|
|
|
|
|
|
|
|
|
# --------------------------------------------------------------------- #
|
|
|
|
|
|
# Internal helpers
|
|
|
|
|
|
# --------------------------------------------------------------------- #
|
|
|
|
|
|
|
2026-03-31 19:19:04 +08:00
|
|
|
|
@staticmethod
|
|
|
|
|
|
def _resolve_api_format_hint(adapter: ApiAdapter, explicit_hint: str | None) -> str | None:
|
|
|
|
|
|
normalized_hint = (explicit_hint or "").strip()
|
|
|
|
|
|
if normalized_hint:
|
|
|
|
|
|
return normalized_hint
|
|
|
|
|
|
|
|
|
|
|
|
allowed_api_formats = getattr(adapter, "allowed_api_formats", None)
|
|
|
|
|
|
if isinstance(allowed_api_formats, (list, tuple)):
|
|
|
|
|
|
for candidate in allowed_api_formats:
|
|
|
|
|
|
if isinstance(candidate, str):
|
|
|
|
|
|
normalized_candidate = candidate.strip()
|
|
|
|
|
|
if normalized_candidate:
|
|
|
|
|
|
return normalized_candidate
|
|
|
|
|
|
|
|
|
|
|
|
adapter_api_format = getattr(adapter, "api_format", None)
|
|
|
|
|
|
if isinstance(adapter_api_format, str):
|
|
|
|
|
|
normalized_adapter_api_format = adapter_api_format.strip()
|
|
|
|
|
|
if normalized_adapter_api_format:
|
|
|
|
|
|
return normalized_adapter_api_format
|
|
|
|
|
|
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
2026-03-15 14:22:59 +08:00
|
|
|
|
async def _check_user_rate_limit(
|
|
|
|
|
|
self,
|
|
|
|
|
|
request: Request,
|
|
|
|
|
|
db: Session,
|
|
|
|
|
|
user: User,
|
|
|
|
|
|
api_key: ApiKey,
|
|
|
|
|
|
) -> None:
|
|
|
|
|
|
limiter = await get_user_rpm_limiter()
|
|
|
|
|
|
system_default_raw = SystemConfigService.get_config(db, SYSTEM_RPM_CONFIG_KEY, default=0)
|
|
|
|
|
|
system_default = max(int(system_default_raw or 0), 0)
|
|
|
|
|
|
|
|
|
|
|
|
if api_key.is_standalone:
|
|
|
|
|
|
effective_user_limit = (
|
|
|
|
|
|
max(int(api_key.rate_limit or 0), 0)
|
|
|
|
|
|
if api_key.rate_limit is not None
|
|
|
|
|
|
else system_default
|
|
|
|
|
|
)
|
|
|
|
|
|
user_rpm_key = limiter.get_standalone_rpm_key(api_key.id)
|
|
|
|
|
|
key_rpm_limit = 0
|
|
|
|
|
|
else:
|
|
|
|
|
|
effective_user_limit = (
|
|
|
|
|
|
max(int(user.rate_limit or 0), 0) if user.rate_limit is not None else system_default
|
|
|
|
|
|
)
|
|
|
|
|
|
user_rpm_key = limiter.get_user_rpm_key(user.id)
|
|
|
|
|
|
key_rpm_limit = max(int(api_key.rate_limit or 0), 0)
|
|
|
|
|
|
|
|
|
|
|
|
result = await limiter.check_and_consume(
|
|
|
|
|
|
user_rpm_key=user_rpm_key,
|
|
|
|
|
|
user_rpm_limit=effective_user_limit,
|
|
|
|
|
|
key_rpm_key=limiter.get_key_rpm_key(api_key.id),
|
|
|
|
|
|
key_rpm_limit=key_rpm_limit,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if result.allowed:
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
scope = result.scope or "user"
|
|
|
|
|
|
limit = result.limit or (effective_user_limit if scope == "user" else key_rpm_limit)
|
|
|
|
|
|
retry_after = result.retry_after or limiter.get_retry_after()
|
|
|
|
|
|
|
|
|
|
|
|
headers = {
|
|
|
|
|
|
"Retry-After": str(retry_after),
|
|
|
|
|
|
"X-RateLimit-Limit": str(limit),
|
|
|
|
|
|
"X-RateLimit-Remaining": "0",
|
|
|
|
|
|
"X-RateLimit-Scope": scope,
|
|
|
|
|
|
}
|
2026-03-31 19:19:04 +08:00
|
|
|
|
update_request_state(request, rate_limit_scope=scope)
|
2026-03-15 14:22:59 +08:00
|
|
|
|
raise HTTPException(status_code=429, detail="请求过于频繁,请稍后重试", headers=headers)
|
|
|
|
|
|
|
2026-03-09 22:57:23 +08:00
|
|
|
|
async def _authenticate_client(
|
2026-02-06 16:37:06 +08:00
|
|
|
|
self, request: Request, db: Session, adapter: ApiAdapter, **_kw: object
|
2026-01-30 03:10:21 +08:00
|
|
|
|
) -> tuple[User, ApiKey]:
|
2026-03-31 19:19:04 +08:00
|
|
|
|
"""Bridge entry for API-key auth.
|
|
|
|
|
|
|
|
|
|
|
|
Trusted gateway headers are the preferred Rust handoff. Once Rust has
|
|
|
|
|
|
locally completed API-key/balance preflight, the gateway strips the raw
|
|
|
|
|
|
provider credential before proxying to Python and this method becomes a
|
|
|
|
|
|
thin ORM reattach shell. Direct client API keys still use the legacy
|
|
|
|
|
|
Python authenticator as the compatibility fallback.
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
trusted_auth = self._authenticate_client_from_gateway_bridge(request, db)
|
2026-03-21 12:57:09 +08:00
|
|
|
|
if trusted_auth is not None:
|
|
|
|
|
|
return trusted_auth
|
|
|
|
|
|
|
2026-03-31 19:19:04 +08:00
|
|
|
|
return await self._authenticate_client_legacy_fallback(request, db, adapter)
|
|
|
|
|
|
|
|
|
|
|
|
def _authenticate_client_from_gateway_bridge(
|
|
|
|
|
|
self,
|
|
|
|
|
|
request: Request,
|
|
|
|
|
|
db: Session,
|
|
|
|
|
|
) -> tuple[User, ApiKey] | None:
|
|
|
|
|
|
"""Rust bridge shell for trusted gateway auth headers."""
|
|
|
|
|
|
|
|
|
|
|
|
return self._try_trusted_gateway_auth(request, db)
|
|
|
|
|
|
|
|
|
|
|
|
async def _authenticate_client_legacy_fallback(
|
|
|
|
|
|
self,
|
|
|
|
|
|
request: Request,
|
|
|
|
|
|
db: Session,
|
|
|
|
|
|
adapter: ApiAdapter,
|
|
|
|
|
|
) -> tuple[User, ApiKey]:
|
|
|
|
|
|
"""Legacy Python fallback for requests without trusted Rust preflight."""
|
|
|
|
|
|
|
2025-12-10 20:52:44 +08:00
|
|
|
|
client_api_key = adapter.extract_api_key(request)
|
|
|
|
|
|
if not client_api_key:
|
|
|
|
|
|
raise HTTPException(status_code=401, detail="请提供API密钥")
|
|
|
|
|
|
|
2026-03-09 22:57:23 +08:00
|
|
|
|
auth_result = await self.auth_service.authenticate_api_key_threadsafe(client_api_key)
|
2025-12-10 20:52:44 +08:00
|
|
|
|
if not auth_result:
|
|
|
|
|
|
raise HTTPException(status_code=401, detail="无效的API密钥")
|
|
|
|
|
|
|
2026-03-09 22:57:23 +08:00
|
|
|
|
user = auth_result.user
|
|
|
|
|
|
api_key = auth_result.api_key
|
2025-12-10 20:52:44 +08:00
|
|
|
|
if not user or not api_key:
|
|
|
|
|
|
raise HTTPException(status_code=401, detail="无效的API密钥")
|
|
|
|
|
|
|
2026-03-31 19:19:04 +08:00
|
|
|
|
return self._bind_authenticated_client(
|
|
|
|
|
|
request,
|
|
|
|
|
|
db,
|
|
|
|
|
|
user_id=getattr(user, "id", None),
|
|
|
|
|
|
api_key_id=getattr(api_key, "id", None),
|
|
|
|
|
|
balance_remaining=auth_result.balance_remaining,
|
|
|
|
|
|
access_allowed=auth_result.access_allowed,
|
|
|
|
|
|
strict=True,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def _bind_authenticated_client(
|
|
|
|
|
|
self,
|
|
|
|
|
|
request: Request,
|
|
|
|
|
|
db: Session,
|
|
|
|
|
|
*,
|
|
|
|
|
|
user_id: str | None,
|
|
|
|
|
|
api_key_id: str | None,
|
|
|
|
|
|
balance_remaining: float | None,
|
|
|
|
|
|
access_allowed: bool,
|
|
|
|
|
|
strict: bool,
|
|
|
|
|
|
) -> tuple[User, ApiKey] | None:
|
|
|
|
|
|
"""Normalize bridge/fallback API-key auth into request-scoped ORM objects.
|
|
|
|
|
|
|
|
|
|
|
|
Trusted gateway auth uses `strict=False` so stale bridge headers can
|
|
|
|
|
|
fall back to the legacy Python path. Direct API-key auth uses
|
|
|
|
|
|
`strict=True` and treats the same invariant checks as terminal.
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
def _fail_or_skip() -> None:
|
|
|
|
|
|
if strict:
|
|
|
|
|
|
raise HTTPException(status_code=401, detail="无效的API密钥")
|
|
|
|
|
|
|
|
|
|
|
|
if not user_id or not api_key_id:
|
|
|
|
|
|
_fail_or_skip()
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
db_user = db.query(User).filter(User.id == user_id).first()
|
|
|
|
|
|
db_api_key = db.query(ApiKey).filter(ApiKey.id == api_key_id).first()
|
2026-03-09 22:57:23 +08:00
|
|
|
|
if not db_user or not db_api_key:
|
2026-03-31 19:19:04 +08:00
|
|
|
|
_fail_or_skip()
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
2026-03-09 22:57:23 +08:00
|
|
|
|
if not db_user.is_active or db_user.is_deleted:
|
2026-03-31 19:19:04 +08:00
|
|
|
|
_fail_or_skip()
|
|
|
|
|
|
return None
|
2026-03-09 22:57:23 +08:00
|
|
|
|
if not db_api_key.is_active:
|
2026-03-31 19:19:04 +08:00
|
|
|
|
_fail_or_skip()
|
|
|
|
|
|
return None
|
2026-03-09 22:57:23 +08:00
|
|
|
|
if db_api_key.is_locked and not db_api_key.is_standalone:
|
|
|
|
|
|
raise HTTPException(status_code=403, detail="该密钥已被管理员锁定,请联系管理员")
|
|
|
|
|
|
if db_api_key.user_id != db_user.id:
|
2026-03-31 19:19:04 +08:00
|
|
|
|
_fail_or_skip()
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
2026-03-09 22:57:23 +08:00
|
|
|
|
if db_api_key.expires_at:
|
|
|
|
|
|
expires_at = db_api_key.expires_at
|
|
|
|
|
|
if expires_at.tzinfo is None:
|
|
|
|
|
|
expires_at = expires_at.replace(tzinfo=timezone.utc)
|
|
|
|
|
|
if expires_at < datetime.now(timezone.utc):
|
2026-03-31 19:19:04 +08:00
|
|
|
|
_fail_or_skip()
|
|
|
|
|
|
return None
|
2026-03-09 22:57:23 +08:00
|
|
|
|
|
2026-03-31 19:19:04 +08:00
|
|
|
|
update_request_state(
|
|
|
|
|
|
request,
|
|
|
|
|
|
user_id=db_user.id,
|
|
|
|
|
|
api_key_id=db_api_key.id,
|
|
|
|
|
|
prefetched_balance_remaining=balance_remaining,
|
|
|
|
|
|
)
|
2026-03-09 22:57:23 +08:00
|
|
|
|
|
2026-03-31 19:19:04 +08:00
|
|
|
|
if not access_allowed:
|
|
|
|
|
|
raise BalanceInsufficientException(balance_type="USD", remaining=balance_remaining)
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
2026-03-09 22:57:23 +08:00
|
|
|
|
return db_user, db_api_key
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
2026-03-21 12:57:09 +08:00
|
|
|
|
def _try_trusted_gateway_auth(
|
|
|
|
|
|
self,
|
|
|
|
|
|
request: Request,
|
|
|
|
|
|
db: Session,
|
|
|
|
|
|
) -> tuple[User, ApiKey] | None:
|
2026-03-31 19:19:04 +08:00
|
|
|
|
"""Rust bridge handoff for API-key auth/balance preflight."""
|
|
|
|
|
|
|
2026-03-21 12:57:09 +08:00
|
|
|
|
if not self._is_trusted_gateway_request(request):
|
|
|
|
|
|
return None
|
refactor: 拆分 Rust gateway/executor 大文件为模块目录结构,拆分 Python gateway.py 为子模块
Rust 侧:
- executor.rs 拆分为 executor/ 目录 (plan_builders, stream, sync, submission)
- 新增 kiro_stream/, local_finalize/, local_stream/ 模块目录
- 新增 video_tasks.rs
- 测试文件 ai_execute/files/video 拆分为子目录
- handlers/headers/control/constants 扩展支持新模块
Python 侧:
- gateway.py 拆分为 24 个子模块 (routes, shared, contract, chat, cli, video, files, finalize, reporting 等)
- 新增 antigravity/gemini_cli/kiro 的 rust_http 适配层
- upstream_fetcher 增加 Rust sidecar 支持
- executor_plan/candidate/pipeline 适配调整
测试:
- 对应拆分 test_internal_gateway_routes 为子目录
- 新增 rust_http 相关测试
2026-03-23 17:19:15 +08:00
|
|
|
|
self._capture_trusted_gateway_execution_path(request)
|
2026-03-21 12:57:09 +08:00
|
|
|
|
|
|
|
|
|
|
user_id = str(request.headers.get(TRUSTED_AUTH_USER_ID_HEADER) or "").strip()
|
|
|
|
|
|
api_key_id = str(request.headers.get(TRUSTED_AUTH_API_KEY_ID_HEADER) or "").strip()
|
|
|
|
|
|
if not user_id or not api_key_id:
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
balance_remaining = self._parse_trusted_balance_remaining(
|
|
|
|
|
|
request.headers.get(TRUSTED_AUTH_BALANCE_HEADER)
|
|
|
|
|
|
)
|
|
|
|
|
|
access_allowed = self._parse_trusted_bool_header(
|
|
|
|
|
|
request.headers.get(TRUSTED_AUTH_ACCESS_ALLOWED_HEADER),
|
|
|
|
|
|
default=True,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-03-31 19:19:04 +08:00
|
|
|
|
return self._bind_authenticated_client(
|
|
|
|
|
|
request,
|
|
|
|
|
|
db,
|
|
|
|
|
|
user_id=user_id,
|
|
|
|
|
|
api_key_id=api_key_id,
|
|
|
|
|
|
balance_remaining=balance_remaining,
|
|
|
|
|
|
access_allowed=access_allowed,
|
|
|
|
|
|
strict=False,
|
|
|
|
|
|
)
|
2026-03-21 12:57:09 +08:00
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
def _is_trusted_gateway_request(request: Request) -> bool:
|
|
|
|
|
|
gateway_marker = str(request.headers.get(TRUSTED_GATEWAY_HEADER) or "").strip().lower()
|
|
|
|
|
|
if not gateway_marker.startswith("rust-phase3"):
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
host = request.client.host if request.client else ""
|
|
|
|
|
|
try:
|
|
|
|
|
|
return ipaddress.ip_address(host).is_loopback
|
|
|
|
|
|
except ValueError:
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
def _parse_trusted_balance_remaining(value: str | None) -> float | None:
|
|
|
|
|
|
raw = str(value or "").strip()
|
|
|
|
|
|
if not raw:
|
|
|
|
|
|
return None
|
|
|
|
|
|
try:
|
|
|
|
|
|
return float(raw)
|
|
|
|
|
|
except ValueError:
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
def _parse_trusted_bool_header(value: str | None, *, default: bool) -> bool:
|
|
|
|
|
|
raw = str(value or "").strip().lower()
|
|
|
|
|
|
if raw in {"1", "true", "yes", "on"}:
|
|
|
|
|
|
return True
|
|
|
|
|
|
if raw in {"0", "false", "no", "off"}:
|
|
|
|
|
|
return False
|
|
|
|
|
|
return default
|
|
|
|
|
|
|
refactor: 拆分 Rust gateway/executor 大文件为模块目录结构,拆分 Python gateway.py 为子模块
Rust 侧:
- executor.rs 拆分为 executor/ 目录 (plan_builders, stream, sync, submission)
- 新增 kiro_stream/, local_finalize/, local_stream/ 模块目录
- 新增 video_tasks.rs
- 测试文件 ai_execute/files/video 拆分为子目录
- handlers/headers/control/constants 扩展支持新模块
Python 侧:
- gateway.py 拆分为 24 个子模块 (routes, shared, contract, chat, cli, video, files, finalize, reporting 等)
- 新增 antigravity/gemini_cli/kiro 的 rust_http 适配层
- upstream_fetcher 增加 Rust sidecar 支持
- executor_plan/candidate/pipeline 适配调整
测试:
- 对应拆分 test_internal_gateway_routes 为子目录
- 新增 rust_http 相关测试
2026-03-23 17:19:15 +08:00
|
|
|
|
def _capture_trusted_gateway_execution_path(self, request: Request) -> str | None:
|
|
|
|
|
|
if not self._is_trusted_gateway_request(request):
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
execution_path = str(
|
|
|
|
|
|
request.headers.get(TRUSTED_GATEWAY_EXECUTION_PATH_HEADER) or ""
|
|
|
|
|
|
).strip()
|
|
|
|
|
|
if not execution_path:
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
2026-03-31 19:19:04 +08:00
|
|
|
|
update_request_state(request, gateway_execution_path=execution_path)
|
refactor: 拆分 Rust gateway/executor 大文件为模块目录结构,拆分 Python gateway.py 为子模块
Rust 侧:
- executor.rs 拆分为 executor/ 目录 (plan_builders, stream, sync, submission)
- 新增 kiro_stream/, local_finalize/, local_stream/ 模块目录
- 新增 video_tasks.rs
- 测试文件 ai_execute/files/video 拆分为子目录
- handlers/headers/control/constants 扩展支持新模块
Python 侧:
- gateway.py 拆分为 24 个子模块 (routes, shared, contract, chat, cli, video, files, finalize, reporting 等)
- 新增 antigravity/gemini_cli/kiro 的 rust_http 适配层
- upstream_fetcher 增加 Rust sidecar 支持
- executor_plan/candidate/pipeline 适配调整
测试:
- 对应拆分 test_internal_gateway_routes 为子目录
- 新增 rust_http 相关测试
2026-03-23 17:19:15 +08:00
|
|
|
|
return execution_path
|
|
|
|
|
|
|
2026-03-31 19:19:04 +08:00
|
|
|
|
def _trusted_gateway_completed_rate_limit_preflight(self, request: Request) -> bool:
|
|
|
|
|
|
if not self._is_trusted_gateway_request(request):
|
|
|
|
|
|
return False
|
|
|
|
|
|
return self._parse_trusted_bool_header(
|
|
|
|
|
|
request.headers.get(TRUSTED_RATE_LIMIT_PREFLIGHT_HEADER),
|
|
|
|
|
|
default=False,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-02-19 21:26:18 +08:00
|
|
|
|
async def _try_token_prefix_auth(
|
|
|
|
|
|
self, token: str, request: Request, db: Session
|
|
|
|
|
|
) -> tuple[User, Any] | None:
|
|
|
|
|
|
"""尝试通过模块注册的 token 前缀认证器认证
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
(User, token_record) 元组,或 None(无前缀匹配)
|
|
|
|
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
|
|
HTTPException: 前缀匹配但认证失败时抛出 401
|
|
|
|
|
|
"""
|
|
|
|
|
|
from src.core.modules.hooks import AUTH_TOKEN_PREFIX_AUTHENTICATORS, get_hook_dispatcher
|
|
|
|
|
|
|
2026-03-09 22:57:23 +08:00
|
|
|
|
authenticators = await get_hook_dispatcher().dispatch(AUTH_TOKEN_PREFIX_AUTHENTICATORS)
|
2026-02-19 21:26:18 +08:00
|
|
|
|
for auth_info in authenticators or []:
|
|
|
|
|
|
prefix = auth_info.get("prefix", "")
|
|
|
|
|
|
authenticate_fn = auth_info.get("authenticate")
|
|
|
|
|
|
if prefix and token.startswith(prefix):
|
|
|
|
|
|
if not authenticate_fn:
|
|
|
|
|
|
logger.warning("Token prefix '{}' has no authenticate callback", prefix)
|
|
|
|
|
|
raise HTTPException(status_code=401, detail="认证服务不可用")
|
2026-03-31 19:19:04 +08:00
|
|
|
|
identity = get_request_identity_metadata(request)
|
2026-03-09 22:57:23 +08:00
|
|
|
|
auth_db = create_session()
|
|
|
|
|
|
try:
|
2026-03-31 19:19:04 +08:00
|
|
|
|
result = await authenticate_fn(auth_db, token, identity.client_ip)
|
2026-03-09 22:57:23 +08:00
|
|
|
|
if result:
|
|
|
|
|
|
for instance in result:
|
|
|
|
|
|
if instance is None:
|
|
|
|
|
|
continue
|
|
|
|
|
|
try:
|
|
|
|
|
|
auth_db.expunge(instance)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
return result
|
|
|
|
|
|
finally:
|
|
|
|
|
|
auth_db.close()
|
2026-02-19 21:26:18 +08:00
|
|
|
|
# 前缀匹配但认证失败
|
|
|
|
|
|
module_name = auth_info.get("module", "unknown")
|
|
|
|
|
|
raise HTTPException(status_code=401, detail=f"无效或过期的 Token ({module_name})")
|
|
|
|
|
|
return None # 无前缀匹配
|
|
|
|
|
|
|
2026-03-09 22:57:23 +08:00
|
|
|
|
def _reattach_token_auth_result(
|
|
|
|
|
|
self,
|
|
|
|
|
|
db: Session,
|
|
|
|
|
|
token_auth_result: tuple[User, Any],
|
|
|
|
|
|
) -> tuple[User, Any]:
|
|
|
|
|
|
"""将前缀认证返回对象绑定到当前请求会话,避免后续写入失效。"""
|
|
|
|
|
|
user, management_token = token_auth_result
|
|
|
|
|
|
|
|
|
|
|
|
db_user = db.query(User).filter(User.id == user.id).first()
|
|
|
|
|
|
if not db_user:
|
|
|
|
|
|
raise HTTPException(status_code=401, detail="无效或过期的 Token")
|
|
|
|
|
|
|
|
|
|
|
|
if management_token is None:
|
|
|
|
|
|
return db_user, None
|
|
|
|
|
|
|
|
|
|
|
|
token_id = getattr(management_token, "id", None)
|
|
|
|
|
|
token_model: Any = type(management_token)
|
|
|
|
|
|
if token_id is None or not hasattr(token_model, "id"):
|
|
|
|
|
|
return db_user, management_token
|
|
|
|
|
|
|
|
|
|
|
|
db_management_token = db.query(token_model).filter(token_model.id == token_id).first()
|
|
|
|
|
|
if not db_management_token:
|
|
|
|
|
|
raise HTTPException(status_code=401, detail="无效或过期的 Token")
|
|
|
|
|
|
return db_user, db_management_token
|
|
|
|
|
|
|
2026-01-07 14:55:07 +08:00
|
|
|
|
async def _authenticate_admin(
|
|
|
|
|
|
self, request: Request, db: Session
|
2026-01-30 03:10:21 +08:00
|
|
|
|
) -> tuple[User, ManagementToken | None]:
|
2026-03-31 19:19:04 +08:00
|
|
|
|
"""Legacy Python admin auth; supports JWT and Management Token."""
|
2025-12-10 20:52:44 +08:00
|
|
|
|
authorization = request.headers.get("authorization")
|
|
|
|
|
|
if not authorization or not authorization.lower().startswith("bearer "):
|
|
|
|
|
|
raise HTTPException(status_code=401, detail="缺少管理员凭证")
|
|
|
|
|
|
|
2025-12-18 19:07:20 +08:00
|
|
|
|
token = authorization[7:].strip()
|
2026-01-07 14:55:07 +08:00
|
|
|
|
|
2026-02-19 21:26:18 +08:00
|
|
|
|
token_auth_result = await self._try_token_prefix_auth(token, request, db)
|
|
|
|
|
|
if token_auth_result is not None:
|
2026-03-09 22:57:23 +08:00
|
|
|
|
user, management_token = self._reattach_token_auth_result(db, token_auth_result)
|
2026-01-07 14:55:07 +08:00
|
|
|
|
|
|
|
|
|
|
if user.role != UserRole.ADMIN:
|
2026-03-09 22:57:23 +08:00
|
|
|
|
logger.warning("非管理员尝试通过 Management Token 访问管理端点: {}", user.email)
|
2026-01-07 14:55:07 +08:00
|
|
|
|
raise HTTPException(status_code=403, detail="需要管理员权限")
|
|
|
|
|
|
|
2026-03-31 19:19:04 +08:00
|
|
|
|
update_request_state(
|
|
|
|
|
|
request,
|
|
|
|
|
|
user_id=user.id,
|
|
|
|
|
|
management_token_id=management_token.id if management_token else None,
|
|
|
|
|
|
user_session_id=None,
|
|
|
|
|
|
)
|
2026-01-07 14:55:07 +08:00
|
|
|
|
return user, management_token
|
|
|
|
|
|
|
2025-12-10 20:52:44 +08:00
|
|
|
|
try:
|
2025-12-18 01:09:22 +08:00
|
|
|
|
payload = await self.auth_service.verify_token(token, token_type="access")
|
2025-12-10 20:52:44 +08:00
|
|
|
|
except HTTPException:
|
|
|
|
|
|
raise
|
|
|
|
|
|
except Exception as exc:
|
2026-03-09 22:57:23 +08:00
|
|
|
|
logger.error("Admin token 验证失败: {}", exc)
|
2025-12-10 20:52:44 +08:00
|
|
|
|
raise HTTPException(status_code=401, detail="无效的管理员令牌")
|
|
|
|
|
|
|
|
|
|
|
|
user_id = payload.get("user_id")
|
2026-03-17 16:34:09 +08:00
|
|
|
|
session_id = payload.get("session_id")
|
2025-12-10 20:52:44 +08:00
|
|
|
|
if not user_id:
|
|
|
|
|
|
raise HTTPException(status_code=401, detail="无效的管理员令牌")
|
2026-03-17 16:34:09 +08:00
|
|
|
|
if not session_id:
|
|
|
|
|
|
raise HTTPException(status_code=401, detail="登录会话已失效,请重新登录")
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
2026-03-09 22:57:23 +08:00
|
|
|
|
db_user = db.query(User).filter(User.id == user_id).first()
|
|
|
|
|
|
if not db_user or not db_user.is_active or db_user.is_deleted:
|
2025-12-10 20:52:44 +08:00
|
|
|
|
raise HTTPException(status_code=403, detail="用户不存在或已禁用")
|
|
|
|
|
|
|
2026-03-09 22:57:23 +08:00
|
|
|
|
if not self.auth_service.token_identity_matches_user(payload, db_user):
|
2026-01-19 03:19:17 +08:00
|
|
|
|
raise HTTPException(status_code=403, detail="无效的管理员令牌")
|
|
|
|
|
|
|
2026-03-09 22:57:23 +08:00
|
|
|
|
if db_user.role != UserRole.ADMIN:
|
|
|
|
|
|
logger.warning("非管理员尝试通过 JWT 访问管理端点: {}", db_user.email)
|
2026-01-07 14:55:07 +08:00
|
|
|
|
raise HTTPException(status_code=403, detail="需要管理员权限")
|
|
|
|
|
|
|
2026-03-31 19:19:04 +08:00
|
|
|
|
session = self._touch_authenticated_session(
|
|
|
|
|
|
request,
|
|
|
|
|
|
db,
|
|
|
|
|
|
user_id=str(user_id),
|
|
|
|
|
|
session_id=str(session_id),
|
|
|
|
|
|
scope="admin",
|
|
|
|
|
|
)
|
|
|
|
|
|
update_request_state(
|
|
|
|
|
|
request,
|
|
|
|
|
|
user_session_id=session.id,
|
|
|
|
|
|
user_id=db_user.id,
|
2026-03-17 16:34:09 +08:00
|
|
|
|
)
|
2026-03-09 22:57:23 +08:00
|
|
|
|
return db_user, None
|
2026-01-07 14:55:07 +08:00
|
|
|
|
|
|
|
|
|
|
async def _authenticate_user(
|
|
|
|
|
|
self, request: Request, db: Session
|
2026-01-30 03:10:21 +08:00
|
|
|
|
) -> tuple[User, ManagementToken | None]:
|
2026-03-31 19:19:04 +08:00
|
|
|
|
"""Legacy Python user auth; supports JWT and Management Token."""
|
2025-12-10 20:52:44 +08:00
|
|
|
|
authorization = request.headers.get("authorization")
|
|
|
|
|
|
if not authorization or not authorization.lower().startswith("bearer "):
|
|
|
|
|
|
raise HTTPException(status_code=401, detail="缺少用户凭证")
|
|
|
|
|
|
|
2025-12-18 19:07:20 +08:00
|
|
|
|
token = authorization[7:].strip()
|
2026-01-07 14:55:07 +08:00
|
|
|
|
|
2026-02-19 21:26:18 +08:00
|
|
|
|
token_auth_result = await self._try_token_prefix_auth(token, request, db)
|
|
|
|
|
|
if token_auth_result is not None:
|
2026-03-09 22:57:23 +08:00
|
|
|
|
user, management_token = self._reattach_token_auth_result(db, token_auth_result)
|
2026-03-31 19:19:04 +08:00
|
|
|
|
update_request_state(
|
|
|
|
|
|
request,
|
|
|
|
|
|
user_id=user.id,
|
|
|
|
|
|
management_token_id=management_token.id if management_token else None,
|
|
|
|
|
|
user_session_id=None,
|
|
|
|
|
|
)
|
2026-01-07 14:55:07 +08:00
|
|
|
|
return user, management_token
|
|
|
|
|
|
|
2025-12-10 20:52:44 +08:00
|
|
|
|
try:
|
2025-12-18 01:09:22 +08:00
|
|
|
|
payload = await self.auth_service.verify_token(token, token_type="access")
|
2025-12-10 20:52:44 +08:00
|
|
|
|
except HTTPException:
|
|
|
|
|
|
raise
|
|
|
|
|
|
except Exception as exc:
|
2026-03-09 22:57:23 +08:00
|
|
|
|
logger.error("User token 验证失败: {}", exc)
|
2025-12-10 20:52:44 +08:00
|
|
|
|
raise HTTPException(status_code=401, detail="无效的用户令牌")
|
|
|
|
|
|
|
|
|
|
|
|
user_id = payload.get("user_id")
|
2026-03-17 16:34:09 +08:00
|
|
|
|
session_id = payload.get("session_id")
|
2025-12-10 20:52:44 +08:00
|
|
|
|
if not user_id:
|
|
|
|
|
|
raise HTTPException(status_code=401, detail="无效的用户令牌")
|
2026-03-17 16:34:09 +08:00
|
|
|
|
if not session_id:
|
|
|
|
|
|
raise HTTPException(status_code=401, detail="登录会话已失效,请重新登录")
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
2026-03-09 22:57:23 +08:00
|
|
|
|
db_user = db.query(User).filter(User.id == user_id).first()
|
|
|
|
|
|
if not db_user or not db_user.is_active or db_user.is_deleted:
|
2025-12-10 20:52:44 +08:00
|
|
|
|
raise HTTPException(status_code=403, detail="用户不存在或已禁用")
|
|
|
|
|
|
|
2026-03-09 22:57:23 +08:00
|
|
|
|
if not self.auth_service.token_identity_matches_user(payload, db_user):
|
2026-01-19 03:19:17 +08:00
|
|
|
|
raise HTTPException(status_code=403, detail="无效的用户令牌")
|
|
|
|
|
|
|
2026-03-31 19:19:04 +08:00
|
|
|
|
session = self._touch_authenticated_session(
|
|
|
|
|
|
request,
|
|
|
|
|
|
db,
|
|
|
|
|
|
user_id=str(user_id),
|
|
|
|
|
|
session_id=str(session_id),
|
|
|
|
|
|
scope="user",
|
|
|
|
|
|
)
|
|
|
|
|
|
update_request_state(
|
|
|
|
|
|
request,
|
|
|
|
|
|
user_session_id=session.id,
|
|
|
|
|
|
user_id=db_user.id,
|
2026-03-17 16:34:09 +08:00
|
|
|
|
)
|
2026-03-09 22:57:23 +08:00
|
|
|
|
return db_user, None
|
2026-01-07 14:55:07 +08:00
|
|
|
|
|
|
|
|
|
|
async def _authenticate_management(
|
|
|
|
|
|
self, request: Request, db: Session
|
2026-01-30 03:10:21 +08:00
|
|
|
|
) -> tuple[User, ManagementToken]:
|
2026-03-31 19:19:04 +08:00
|
|
|
|
"""Legacy Python Management Token 认证。"""
|
2026-01-07 14:55:07 +08:00
|
|
|
|
authorization = request.headers.get("authorization")
|
|
|
|
|
|
if not authorization or not authorization.lower().startswith("bearer "):
|
|
|
|
|
|
raise HTTPException(status_code=401, detail="缺少 Management Token")
|
|
|
|
|
|
|
|
|
|
|
|
token = authorization[7:].strip()
|
|
|
|
|
|
|
2026-02-19 21:26:18 +08:00
|
|
|
|
# 通过钩子检查是否匹配模块注册的 token 前缀
|
|
|
|
|
|
# _try_token_prefix_auth 会在前缀匹配但认证失败时直接抛 HTTPException
|
|
|
|
|
|
token_auth_result = await self._try_token_prefix_auth(token, request, db)
|
|
|
|
|
|
if token_auth_result is not None:
|
2026-03-09 22:57:23 +08:00
|
|
|
|
user, management_token = self._reattach_token_auth_result(db, token_auth_result)
|
2026-01-07 14:55:07 +08:00
|
|
|
|
|
2026-03-31 19:19:04 +08:00
|
|
|
|
update_request_state(
|
|
|
|
|
|
request,
|
|
|
|
|
|
user_id=user.id,
|
|
|
|
|
|
management_token_id=management_token.id if management_token else None,
|
|
|
|
|
|
user_session_id=None,
|
|
|
|
|
|
)
|
2026-01-07 14:55:07 +08:00
|
|
|
|
|
2026-02-19 21:26:18 +08:00
|
|
|
|
return user, management_token
|
2026-01-07 14:55:07 +08:00
|
|
|
|
|
2026-02-19 21:26:18 +08:00
|
|
|
|
raise HTTPException(
|
|
|
|
|
|
status_code=401,
|
|
|
|
|
|
detail="无效的 Token 格式,需要 Management Token",
|
|
|
|
|
|
)
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
2026-03-09 22:57:23 +08:00
|
|
|
|
async def _calculate_balance_remaining_async(
|
|
|
|
|
|
self, user: User | None, api_key: ApiKey | None = None
|
2026-03-08 00:05:48 +08:00
|
|
|
|
) -> float | None:
|
2025-12-10 20:52:44 +08:00
|
|
|
|
if not user:
|
|
|
|
|
|
return None
|
2026-03-09 22:57:23 +08:00
|
|
|
|
|
|
|
|
|
|
user_id = getattr(user, "id", None)
|
|
|
|
|
|
api_key_id = getattr(api_key, "id", None)
|
|
|
|
|
|
|
|
|
|
|
|
# API Key 链路通常已在认证阶段预取余额;这里只保留为无预取路径的兜底查询。
|
|
|
|
|
|
def _load_balance() -> float | None:
|
|
|
|
|
|
thread_db = create_session()
|
|
|
|
|
|
try:
|
|
|
|
|
|
db_user = (
|
|
|
|
|
|
thread_db.query(User).filter(User.id == user_id).first() if user_id else None
|
|
|
|
|
|
)
|
|
|
|
|
|
db_api_key = (
|
|
|
|
|
|
thread_db.query(ApiKey).filter(ApiKey.id == api_key_id).first()
|
|
|
|
|
|
if api_key_id
|
|
|
|
|
|
else None
|
|
|
|
|
|
)
|
|
|
|
|
|
balance = WalletService.get_balance_snapshot(
|
|
|
|
|
|
thread_db,
|
|
|
|
|
|
user=db_user,
|
|
|
|
|
|
api_key=db_api_key,
|
|
|
|
|
|
)
|
|
|
|
|
|
return float(balance) if balance is not None else None
|
|
|
|
|
|
finally:
|
|
|
|
|
|
thread_db.close()
|
|
|
|
|
|
|
|
|
|
|
|
return await run_in_threadpool(_load_balance)
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
def _record_audit_event(
|
|
|
|
|
|
self,
|
|
|
|
|
|
context: ApiRequestContext,
|
|
|
|
|
|
adapter: ApiAdapter,
|
|
|
|
|
|
*,
|
|
|
|
|
|
success: bool,
|
2026-01-30 03:10:21 +08:00
|
|
|
|
status_code: int | None = None,
|
|
|
|
|
|
error: str | None = None,
|
2025-12-10 20:52:44 +08:00
|
|
|
|
) -> None:
|
2026-03-31 19:19:04 +08:00
|
|
|
|
"""Legacy Python request-scoped audit writer.
|
2025-12-18 01:59:40 +08:00
|
|
|
|
|
2026-03-17 16:34:09 +08:00
|
|
|
|
事务策略:
|
|
|
|
|
|
- 默认复用请求级 Session,由中间件在请求结束时统一提交。
|
|
|
|
|
|
- 若路由已显式提交主事务(tx_committed_by_route=True),则审计日志会落在新的事务中,
|
|
|
|
|
|
这里需要立即提交,否则中间件会跳过二次提交,导致审计记录丢失。
|
2025-12-18 01:59:40 +08:00
|
|
|
|
"""
|
2025-12-10 20:52:44 +08:00
|
|
|
|
if not getattr(adapter, "audit_log_enabled", True):
|
|
|
|
|
|
return
|
|
|
|
|
|
|
2025-12-18 01:59:40 +08:00
|
|
|
|
if context.db is None:
|
2025-12-10 20:52:44 +08:00
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
event_type = adapter.audit_success_event if success else adapter.audit_failure_event
|
|
|
|
|
|
if not event_type:
|
|
|
|
|
|
if not success and status_code == 401:
|
|
|
|
|
|
event_type = AuditEventType.UNAUTHORIZED_ACCESS
|
|
|
|
|
|
else:
|
|
|
|
|
|
event_type = (
|
|
|
|
|
|
AuditEventType.REQUEST_SUCCESS if success else AuditEventType.REQUEST_FAILED
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
metadata = self._build_audit_metadata(
|
|
|
|
|
|
context=context,
|
|
|
|
|
|
adapter=adapter,
|
|
|
|
|
|
success=success,
|
|
|
|
|
|
status_code=status_code,
|
|
|
|
|
|
error=error,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-03-31 19:19:04 +08:00
|
|
|
|
context.sync_runtime_state_from_request()
|
2026-03-17 16:34:09 +08:00
|
|
|
|
|
2025-12-10 20:52:44 +08:00
|
|
|
|
try:
|
2025-12-18 01:59:40 +08:00
|
|
|
|
# 复用请求级 Session,不创建新的连接
|
|
|
|
|
|
# 审计记录随主事务一起提交,由中间件统一管理
|
2025-12-10 20:52:44 +08:00
|
|
|
|
self.audit_service.log_event(
|
2025-12-18 01:59:40 +08:00
|
|
|
|
db=context.db,
|
2025-12-10 20:52:44 +08:00
|
|
|
|
event_type=event_type,
|
2026-03-31 19:19:04 +08:00
|
|
|
|
description=f"{context.request_method} {context.request_path} via {adapter.name}",
|
2025-12-10 20:52:44 +08:00
|
|
|
|
user_id=context.user.id if context.user else None,
|
|
|
|
|
|
api_key_id=context.api_key.id if context.api_key else None,
|
|
|
|
|
|
ip_address=context.client_ip,
|
|
|
|
|
|
user_agent=context.user_agent,
|
|
|
|
|
|
request_id=context.request_id,
|
|
|
|
|
|
status_code=status_code,
|
|
|
|
|
|
error_message=error,
|
|
|
|
|
|
metadata=metadata,
|
|
|
|
|
|
)
|
2026-03-31 19:19:04 +08:00
|
|
|
|
if context.tx_committed_by_route:
|
2026-03-17 16:34:09 +08:00
|
|
|
|
try:
|
|
|
|
|
|
context.db.commit()
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
context.db.rollback()
|
|
|
|
|
|
raise
|
2025-12-10 20:52:44 +08:00
|
|
|
|
except Exception as exc:
|
2025-12-18 01:59:40 +08:00
|
|
|
|
# 审计失败不应影响主请求,仅记录警告
|
2026-03-09 22:57:23 +08:00
|
|
|
|
logger.warning("[Audit] Failed to record event for adapter={}: {}", adapter.name, exc)
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
def _build_audit_metadata(
|
|
|
|
|
|
self,
|
|
|
|
|
|
context: ApiRequestContext,
|
|
|
|
|
|
adapter: ApiAdapter,
|
|
|
|
|
|
*,
|
|
|
|
|
|
success: bool,
|
2026-01-30 03:10:21 +08:00
|
|
|
|
status_code: int | None,
|
|
|
|
|
|
error: str | None,
|
2025-12-10 20:52:44 +08:00
|
|
|
|
) -> dict:
|
|
|
|
|
|
duration_ms = max((time.time() - context.start_time) * 1000, 0.0)
|
|
|
|
|
|
metadata: dict[str, Any] = {
|
2026-03-31 19:19:04 +08:00
|
|
|
|
"path": context.request_path,
|
|
|
|
|
|
"path_params": dict(context.path_params or {}),
|
|
|
|
|
|
"method": context.request_method,
|
2025-12-10 20:52:44 +08:00
|
|
|
|
"adapter": adapter.name,
|
|
|
|
|
|
"adapter_class": adapter.__class__.__name__,
|
|
|
|
|
|
"adapter_mode": getattr(adapter.mode, "value", str(adapter.mode)),
|
|
|
|
|
|
"mode": context.mode,
|
|
|
|
|
|
"api_format_hint": context.api_format_hint,
|
|
|
|
|
|
"query": context.query_params,
|
|
|
|
|
|
"duration_ms": round(duration_ms, 2),
|
|
|
|
|
|
"request_body_bytes": len(context.raw_body or b""),
|
|
|
|
|
|
"has_body": bool(context.raw_body),
|
2026-03-31 19:19:04 +08:00
|
|
|
|
"request_content_type": context.request_content_type,
|
2026-03-08 00:05:48 +08:00
|
|
|
|
"balance_remaining": context.balance_remaining,
|
2025-12-10 20:52:44 +08:00
|
|
|
|
"success": success,
|
2026-01-28 23:07:58 +08:00
|
|
|
|
# 传递 quiet_logging 标志给审计服务,用于抑制高频轮询日志
|
|
|
|
|
|
"quiet_logging": getattr(context, "quiet_logging", False),
|
2025-12-10 20:52:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
if status_code is not None:
|
|
|
|
|
|
metadata["status_code"] = status_code
|
2026-03-31 19:19:04 +08:00
|
|
|
|
if context.gateway_execution_path:
|
|
|
|
|
|
metadata["gateway_execution_path"] = context.gateway_execution_path
|
|
|
|
|
|
if context.rate_limit_scope:
|
|
|
|
|
|
metadata["rate_limit_scope"] = context.rate_limit_scope
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
if context.user and getattr(context.user, "role", None):
|
|
|
|
|
|
role = context.user.role
|
|
|
|
|
|
metadata["user_role"] = getattr(role, "value", role)
|
|
|
|
|
|
|
|
|
|
|
|
if context.api_key:
|
|
|
|
|
|
if getattr(context.api_key, "name", None):
|
|
|
|
|
|
metadata["api_key_name"] = context.api_key.name
|
|
|
|
|
|
# 使用脱敏后的密钥显示
|
|
|
|
|
|
if hasattr(context.api_key, "get_display_key"):
|
|
|
|
|
|
metadata["api_key_display"] = context.api_key.get_display_key()
|
|
|
|
|
|
|
|
|
|
|
|
extra_details: dict[str, Any] = {}
|
|
|
|
|
|
if context.audit_metadata:
|
|
|
|
|
|
extra_details.update(context.audit_metadata)
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
adapter_details = adapter.get_audit_metadata(
|
|
|
|
|
|
context,
|
|
|
|
|
|
success=success,
|
|
|
|
|
|
status_code=status_code,
|
|
|
|
|
|
error=error,
|
|
|
|
|
|
)
|
|
|
|
|
|
if adapter_details:
|
|
|
|
|
|
extra_details.update(adapter_details)
|
|
|
|
|
|
except Exception as exc:
|
2026-03-09 22:57:23 +08:00
|
|
|
|
logger.warning(
|
|
|
|
|
|
"[Audit] Adapter metadata failed: {}: {}", adapter.__class__.__name__, exc
|
|
|
|
|
|
)
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
if extra_details:
|
|
|
|
|
|
metadata["details"] = extra_details
|
|
|
|
|
|
|
|
|
|
|
|
if error:
|
|
|
|
|
|
metadata["error"] = error
|
|
|
|
|
|
|
|
|
|
|
|
return self._sanitize_metadata(metadata)
|
|
|
|
|
|
|
2026-03-09 22:57:23 +08:00
|
|
|
|
def _sanitize_metadata(self, value: Any, depth: int = 0) -> Any:
|
2025-12-10 20:52:44 +08:00
|
|
|
|
if value is None:
|
|
|
|
|
|
return None
|
|
|
|
|
|
if depth > 5:
|
|
|
|
|
|
return str(value)
|
|
|
|
|
|
if isinstance(value, (str, int, float, bool)):
|
|
|
|
|
|
return value
|
|
|
|
|
|
if isinstance(value, Enum):
|
|
|
|
|
|
return value.value
|
|
|
|
|
|
if isinstance(value, dict):
|
|
|
|
|
|
sanitized = {}
|
|
|
|
|
|
for key, val in value.items():
|
|
|
|
|
|
cleaned = self._sanitize_metadata(val, depth + 1)
|
|
|
|
|
|
if cleaned is not None:
|
|
|
|
|
|
sanitized[str(key)] = cleaned
|
|
|
|
|
|
return sanitized
|
|
|
|
|
|
if isinstance(value, (list, tuple, set)):
|
|
|
|
|
|
return [self._sanitize_metadata(item, depth + 1) for item in value]
|
|
|
|
|
|
if hasattr(value, "isoformat"):
|
|
|
|
|
|
try:
|
|
|
|
|
|
return value.isoformat()
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
return str(value)
|
|
|
|
|
|
return str(value)
|
2026-03-14 11:59:07 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_shared_pipeline = ApiRequestPipeline()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_pipeline() -> ApiRequestPipeline:
|
|
|
|
|
|
"""返回全局共享的无状态请求管道实例。"""
|
|
|
|
|
|
return _shared_pipeline
|