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

107 lines
4.5 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.
# -*- 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}