fix(desktop): agent_gate 路由顺序修复 + 权限聚合缓存穿透

- /agents/directory 与 /agent-grants/* 提前注册,避免被 /agents/{agentId} 抢占
- /permissions/me 支持 nocache=1 绕过 60s 缓存,转发失败时回退缓存
This commit is contained in:
Pine
2026-09-13 11:14:37 +08:00
parent edc4400fa4
commit ef703ae4fa
3 changed files with 102 additions and 11 deletions
+5 -2
View File
@@ -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:
+73 -4
View File
@@ -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
+24 -5
View File
@@ -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/meTTL 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