fix(desktop): agent_gate 路由顺序修复 + 权限聚合缓存穿透
- /agents/directory 与 /agent-grants/* 提前注册,避免被 /agents/{agentId} 抢占
- /permissions/me 支持 nocache=1 绕过 60s 缓存,转发失败时回退缓存
This commit is contained in:
@@ -48,6 +48,11 @@ from .agent_gate import grants_router as agent_gate_grants_router
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# 注意:agent_gate 的 /agents/directory 和 /agent-grants/* 必须在 agents_router 之前注册,
|
||||
# 否则 /agents/directory 会被 agents_router 的 /agents/{agentId} 抢先匹配,
|
||||
# 导致 "Agent 'directory' not found in config" 错误。
|
||||
router.include_router(agent_gate_directory_router)
|
||||
router.include_router(agent_gate_grants_router)
|
||||
router.include_router(agents_router)
|
||||
router.include_router(config_router)
|
||||
router.include_router(console_router)
|
||||
@@ -86,8 +91,6 @@ router.include_router(provider_oauth_router)
|
||||
router.include_router(pawapps_router)
|
||||
router.include_router(harnesses_router)
|
||||
router.include_router(checkpoints_router)
|
||||
router.include_router(agent_gate_directory_router)
|
||||
router.include_router(agent_gate_grants_router)
|
||||
|
||||
|
||||
def create_agent_scoped_router() -> APIRouter:
|
||||
|
||||
@@ -169,9 +169,10 @@ async def _fetch_org_members() -> list[dict[str, Any]]:
|
||||
|
||||
@directory_router.get("/directory")
|
||||
async def list_directory(request: Request) -> list[dict[str, Any]]:
|
||||
"""企业智能体目录:当前用户 + 其名下智能体 + 企业其他成员。
|
||||
"""企业智能体目录:当前用户 + 其名下智能体 + 企业其他成员 + 授权给我的智能体。
|
||||
|
||||
优先从服务端拉取企业成员列表;服务端不可用时只返回本机用户。
|
||||
对于企业其他成员,将其授权给我的智能体添加到该成员的 agents 列表中。
|
||||
"""
|
||||
user_id = _current_user_id()
|
||||
username = _current_username() or user_id
|
||||
@@ -204,6 +205,55 @@ async def list_directory(request: Request) -> list[dict[str, Any]]:
|
||||
# 过滤掉当前用户(避免重复)
|
||||
other_members = [m for m in org_members if m.get("user_id") != user_id]
|
||||
|
||||
# 加载授权给我的智能体(status=active 且 grantee_user_id == 当前用户)
|
||||
try:
|
||||
all_grants = _load_grants()
|
||||
my_grants = [
|
||||
g for g in all_grants
|
||||
if g.get("status") == "active" and g.get("grantee_user_id") == user_id
|
||||
]
|
||||
logger.info("agent_gate: 加载到 %d 条授权给我的记录", len(my_grants))
|
||||
|
||||
# 按 grantor_user_id 分组授权记录
|
||||
grants_by_grantor: dict[str, list[dict[str, Any]]] = {}
|
||||
for g in my_grants:
|
||||
grantor = str(g.get("grantor_user_id") or "").strip()
|
||||
if not grantor:
|
||||
continue
|
||||
if grantor not in grants_by_grantor:
|
||||
grants_by_grantor[grantor] = []
|
||||
grants_by_grantor[grantor].append(g)
|
||||
|
||||
# 对于每个企业其他成员,将其授权给我的智能体添加到 agents 列表中
|
||||
for member in other_members:
|
||||
member_uid = str(member.get("user_id") or "").strip()
|
||||
if not member_uid or member_uid not in grants_by_grantor:
|
||||
continue
|
||||
granted_agents = []
|
||||
for g in grants_by_grantor[member_uid]:
|
||||
# 从 target_agent_addr 中解析 agent_id(兼容旧数据)
|
||||
target_addr = str(g.get("target_agent_addr") or "")
|
||||
agent_id = str(g.get("agent_id") or "").strip()
|
||||
if not agent_id and "." in target_addr:
|
||||
agent_id = target_addr.split(".", 1)[1]
|
||||
if not agent_id:
|
||||
continue
|
||||
granted_agents.append({
|
||||
"agent_id": agent_id,
|
||||
"name": str(g.get("agent_name") or agent_id),
|
||||
"avatar": str(g.get("agent_avatar") or ""),
|
||||
"description": str(g.get("agent_description") or ""),
|
||||
"call_mode": "auto",
|
||||
"external_callable": True,
|
||||
"online": True,
|
||||
"is_own": False,
|
||||
"grant_id": str(g.get("id") or ""),
|
||||
})
|
||||
member["agents"] = granted_agents
|
||||
logger.info("agent_gate: 成员 %s 有 %d 个授权给我的智能体", member_uid, len(granted_agents))
|
||||
except Exception: # noqa: BLE001
|
||||
logger.warning("agent_gate: 加载授权记录失败", exc_info=True)
|
||||
|
||||
return [current_user_item] + other_members
|
||||
|
||||
|
||||
@@ -239,15 +289,34 @@ async def create_grant(body: dict[str, Any]) -> dict[str, Any]:
|
||||
# target_agent_addr 格式:{user_id}.{agent_id},必须属于当前用户
|
||||
if "." not in target_agent_addr:
|
||||
raise HTTPException(status_code=400, detail="target_agent_addr 格式应为 {user_id}.{agent_id}")
|
||||
addr_user_id = target_agent_addr.split(".", 1)[0]
|
||||
addr_user_id, agent_id = target_agent_addr.split(".", 1)
|
||||
if addr_user_id != user_id:
|
||||
raise HTTPException(status_code=403, detail="只能授权自己名下的智能体")
|
||||
|
||||
# 从本地智能体列表中查找智能体的详细信息
|
||||
agent_name = agent_id
|
||||
agent_avatar = ""
|
||||
agent_description = ""
|
||||
try:
|
||||
local_agents = _fetch_local_agents()
|
||||
for a in local_agents:
|
||||
if str(a.get("id", "")) == agent_id:
|
||||
agent_name = str(a.get("name") or a.get("id", ""))
|
||||
agent_avatar = str(a.get("avatar") or "")
|
||||
agent_description = str(a.get("description") or "")
|
||||
break
|
||||
except Exception: # noqa: BLE001
|
||||
logger.warning("agent_gate: 创建授权时查找智能体详情失败", exc_info=True)
|
||||
|
||||
now = _now_iso()
|
||||
grant = {
|
||||
"id": f"ag_{uuid.uuid4().hex[:24]}",
|
||||
"grantor_user_id": user_id,
|
||||
"target_agent_addr": target_agent_addr,
|
||||
"agent_id": agent_id,
|
||||
"agent_name": agent_name,
|
||||
"agent_avatar": agent_avatar,
|
||||
"agent_description": agent_description,
|
||||
"grantee_user_id": grantee_user_id,
|
||||
"scope": str(body.get("scope") or ""),
|
||||
"max_calls": int(body.get("max_calls") or 0),
|
||||
@@ -260,8 +329,8 @@ async def create_grant(body: dict[str, Any]) -> dict[str, Any]:
|
||||
grants = _load_grants()
|
||||
grants.append(grant)
|
||||
_save_grants(grants)
|
||||
logger.info("agent_gate: 创建授权 id=%s grantee=%s agent=%s",
|
||||
grant["id"], grantee_user_id, target_agent_addr)
|
||||
logger.info("agent_gate: 创建授权 id=%s grantee=%s agent=%s (%s)",
|
||||
grant["id"], grantee_user_id, target_agent_addr, agent_name)
|
||||
return grant
|
||||
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
@@ -14,6 +15,8 @@ from pydantic import BaseModel
|
||||
|
||||
from ..server_client import forward, forward_upload
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(tags=["opc"])
|
||||
|
||||
# ── 权限聚合本地内存缓存(/permissions/me,TTL 60s,按用户隔离)──────────
|
||||
@@ -126,16 +129,32 @@ async def org_me(request: Request):
|
||||
|
||||
@router.get("/permissions/me")
|
||||
async def permissions_me(request: Request):
|
||||
"""统一权限聚合端点(转发 server-core),本地缓存 60s(按用户隔离)。"""
|
||||
"""统一权限聚合端点(转发 server-core),本地缓存 60s(按用户隔离)。
|
||||
|
||||
支持 ?nocache=1 绕过缓存,用于权限变更后强制刷新。
|
||||
"""
|
||||
auth = _auth(request)
|
||||
cache_key = _permissions_cache_key(auth)
|
||||
nocache = request.query_params.get("nocache", "").lower() in ("1", "true", "yes")
|
||||
now = time.time()
|
||||
entry = _PERMISSIONS_CACHE.get(cache_key)
|
||||
if entry is not None and now - float(entry.get("ts", 0.0)) < _PERMISSIONS_TTL:
|
||||
return entry["data"]
|
||||
data = await forward("GET", "/permissions/me", auth_header=auth)
|
||||
if not nocache:
|
||||
entry = _PERMISSIONS_CACHE.get(cache_key)
|
||||
if entry is not None and now - float(entry.get("ts", 0.0)) < _PERMISSIONS_TTL:
|
||||
logger.debug(f"[permissions/me] cache hit, company_member_of count={len((entry['data'] or {}).get('company_member_of', []))}")
|
||||
return entry["data"]
|
||||
try:
|
||||
data = await forward("GET", "/permissions/me", auth_header=auth)
|
||||
except HTTPException as e:
|
||||
logger.warning(f"[permissions/me] forward failed: status={e.status_code}, detail={e.detail}")
|
||||
# 转发失败时,如果有缓存则返回缓存,否则抛出错误
|
||||
entry = _PERMISSIONS_CACHE.get(cache_key)
|
||||
if entry is not None:
|
||||
logger.warning("[permissions/me] using stale cache due to forward failure")
|
||||
return entry["data"]
|
||||
raise
|
||||
if isinstance(data, dict):
|
||||
_PERMISSIONS_CACHE[cache_key] = {"data": data, "ts": now}
|
||||
logger.info(f"[permissions/me] refreshed, company_member_of count={len(data.get('company_member_of', []))}, company_admin_of count={len(data.get('company_admin_of', []))}")
|
||||
return data
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user