"""routes/ave_docs.py — 📚 API 文档中心 把本地存档的各平台官方 API 文档(`09_ave/docs/api/`)暴露给看板: · GET /api/ave/docs/index 文档索引(各平台 + 页面清单) · GET /api/ave/docs/page?file=raw/... 读取单个文档正文(Markdown) · GET /api/ave/docs/refresh 触发重新抓取(后台线程,只读网络) 数据来源:`09_ave/docs/api/index.json`(由 scripts/tools/fetch_api_docs.py 生成) """ from __future__ import annotations import json import logging import os import subprocess import sys import threading from pathlib import Path from fastapi import APIRouter, HTTPException log = logging.getLogger("dashboard.ave_docs") router = APIRouter(prefix="/api/ave/docs", tags=["ave-docs"]) # 与其它 ave 路由一致的路径解析方式(支持 AGENT_SYNC 覆盖) _AGENT_SYNC = Path(os.environ.get("AGENT_SYNC", str(Path(__file__).resolve().parents[3]))) AVE_ROOT = _AGENT_SYNC / "05_tools" / "09_ave" DOCS_DIR = AVE_ROOT / "docs" / "api" INDEX = DOCS_DIR / "index.json" PY = os.environ.get("AGENTOS_PYTHON") or sys.executable def _load_index() -> dict: if not INDEX.exists(): return {"generated_at": None, "sources": [], "hint": "尚未抓取,点「🔄 更新文档」"} try: return json.loads(INDEX.read_text(encoding="utf-8")) except Exception as e: raise HTTPException(500, detail=f"索引读取失败: {e}") @router.get("/index") def api_docs_index(): """文档索引:各平台 + 页面清单(含本地文件路径 / 原文 URL)。""" d = _load_index() d["docs_dir"] = str(DOCS_DIR) d["has_local"] = any((p.get("file") for s in d.get("sources", []) for p in s.get("pages", []))) return d @router.get("/page") def api_docs_page(file: str): """读取单个文档正文(限制在 docs/api 目录内,防目录穿越)。""" p = (DOCS_DIR / file).resolve() base = DOCS_DIR.resolve() if not str(p).startswith(str(base)): raise HTTPException(400, detail="非法路径") if not p.exists() or not p.is_file(): raise HTTPException(404, detail=f"文档不存在: {file}") return {"file": file, "bytes": p.stat().st_size, "content": p.read_text(encoding="utf-8", errors="ignore")} @router.post("/refresh") def api_docs_refresh(force: bool = False): """触发重新抓取(后台线程)。force=true 时全量重抓。""" script = AVE_ROOT / "scripts" / "tools" / "fetch_api_docs.py" if not script.exists(): raise HTTPException(500, detail=f"抓取脚本不存在: {script}") def _run(): try: cmd = [PY, str(script)] + (["--force"] if force else []) r = subprocess.run(cmd, capture_output=True, text=True, timeout=900) log.info("文档抓取完成 rc=%s\n%s", r.returncode, (r.stdout or "")[-800:]) except Exception as e: log.warning("文档抓取失败: %s", e) threading.Thread(target=_run, daemon=True).start() return {"status": "ok", "note": "抓取已在后台开始(完成刷新页面即可看到)"}