Files
PlantAI-rag-new/sdk/app/routers/capabilities.py
T

112 lines
4.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""routers/capabilities.py —— 能力端点(S4)。
- GET /api/capabilities 能力清单 + 默认值 + 当前会话开关 + QA 已装插件
- POST /api/capabilities/install 透传 QA /capabilities/install(聊天内自助安装)
"""
from __future__ import annotations
import json
from typing import Any, Optional
import httpx
from fastapi import APIRouter, Depends, Request
from fastapi.responses import JSONResponse
from ..db import Database, get_db, loads_json
from ..services import sse_relay
from ..services.capabilities import (
DEFAULTS,
MANIFEST,
merged_capabilities,
sanitize_capabilities,
)
from ..upstream import envelope
logger = __import__("logging").getLogger(__name__)
router = APIRouter(prefix="/api/capabilities", tags=["capabilities"])
@router.get("")
async def list_capabilities(
request: Request, db: Database = Depends(get_db)
) -> JSONResponse:
"""能力清单 + 当前会话开关(?session_id= 可选)+ QA 注册表合并。"""
session_id = str(request.query_params.get("session_id") or "").strip()
session_caps: Optional[dict] = None
if session_id:
try:
user = getattr(request.state, "user", None)
owner_id = user["id"] if user else None
if owner_id is None:
row = await db.fetchone("SELECT meta FROM sessions WHERE id = ?", (session_id,))
else:
row = await db.fetchone("SELECT meta FROM sessions WHERE id = ? AND owner_id = ?", (session_id, owner_id))
if row is not None:
meta = loads_json(row["meta"], default={})
if isinstance(meta, dict):
session_caps = sanitize_capabilities(meta.get("capabilities"))
except Exception as exc:
logger.warning("读取会话能力失败: %s", exc)
# QA 注册表合并(失败静默降级:无已装插件信息)
qa_registry: list = []
qa_warnings: list = []
try:
client = sse_relay.get_qa_client()
url = f"{sse_relay.qa_base_url()}{sse_relay.QA_UPSTREAM_PREFIX}/capabilities/registry"
resp = await client.get(url, timeout=5.0)
if resp.status_code < 400:
payload = resp.json() if isinstance(resp.json(), dict) else {}
qa_registry = payload.get("installed") or payload.get("tools") or []
qa_warnings = payload.get("load_warnings") or []
except Exception:
pass # QA 侧不可达:面板仍可用(BFF 清单独立)
body = merged_capabilities(session_caps, qa_registry, qa_warnings)
return JSONResponse(content=body)
@router.post("/install")
async def install_capability(request: Request) -> JSONResponse:
"""透传 QA /capabilities/install(loopback)。"""
try:
payload = await request.json()
except Exception:
body, status = envelope("VALIDATION", message="请求参数不合法", status=422)
return JSONResponse(status_code=status, content=body)
action = str(payload.get("action") or "register")
if action not in ("register", "unregister"):
body, status = envelope("VALIDATION", message="action 仅支持 register|unregister", status=422)
return JSONResponse(status_code=status, content=body)
client = sse_relay.get_qa_client()
url = f"{sse_relay.qa_base_url()}{sse_relay.QA_UPSTREAM_PREFIX}/capabilities/install"
try:
resp = await client.post(url, json=payload, timeout=30.0)
except (httpx.ConnectError, httpx.ConnectTimeout, httpx.PoolTimeout) as exc:
logger.warning("能力安装 QA 连接失败: %s", exc)
body, status = envelope("UPSTREAM_DOWN", status=503)
return JSONResponse(status_code=status, content=body)
except httpx.HTTPError as exc:
logger.warning("能力安装请求失败: %s", exc)
body, status = envelope("UPSTREAM_ERROR", status=502)
return JSONResponse(status_code=status, content=body)
if resp.status_code >= 400:
detail = {}
try:
detail = resp.json()
except Exception:
pass
message = (
detail.get("detail", {}).get("message")
if isinstance(detail.get("detail"), dict)
else detail.get("message")
) or "能力安装失败"
body, status = envelope("UPSTREAM_ERROR", message=message, status=502)
return JSONResponse(status_code=status, content=body)
try:
return JSONResponse(content=resp.json())
except Exception:
body, status = envelope("UPSTREAM_ERROR", message="能力安装响应异常", status=502)
return JSONResponse(status_code=status, content=body)