Files
server-core/app/api/routers/agent_gate.py
T

107 lines
4.5 KiB
Python
Raw Normal View History

# -*- coding: utf-8 -*-
"""智能体互通 API:内部事件接收 + 临时授权管理。
- ``POST /internal/im/agent-events``im-service 在 agent_task 消息落库后回调,
由编排层完成权限校验 → 在线查询 → MQTT 派发(携带 X-IM-Token 内部令牌)。
- ``POST/GET/DELETE /agent-grants``L3 成员开放调用(临时授权)管理(用户 API)。
"""
from __future__ import annotations
import logging
from fastapi import APIRouter, Depends, Header, HTTPException
from ..dependencies import get_db, get_current_user
from ... import config
from ...agent_gate import handle_agent_task_event
from ...im import client as im_client
from ...infrastructure.repositories import Database
log = logging.getLogger("agent_gate.api")
router = APIRouter(tags=["agent-gate"])
internal_router = APIRouter(prefix="/internal/im", tags=["agent-gate-internal"])
# ── 内部事件接收(im-service → server-core) ─────────────────────
def _require_im_internal(x_im_token: str = Header(default="", alias="X-IM-Token")) -> None:
"""仅允许携带 IM_INTERNAL_TOKEN 的调用方(im-service)。"""
if not config.IM_INTERNAL_TOKEN or x_im_token != config.IM_INTERNAL_TOKEN:
raise HTTPException(status_code=403, detail="内部接口访问被拒绝")
@internal_router.post("/agent-events", dependencies=[Depends(_require_im_internal)])
async def agent_events(payload: dict, db: Database = Depends(get_db)):
"""接收 im-service 的智能体任务事件(agent_task 消息落库后回调)。
body: {event, conv_id, sender_id, sender_name, content, metadata}
"""
event = payload.get("event", "")
if event != "agent_task":
return {"ok": False, "reason": f"unsupported event: {event}"}
conv_id = str(payload.get("conv_id", ""))
if not conv_id:
return {"ok": False, "reason": "missing conv_id"}
result = await handle_agent_task_event(
db,
im_client,
conv_id=conv_id,
sender_id=str(payload.get("sender_id", "")),
sender_name=str(payload.get("sender_name", "")),
content=str(payload.get("content", "")),
metadata=payload.get("metadata") or {},
)
return result
# ── 用户 API:L3 临时授权管理 ────────────────────────────────────
@router.post("/agent-grants", summary="创建智能体临时授权")
async def create_grant(payload: dict, user: dict = Depends(get_current_user),
db: Database = Depends(get_db)):
"""成员开放调用:授权另一个成员调用自己(的某个)智能体。
body: {grantee_user_id, target_agent_addr, scope?, max_calls?, expires_at?}
"""
grantee = str(payload.get("grantee_user_id", "")).strip()
target_addr = str(payload.get("target_agent_addr", "")).strip()
if not grantee or not target_addr:
raise HTTPException(status_code=422, detail="grantee_user_id 与 target_agent_addr 必填")
from ...agent_gate import parse_agent_addr
parsed = parse_agent_addr(target_addr)
if parsed is None or parsed[0] != user["id"]:
raise HTTPException(status_code=403, detail="只能授权自己名下的智能体")
if grantee == user["id"]:
raise HTTPException(status_code=422, detail="不能授权给自己")
grant = await db.agent_access_grants.create(
grantor_user_id=user["id"],
target_agent_addr=target_addr,
grantee_user_id=grantee,
scope=str(payload.get("scope", "")),
max_calls=int(payload.get("max_calls", 0) or 0),
expires_at=str(payload.get("expires_at", "")),
)
return grant
@router.get("/agent-grants", summary="智能体临时授权列表")
async def list_grants(direction: str = "grantor",
user: dict = Depends(get_current_user),
db: Database = Depends(get_db)):
"""direction=grantor(我授权的)| grantee(我被授权的)。"""
if direction == "grantee":
return await db.agent_access_grants.list_by_grantee(user["id"])
return await db.agent_access_grants.list_by_grantor(user["id"])
@router.delete("/agent-grants/{grant_id}", summary="撤销智能体临时授权")
async def revoke_grant(grant_id: str, user: dict = Depends(get_current_user),
db: Database = Depends(get_db)):
ok = await db.agent_access_grants.revoke(grant_id, user["id"])
if not ok:
raise HTTPException(status_code=404, detail="授权不存在或无权撤销")
return {"ok": True}