mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-08 22:20:54 +08:00
本次迁移将网关/通道/工具/技能/脚本与协议资源集中到新结构,统一路径常量与脚本转发机制,减少顶层噪音并保证运行与测试行为一致。 Made-with: Cursor
200 lines
7.2 KiB
Python
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"]
|
|
|