oclaw/interfaces/http/fastapi_app.py
oliver 4a23b715a2 重构仓库目录为统一的 runtime 分层并清理历史 openclaw 残留。
本次迁移将网关/通道/工具/技能/脚本与协议资源集中到新结构,统一路径常量与脚本转发机制,减少顶层噪音并保证运行与测试行为一致。

Made-with: Cursor
2026-04-25 01:24:23 +08:00

200 lines
7.2 KiB
Python

from __future__ import annotations
import asyncio
import os
from contextlib import asynccontextmanager
from typing import Any
from fastapi import FastAPI, WebSocket
from fastapi import Request
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from oclaw.runtime.application.gateway import process_inbound_payload_usecase
from oclaw.interfaces.gateway.http_adapter import dispatch_gateway_http_method
from oclaw.interfaces.ws import ws_gateway_loop
from oclaw.interfaces.admin.routes import admin_static_dir, build_admin_router
from oclaw.runtime.agents.agent_scope import list_agent_ids, resolve_agent_workspace_dir, resolve_default_agent_id
from oclaw.interfaces.gateway.server_startup_plugins import prepare_gateway_plugin_bootstrap
from oclaw.runtime.hooks_runtime import (
initialize_hooks_runtime,
resolve_runtime_config,
trigger_hook_event,
)
from oclaw.runtime.skills import skill_runtime_diagnostics
from oclaw.platform.config.paths import PROJECT_ROOT, db_path
from oclaw.platform.persistence.sqlite_store import SqliteStore
from oclaw.interfaces.http.weixin_ilink_api import router as weixin_ilink_router
def _resolve_startup_workspace_dir(cfg: dict[str, Any]) -> str:
try:
default_agent_id = resolve_default_agent_id(cfg)
ws = resolve_agent_workspace_dir(cfg, default_agent_id)
ws_text = str(ws or "").strip()
if ws_text:
return ws_text
except Exception:
pass
return str((PROJECT_ROOT / "oclaw" / "runtime" / "assets" / "agent_workspaces" / "workspace-main").resolve())
def _resolve_startup_workspace_dirs(cfg: dict[str, Any]) -> list[tuple[str, str]]:
out: list[tuple[str, str]] = []
seen_ws: set[str] = set()
try:
agent_ids = list_agent_ids(cfg)
except Exception:
agent_ids = []
if not agent_ids:
agent_ids = [resolve_default_agent_id(cfg)]
for aid in agent_ids:
try:
ws = str(resolve_agent_workspace_dir(cfg, aid) or "").strip()
except Exception:
ws = ""
if not ws:
continue
key = ws.lower() if os.name == "nt" else ws
if key in seen_ws:
continue
seen_ws.add(key)
out.append((str(aid or "default"), ws))
if out:
return out
return [("default", _resolve_startup_workspace_dir(cfg))]
def _run_startup_hooks(app: FastAPI) -> None:
def _log_info(message: str) -> None:
print(message)
def _log_warn(message: str) -> None:
print(message)
def _log_error(message: str) -> None:
print(message)
def _log_debug(message: str) -> None:
_ = message
# Force re-login after every gateway restart.
try:
revoked = SqliteStore(db_path()).revoke_all_auth_sessions()
if revoked > 0:
_log_info(f"[auth] revoked sessions on startup: {revoked}")
except Exception as exc:
_log_warn(f"[auth] failed to revoke sessions on startup: {exc}")
try:
cfg: dict[str, Any] = {}
boot = prepare_gateway_plugin_bootstrap(
cfg_at_start=cfg,
startup_runtime_config=cfg,
minimal_test_gateway=False,
log={
"info": _log_info,
"warn": _log_warn,
"error": _log_error,
"debug": _log_debug,
},
core_gateway_handlers={},
base_methods=[],
)
app.state.gateway_plugin_bootstrap = boot
except Exception:
app.state.gateway_plugin_bootstrap = None
cfg = resolve_runtime_config()
try:
diag = skill_runtime_diagnostics()
_log_info(f"[skills] root={diag.get('skills_root')} total={diag.get('skills_total')}")
except Exception:
pass
startup_targets = _resolve_startup_workspace_dirs(cfg)
initialize_hooks_runtime(cfg=cfg, workspace_dir=startup_targets[0][1])
for agent_id, ws_dir in startup_targets:
trigger_hook_event(
event_type="gateway",
action="startup",
session_key=f"system:gateway:startup:{agent_id}",
context={"cfg": cfg, "workspaceDir": ws_dir, "agentId": agent_id},
)
@asynccontextmanager
async def _lifespan(app: FastAPI): # type: ignore[no-untyped-def]
_run_startup_hooks(app)
yield
def create_app() -> FastAPI:
app = FastAPI(title="ops-gateway", version="0.1", lifespan=_lifespan)
app.mount("/admin/assets", StaticFiles(directory=str(admin_static_dir())), name="admin-assets")
app.include_router(build_admin_router())
app.include_router(weixin_ilink_router)
@app.middleware("http")
async def _no_cache_for_admin_assets(request: Request, call_next): # type: ignore[no-untyped-def]
resp = await call_next(request)
# Avoid stale JS/CSS after refactors (especially in Electron webview).
# The admin SPA and /chat both load from /admin/assets/...
if str(request.url.path or "").startswith("/admin/assets/"):
resp.headers["Cache-Control"] = "no-store, max-age=0"
resp.headers["Pragma"] = "no-cache"
return resp
@app.get("/health")
def health() -> dict[str, str]:
return {"ok": "1"}
@app.get("/chat", response_class=HTMLResponse)
def chat_standalone() -> HTMLResponse:
p = admin_static_dir() / "chat.html"
return HTMLResponse(p.read_text(encoding="utf-8"))
@app.post("/inbound")
async def inbound(payload: dict[str, Any]) -> dict[str, Any]:
return await asyncio.to_thread(process_inbound_payload_usecase, payload)
@app.post("/inbound/{channel}")
async def inbound_by_channel(channel: str, payload: dict[str, Any]) -> dict[str, Any]:
payload = payload if isinstance(payload, dict) else {}
payload["channel"] = str(channel or "").strip().lower()
return await asyncio.to_thread(process_inbound_payload_usecase, payload)
@app.post("/wecom/inbound")
async def wecom_inbound(payload: dict[str, Any]) -> dict[str, Any]:
payload = payload if isinstance(payload, dict) else {}
payload["channel"] = "wecom"
return await asyncio.to_thread(process_inbound_payload_usecase, payload)
@app.post("/gateway/method")
async def gateway_method(payload: dict[str, Any]) -> dict[str, Any]:
body = payload if isinstance(payload, dict) else {}
method = str(body.get("method") or "").strip()
params = body.get("params") if isinstance(body.get("params"), dict) else {}
return await asyncio.to_thread(dispatch_gateway_http_method, method=method, params=params)
@app.websocket("/ws")
async def ws_endpoint(ws: WebSocket) -> None:
await ws_gateway_loop(ws)
return app
def main() -> int:
host = (os.getenv("AIA_ASSISTANT_GATEWAY_HOST") or "0.0.0.0").strip()
port = int(os.getenv("AIA_ASSISTANT_GATEWAY_PORT") or "8787")
try:
import uvicorn
except Exception as exc:
raise RuntimeError("missing dependency: uvicorn (pip install -r requirements.txt)") from exc
uvicorn.run("oclaw.interfaces.http.fastapi_app:create_app", host=host, port=port, reload=False, factory=True)
return 0
__all__ = ["create_app", "main", "_resolve_startup_workspace_dir", "_resolve_startup_workspace_dirs"]