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"]