112 lines
4.6 KiB
Python
112 lines
4.6 KiB
Python
"""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)
|