220 lines
9.1 KiB
Python
220 lines
9.1 KiB
Python
# -*- 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}
|
||
|
||
|
||
# ── 用户 API:企业智能体目录(可调用智能体列表 + 在线状态) ──────────
|
||
|
||
@router.get("/agents/directory", summary="企业可调用智能体目录")
|
||
async def list_agent_directory(user: dict = Depends(get_current_user),
|
||
db: Database = Depends(get_db)):
|
||
"""返回当前用户可调用的智能体目录(按成员分组),含在线状态。
|
||
|
||
可调用范围:自己的智能体 + 同企业成员 external_callable=True 的智能体
|
||
(企业管理员可看全部;普通成员受 L2/L3 权限约束,但目录仅展示「可被调用」
|
||
的智能体,实际调用时再由 authorize_call 做最终校验)。
|
||
"""
|
||
from ...api.dependencies import get_user_organizations, get_user_primary_org
|
||
from ...infrastructure.models import OrganizationMember, Organization, CompanyMember
|
||
from sqlalchemy import select
|
||
|
||
caller_id = user["id"]
|
||
is_admin = "enterprise" in (user.get("capabilities") or [])
|
||
|
||
# 1. 确定当前用户的主企业(enterprise 类型)
|
||
primary_org = await get_user_primary_org(db, user, org_type="enterprise")
|
||
if not primary_org:
|
||
# 无企业归属:仅返回自己的智能体
|
||
org_id = ""
|
||
else:
|
||
org_id = primary_org["org_id"]
|
||
|
||
# 2. 收集企业成员 user_id 集合(含自己)
|
||
member_ids: set[str] = {caller_id}
|
||
if org_id:
|
||
# 优先查统一组织表
|
||
try:
|
||
members = (await db.session.scalars(
|
||
select(OrganizationMember).where(
|
||
OrganizationMember.org_id == org_id,
|
||
OrganizationMember.status == "active",
|
||
)
|
||
)).all()
|
||
for m in members:
|
||
member_ids.add(m.user_id)
|
||
except Exception:
|
||
pass
|
||
# 回退旧表 company_members(通过 company_id 关联)
|
||
if len(member_ids) <= 1:
|
||
try:
|
||
from ...infrastructure.models import Company
|
||
company = await db.session.scalar(
|
||
select(Company).where(Company.org_id == org_id).limit(1)
|
||
)
|
||
if company:
|
||
cms = (await db.session.scalars(
|
||
select(CompanyMember).where(
|
||
CompanyMember.company_id == company.id,
|
||
CompanyMember.status == "active",
|
||
)
|
||
)).all()
|
||
for cm in cms:
|
||
member_ids.add(cm.user_id)
|
||
except Exception:
|
||
pass
|
||
|
||
# 3. 批量查询每个成员的用户资料(名称/头像)
|
||
user_profiles: dict[str, dict] = {}
|
||
for uid in member_ids:
|
||
u = await db.users.get_by_id(uid)
|
||
if u:
|
||
user_profiles[uid] = u
|
||
|
||
# 4. 批量查询每个成员的可外部调用智能体(port=opc)
|
||
from ...im import client as im_client
|
||
result: list[dict] = []
|
||
for uid in sorted(member_ids):
|
||
profile = user_profiles.get(uid) or {}
|
||
agents = await db.agents.get_by_user(uid, port="opc")
|
||
# 过滤:自己的全部展示;他人的仅展示 external_callable=True
|
||
visible_agents = []
|
||
for a in agents:
|
||
if uid == caller_id or is_admin or a.get("external_callable"):
|
||
visible_agents.append(a)
|
||
if not visible_agents:
|
||
continue
|
||
|
||
agent_items = []
|
||
for a in visible_agents:
|
||
addr = f"{uid}.{a['id']}"
|
||
# 在线状态查询(IM 内部接口,失败视为离线)
|
||
online = False
|
||
try:
|
||
online_info = await im_client.internal_agent_online(addr)
|
||
online = bool(online_info.get("online"))
|
||
except Exception:
|
||
pass
|
||
agent_items.append({
|
||
"agent_id": a["id"],
|
||
"name": a["name"],
|
||
"avatar": "", # 智能体头像暂由前端默认占位
|
||
"description": a.get("description", ""),
|
||
"call_mode": a.get("call_mode", "auto"),
|
||
"external_callable": bool(a.get("external_callable")),
|
||
"online": online,
|
||
"is_own": uid == caller_id,
|
||
})
|
||
|
||
result.append({
|
||
"user_id": uid,
|
||
"name": profile.get("name") or profile.get("nickname") or profile.get("username") or uid,
|
||
"avatar": profile.get("avatar", ""),
|
||
"is_admin": bool(profile.get("is_admin")) or (uid == caller_id and is_admin),
|
||
"agents": agent_items,
|
||
})
|
||
|
||
return result
|