api_server.py 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  1. """
  2. API Server - FastAPI 应用入口
  3. 聚合所有模块的 API 路由:
  4. - GET /api/traces — 查询(trace/api.py)
  5. - POST /api/traces — 执行控制(trace/run_api.py,需配置 Runner)
  6. - WS /api/traces/{id}/watch — 实时推送(trace/websocket.py)
  7. - GET /api/experiences — 经验查询(trace/run_api.py,需配置 Runner)
  8. """
  9. import json
  10. import logging
  11. import os
  12. import fcntl
  13. from ipaddress import ip_address
  14. from pathlib import Path
  15. from dotenv import load_dotenv
  16. load_dotenv()
  17. from fastapi import FastAPI, Request, WebSocket
  18. from fastapi.responses import JSONResponse
  19. from fastapi.middleware.cors import CORSMiddleware
  20. from fastapi.middleware.trustedhost import TrustedHostMiddleware
  21. import uvicorn
  22. from cyber_agent.trace import FileSystemTraceStore
  23. from cyber_agent.trace.api import router as api_router, set_trace_store as set_api_trace_store
  24. from cyber_agent.trace.run_api import router as run_router, experiences_router, set_runner
  25. from cyber_agent.trace.websocket import router as ws_router, set_trace_store as set_ws_trace_store
  26. from cyber_agent.trace.examples_api import router as examples_router
  27. from cyber_agent.trace.logs_websocket import router as logs_router, setup_websocket_logging
  28. from cyber_agent.trace.upload_api import router as upload_router, set_trace_store as set_upload_trace_store
  29. # ===== 日志配置 =====
  30. logging.basicConfig(
  31. level=logging.INFO,
  32. format="%(asctime)s [%(levelname)s] %(name)s: %(message)s"
  33. )
  34. logger = logging.getLogger(__name__)
  35. # ===== 单用户本地开发边界 =====
  36. LOCAL_DEVELOPMENT_ORIGINS = (
  37. "http://127.0.0.1:3000",
  38. "http://localhost:3000",
  39. )
  40. LOCAL_DEVELOPMENT_HOSTS = ("127.0.0.1", "localhost")
  41. _writer_lock_file = None
  42. def _env_flag(name: str, *, default: bool = False) -> bool:
  43. """解析显式开关;未配置和未识别的值均按关闭处理。"""
  44. value = os.getenv(name)
  45. if value is None:
  46. return default
  47. return value.strip().lower() in {"1", "true", "yes", "on"}
  48. def _is_local_websocket_origin(websocket: WebSocket) -> bool:
  49. """浏览器 WebSocket 必须来自本地前端。"""
  50. return websocket.headers.get("origin") in LOCAL_DEVELOPMENT_ORIGINS
  51. def _is_loopback_host(host: str | None) -> bool:
  52. """Only numeric IPv4/IPv6 loopback peers are trusted."""
  53. if not host:
  54. return False
  55. try:
  56. return ip_address(host).is_loopback
  57. except ValueError:
  58. return False
  59. def _is_loopback_websocket_peer(websocket: WebSocket) -> bool:
  60. return _is_loopback_host(websocket.client.host if websocket.client else None)
  61. def _acquire_single_writer_lock():
  62. """Fail startup when another API process owns this Trace directory."""
  63. global _writer_lock_file
  64. if _writer_lock_file is not None:
  65. return
  66. lock_path = Path(".trace").resolve() / ".api-writer.lock"
  67. lock_path.parent.mkdir(parents=True, exist_ok=True)
  68. handle = lock_path.open("a+")
  69. try:
  70. fcntl.flock(handle.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
  71. except BlockingIOError as exc:
  72. handle.close()
  73. raise RuntimeError(
  74. f"another Agent API writer already owns {lock_path.parent}"
  75. ) from exc
  76. _writer_lock_file = handle
  77. def _release_single_writer_lock():
  78. global _writer_lock_file
  79. if _writer_lock_file is None:
  80. return
  81. fcntl.flock(_writer_lock_file.fileno(), fcntl.LOCK_UN)
  82. _writer_lock_file.close()
  83. _writer_lock_file = None
  84. # 设置WebSocket日志推送
  85. setup_websocket_logging(level=logging.INFO)
  86. # ===== FastAPI 应用 =====
  87. app = FastAPI(
  88. title="Agent API",
  89. description="Agent 查询 + 执行 API",
  90. version="1.0.0"
  91. )
  92. # CORS 配置(允许前端跨域访问)
  93. app.add_middleware(
  94. CORSMiddleware,
  95. allow_origins=list(LOCAL_DEVELOPMENT_ORIGINS),
  96. allow_credentials=False,
  97. allow_methods=["*"],
  98. allow_headers=["*"],
  99. )
  100. # 拒绝非本地 Host,防止误暴露和 DNS rebinding。
  101. app.add_middleware(
  102. TrustedHostMiddleware,
  103. allowed_hosts=list(LOCAL_DEVELOPMENT_HOSTS),
  104. )
  105. @app.middleware("http")
  106. async def reject_non_local_browser_origins(request: Request, call_next):
  107. """Fail closed unless the TCP peer and optional browser Origin are local."""
  108. if not _is_loopback_host(request.client.host if request.client else None):
  109. return JSONResponse(
  110. status_code=403,
  111. content={"detail": "Loopback client required"},
  112. )
  113. origin = request.headers.get("origin")
  114. if origin is not None and origin not in LOCAL_DEVELOPMENT_ORIGINS:
  115. return JSONResponse(
  116. status_code=403,
  117. content={"detail": "Local development origin required"},
  118. )
  119. return await call_next(request)
  120. # ===== 初始化存储 =====
  121. # 使用文件系统存储(支持跨进程和持久化)
  122. trace_store = FileSystemTraceStore(base_path=".trace")
  123. # 注入到 step_tree 模块
  124. set_api_trace_store(trace_store)
  125. set_ws_trace_store(trace_store)
  126. set_upload_trace_store(trace_store)
  127. # ===== 可选:配置 Runner(启用执行 API)=====
  128. # 如需启用 POST /api/traces(新建/运行/停止/反思),取消以下注释并配置 LLM:
  129. from cyber_agent.core.runner import AgentRunner
  130. from cyber_agent.llm import create_openrouter_llm_call
  131. runner = AgentRunner(
  132. trace_store=trace_store,
  133. llm_call=create_openrouter_llm_call(model="anthropic/claude-sonnet-4.5"),
  134. )
  135. set_runner(runner)
  136. # ===== 注册路由 =====
  137. # Examples API(GET /api/examples)
  138. app.include_router(examples_router)
  139. # Trace 上传 API(POST /api/traces/upload)
  140. app.include_router(upload_router)
  141. # Trace 执行 API(POST + GET /running,需配置 Runner)
  142. # 注意:run_router 必须在 api_router 之前注册,否则 GET /running 会被 /{trace_id} 捕获
  143. app.include_router(run_router)
  144. # 经验 API(GET /api/experiences,需配置 Runner)
  145. app.include_router(experiences_router)
  146. # Trace 查询 API(GET)
  147. app.include_router(api_router)
  148. # Trace WebSocket(实时推送)
  149. app.include_router(ws_router)
  150. # Logs WebSocket(日志推送)
  151. app.include_router(logs_router)
  152. @app.on_event("startup")
  153. async def on_startup():
  154. """服务器启动时执行状态对齐"""
  155. _acquire_single_writer_lock()
  156. for trace in await trace_store.list_traces(limit=10_000):
  157. await trace_store.reconcile_trace_integrity(trace.trace_id)
  158. from cyber_agent.trace.run_api import reconcile_traces
  159. await reconcile_traces()
  160. @app.on_event("shutdown")
  161. async def on_shutdown():
  162. _release_single_writer_lock()
  163. @app.websocket("/ws_ping")
  164. async def ws_ping(websocket: WebSocket):
  165. if not _is_loopback_websocket_peer(websocket):
  166. await websocket.close(code=1008, reason="Loopback client required")
  167. return
  168. if not _is_local_websocket_origin(websocket):
  169. await websocket.close(code=1008, reason="Local development origin required")
  170. return
  171. await websocket.accept()
  172. await websocket.send_text("pong")
  173. await websocket.close()
  174. # ===== 健康检查 =====
  175. @app.get("/health")
  176. async def health_check():
  177. """健康检查"""
  178. return {
  179. "status": "ok",
  180. "service": "Agent Step Tree API",
  181. "version": "1.0.0",
  182. "deployment_mode": "local_development",
  183. "network_scope": "loopback_only",
  184. "authentication": "none",
  185. }
  186. # ===== 启动服务 =====
  187. if __name__ == "__main__":
  188. logger.info("Starting API server...")
  189. uvicorn.run(
  190. "api_server:app",
  191. host="127.0.0.1",
  192. port=8000,
  193. reload=_env_flag("TRACE_API_RELOAD"),
  194. log_level="info"
  195. )