mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 09:20:22 +08:00
- 删除全部 Python 源码 (src/) 及 Alembic 迁移脚本,归档至 _deprecated_py_src/ - 重构 Rust gateway ai_pipeline: 拆分 planner/finalize 模块,新增 contracts/adaptation 层 - 重组 handlers 模块为 admin/public/proxy/internal/shared 子模块结构 - 新增 executor 模块,引入 Rust 原生数据库迁移 (aether-data/migrations) - 简化 CI/Docker 构建流程,移除 base image 二级构建,统一为单一 app image - 移除 Python 相关基础设施文件 (entrypoint.sh, gunicorn_conf.py, Dockerfile.base)
387 lines
13 KiB
Python
387 lines
13 KiB
Python
from __future__ import annotations
|
|
|
|
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Request
|
|
from fastapi.responses import JSONResponse, Response
|
|
from sqlalchemy.orm import Session
|
|
|
|
from src.database import get_db
|
|
|
|
from . import gateway as gateway_impl
|
|
from .gateway_contract import (
|
|
CONTROL_EXECUTED_HEADER,
|
|
GatewayAuthContextRequest,
|
|
GatewayExecuteRequest,
|
|
GatewayResolveRequest,
|
|
GatewayStreamReportRequest,
|
|
GatewaySyncReportRequest,
|
|
classify_gateway_route,
|
|
)
|
|
|
|
router = APIRouter(
|
|
prefix="/api/internal/gateway",
|
|
tags=["Internal - Gateway"],
|
|
include_in_schema=False,
|
|
)
|
|
|
|
|
|
def _ensure_legacy_internal_gateway_request(request: Request) -> Response | None:
|
|
if gateway_impl._request_allows_legacy_chat_cli_internal_gateway(request):
|
|
return None
|
|
return gateway_impl._build_retired_internal_gateway_response()
|
|
|
|
|
|
@router.post("/resolve")
|
|
async def resolve_gateway_route(
|
|
request: Request, payload: GatewayResolveRequest
|
|
) -> Response:
|
|
gateway_impl.ensure_loopback(request)
|
|
retired = _ensure_legacy_internal_gateway_request(request)
|
|
if retired is not None:
|
|
return retired
|
|
decision = classify_gateway_route(payload.method, payload.path, payload.headers)
|
|
decision.auth_context = await gateway_impl._resolve_auth_context(payload, decision)
|
|
return JSONResponse(status_code=200, content=decision.model_dump(exclude_none=True))
|
|
|
|
|
|
@router.post("/auth-context")
|
|
async def resolve_gateway_auth_context(
|
|
request: Request,
|
|
payload: GatewayAuthContextRequest,
|
|
) -> Response:
|
|
gateway_impl.ensure_loopback(request)
|
|
retired = _ensure_legacy_internal_gateway_request(request)
|
|
if retired is not None:
|
|
return retired
|
|
auth_context = await gateway_impl._resolve_auth_context_signature(
|
|
headers=payload.headers,
|
|
query_string=payload.query_string,
|
|
auth_endpoint_signature=payload.auth_endpoint_signature,
|
|
)
|
|
return JSONResponse(status_code=200, content={"auth_context": auth_context})
|
|
|
|
|
|
@router.post("/decision-sync")
|
|
async def decide_gateway_sync(
|
|
request: Request,
|
|
payload: GatewayExecuteRequest,
|
|
db: Session = Depends(get_db),
|
|
) -> Response:
|
|
gateway_impl.ensure_loopback(request)
|
|
retired = _ensure_legacy_internal_gateway_request(request)
|
|
if retired is not None:
|
|
return retired
|
|
decision = classify_gateway_route(payload.method, payload.path, payload.headers)
|
|
if not gateway_impl._allows_legacy_chat_cli_internal_route(request, decision):
|
|
return gateway_impl._build_retired_internal_gateway_response()
|
|
auth_context = await gateway_impl._resolve_gateway_execute_auth_context(
|
|
payload=payload,
|
|
decision=decision,
|
|
)
|
|
|
|
if auth_context is None or not auth_context.access_allowed:
|
|
return JSONResponse(status_code=200, content={"action": "fallback_plan"})
|
|
|
|
try:
|
|
resolved = await gateway_impl._build_gateway_sync_decision_response(
|
|
request=request,
|
|
payload=payload,
|
|
db=db,
|
|
auth_context=auth_context,
|
|
decision=decision,
|
|
)
|
|
except HTTPException as exc:
|
|
headers = dict(exc.headers or {})
|
|
headers[CONTROL_EXECUTED_HEADER] = "true"
|
|
return JSONResponse(
|
|
status_code=exc.status_code,
|
|
content={"detail": exc.detail},
|
|
headers=headers,
|
|
)
|
|
|
|
if resolved is None:
|
|
body: dict[str, object] = {"action": "fallback_plan"}
|
|
if payload.auth_context is None:
|
|
body["auth_context"] = auth_context.model_dump(exclude_none=True)
|
|
return JSONResponse(status_code=200, content=body)
|
|
|
|
if payload.auth_context is not None:
|
|
resolved.auth_context = None
|
|
|
|
return JSONResponse(status_code=200, content=resolved.model_dump(exclude_none=True))
|
|
|
|
|
|
@router.post("/decision-stream")
|
|
async def decide_gateway_stream(
|
|
request: Request,
|
|
payload: GatewayExecuteRequest,
|
|
db: Session = Depends(get_db),
|
|
) -> Response:
|
|
gateway_impl.ensure_loopback(request)
|
|
retired = _ensure_legacy_internal_gateway_request(request)
|
|
if retired is not None:
|
|
return retired
|
|
decision = classify_gateway_route(payload.method, payload.path, payload.headers)
|
|
if not gateway_impl._allows_legacy_chat_cli_internal_route(request, decision):
|
|
return gateway_impl._build_retired_internal_gateway_response()
|
|
auth_context = await gateway_impl._resolve_gateway_execute_auth_context(
|
|
payload=payload,
|
|
decision=decision,
|
|
)
|
|
|
|
if auth_context is None or not auth_context.access_allowed:
|
|
return JSONResponse(status_code=200, content={"action": "fallback_plan"})
|
|
|
|
try:
|
|
resolved = await gateway_impl._build_gateway_stream_decision_response(
|
|
request=request,
|
|
payload=payload,
|
|
db=db,
|
|
auth_context=auth_context,
|
|
decision=decision,
|
|
)
|
|
except HTTPException as exc:
|
|
headers = dict(exc.headers or {})
|
|
headers[CONTROL_EXECUTED_HEADER] = "true"
|
|
return JSONResponse(
|
|
status_code=exc.status_code,
|
|
content={"detail": exc.detail},
|
|
headers=headers,
|
|
)
|
|
|
|
if resolved is None:
|
|
body: dict[str, object] = {"action": "fallback_plan"}
|
|
if payload.auth_context is None:
|
|
body["auth_context"] = auth_context.model_dump(exclude_none=True)
|
|
return JSONResponse(status_code=200, content=body)
|
|
|
|
if payload.auth_context is not None:
|
|
resolved.auth_context = None
|
|
|
|
return JSONResponse(status_code=200, content=resolved.model_dump(exclude_none=True))
|
|
|
|
|
|
@router.post("/execute-sync")
|
|
async def execute_gateway_sync(
|
|
request: Request,
|
|
payload: GatewayExecuteRequest,
|
|
db: Session = Depends(get_db),
|
|
) -> Response:
|
|
retired = _ensure_legacy_internal_gateway_request(request)
|
|
if retired is not None:
|
|
return retired
|
|
return await gateway_impl._execute_gateway_control_request(
|
|
request=request,
|
|
payload=payload,
|
|
db=db,
|
|
require_stream=False,
|
|
)
|
|
|
|
|
|
@router.post("/execute-stream")
|
|
async def execute_gateway_stream(
|
|
request: Request,
|
|
payload: GatewayExecuteRequest,
|
|
db: Session = Depends(get_db),
|
|
) -> Response:
|
|
retired = _ensure_legacy_internal_gateway_request(request)
|
|
if retired is not None:
|
|
return retired
|
|
return await gateway_impl._execute_gateway_control_request(
|
|
request=request,
|
|
payload=payload,
|
|
db=db,
|
|
require_stream=True,
|
|
)
|
|
|
|
|
|
@router.post("/plan-stream")
|
|
async def plan_gateway_stream(
|
|
request: Request,
|
|
payload: GatewayExecuteRequest,
|
|
db: Session = Depends(get_db),
|
|
) -> Response:
|
|
gateway_impl.ensure_loopback(request)
|
|
retired = _ensure_legacy_internal_gateway_request(request)
|
|
if retired is not None:
|
|
return retired
|
|
decision = classify_gateway_route(payload.method, payload.path, payload.headers)
|
|
if not gateway_impl._allows_legacy_chat_cli_internal_route(request, decision):
|
|
return gateway_impl._build_retired_internal_gateway_response()
|
|
try:
|
|
planned = await gateway_impl._build_gateway_stream_plan_response(
|
|
request=request,
|
|
payload=payload,
|
|
db=db,
|
|
)
|
|
except HTTPException as exc:
|
|
headers = dict(exc.headers or {})
|
|
headers[CONTROL_EXECUTED_HEADER] = "true"
|
|
return JSONResponse(
|
|
status_code=exc.status_code,
|
|
content={"detail": exc.detail},
|
|
headers=headers,
|
|
)
|
|
|
|
if planned is None:
|
|
response = await gateway_impl._execute_gateway_control_request(
|
|
request=request,
|
|
payload=payload,
|
|
db=db,
|
|
require_stream=True,
|
|
)
|
|
if gateway_impl._is_gateway_control_executed_response(response):
|
|
return response
|
|
return gateway_impl._build_proxy_public_fallback_response()
|
|
|
|
if payload.auth_context is not None:
|
|
planned.auth_context = None
|
|
|
|
return JSONResponse(status_code=200, content=planned.model_dump(exclude_none=True))
|
|
|
|
|
|
@router.post("/plan-sync")
|
|
async def plan_gateway_sync(
|
|
request: Request,
|
|
payload: GatewayExecuteRequest,
|
|
db: Session = Depends(get_db),
|
|
) -> Response:
|
|
gateway_impl.ensure_loopback(request)
|
|
retired = _ensure_legacy_internal_gateway_request(request)
|
|
if retired is not None:
|
|
return retired
|
|
decision = classify_gateway_route(payload.method, payload.path, payload.headers)
|
|
if not gateway_impl._allows_legacy_chat_cli_internal_route(request, decision):
|
|
return gateway_impl._build_retired_internal_gateway_response()
|
|
try:
|
|
planned = await gateway_impl._build_gateway_sync_plan_response(
|
|
request=request,
|
|
payload=payload,
|
|
db=db,
|
|
)
|
|
except HTTPException as exc:
|
|
headers = dict(exc.headers or {})
|
|
headers[CONTROL_EXECUTED_HEADER] = "true"
|
|
return JSONResponse(
|
|
status_code=exc.status_code,
|
|
content={"detail": exc.detail},
|
|
headers=headers,
|
|
)
|
|
|
|
if planned is None:
|
|
response = await gateway_impl._execute_gateway_control_request(
|
|
request=request,
|
|
payload=payload,
|
|
db=db,
|
|
require_stream=False,
|
|
)
|
|
if gateway_impl._is_gateway_control_executed_response(response):
|
|
return response
|
|
return gateway_impl._build_proxy_public_fallback_response()
|
|
|
|
if payload.auth_context is not None:
|
|
planned.auth_context = None
|
|
|
|
return JSONResponse(status_code=200, content=planned.model_dump(exclude_none=True))
|
|
|
|
|
|
@router.post("/report-sync")
|
|
async def report_gateway_sync(
|
|
request: Request,
|
|
payload: GatewaySyncReportRequest,
|
|
background_tasks: BackgroundTasks,
|
|
) -> Response:
|
|
gateway_impl.ensure_loopback(request)
|
|
retired = _ensure_legacy_internal_gateway_request(request)
|
|
if retired is not None:
|
|
return retired
|
|
if not gateway_impl._allows_legacy_chat_cli_report_route(request, payload.report_kind):
|
|
return gateway_impl._build_retired_internal_gateway_response()
|
|
payload_copy = payload.model_copy(deep=True)
|
|
if gateway_impl._gateway_sync_report_requires_inline(payload_copy):
|
|
db, cleanup = gateway_impl._resolve_gateway_background_db(getattr(request, "app", None))
|
|
try:
|
|
await gateway_impl._run_gateway_sync_report_background(payload_copy, db)
|
|
finally:
|
|
if cleanup is not None:
|
|
cleanup()
|
|
return JSONResponse(status_code=200, content={"ok": True})
|
|
background_tasks.add_task(
|
|
gateway_impl._run_gateway_sync_report_background_with_session,
|
|
payload_copy,
|
|
getattr(request, "app", None),
|
|
)
|
|
return JSONResponse(status_code=200, content={"ok": True})
|
|
|
|
|
|
@router.post("/finalize-sync")
|
|
async def finalize_gateway_sync(
|
|
request: Request,
|
|
payload: GatewaySyncReportRequest,
|
|
background_tasks: BackgroundTasks,
|
|
) -> Response:
|
|
gateway_impl.ensure_loopback(request)
|
|
retired = _ensure_legacy_internal_gateway_request(request)
|
|
if retired is not None:
|
|
return retired
|
|
if not gateway_impl._allows_legacy_chat_cli_report_route(request, payload.report_kind):
|
|
return gateway_impl._build_retired_internal_gateway_response()
|
|
fast_response = await gateway_impl._maybe_build_gateway_core_sync_fast_success_response(payload)
|
|
if fast_response is not None:
|
|
if payload.report_kind in {
|
|
"openai_chat_sync_finalize",
|
|
"claude_chat_sync_finalize",
|
|
"gemini_chat_sync_finalize",
|
|
}:
|
|
background_tasks.add_task(
|
|
gateway_impl._run_gateway_chat_sync_finalize_background_with_session,
|
|
payload.model_copy(deep=True),
|
|
)
|
|
elif payload.report_kind in {
|
|
"openai_cli_sync_finalize",
|
|
"openai_compact_sync_finalize",
|
|
"claude_cli_sync_finalize",
|
|
"gemini_cli_sync_finalize",
|
|
}:
|
|
background_tasks.add_task(
|
|
gateway_impl._run_gateway_cli_sync_finalize_background_with_session,
|
|
payload.model_copy(deep=True),
|
|
)
|
|
fast_response.headers[CONTROL_EXECUTED_HEADER] = "true"
|
|
return fast_response
|
|
|
|
db, cleanup = gateway_impl._resolve_gateway_finalize_db(request)
|
|
try:
|
|
response = await gateway_impl._finalize_gateway_sync_response(
|
|
payload,
|
|
db=db,
|
|
background_tasks=background_tasks,
|
|
)
|
|
except Exception:
|
|
if cleanup is not None:
|
|
cleanup()
|
|
raise
|
|
if cleanup is not None:
|
|
background_tasks.add_task(cleanup)
|
|
response.headers[CONTROL_EXECUTED_HEADER] = "true"
|
|
return response
|
|
|
|
|
|
@router.post("/report-stream")
|
|
async def report_gateway_stream(
|
|
request: Request,
|
|
payload: GatewayStreamReportRequest,
|
|
background_tasks: BackgroundTasks,
|
|
) -> Response:
|
|
gateway_impl.ensure_loopback(request)
|
|
retired = _ensure_legacy_internal_gateway_request(request)
|
|
if retired is not None:
|
|
return retired
|
|
if not gateway_impl._allows_legacy_chat_cli_report_route(request, payload.report_kind):
|
|
return gateway_impl._build_retired_internal_gateway_response()
|
|
background_tasks.add_task(
|
|
gateway_impl._run_gateway_stream_report_background_with_session,
|
|
payload.model_copy(deep=True),
|
|
getattr(request, "app", None),
|
|
)
|
|
return JSONResponse(status_code=200, content={"ok": True})
|