diff --git a/app/api/routers/agent_gate.py b/app/api/routers/agent_gate.py index eb18a6c..d03dc9d 100644 --- a/app/api/routers/agent_gate.py +++ b/app/api/routers/agent_gate.py @@ -104,3 +104,116 @@ async def revoke_grant(grant_id: str, user: dict = Depends(get_current_user), 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