diff --git a/src/pineagents/app/routers/__init__.py b/src/pineagents/app/routers/__init__.py index 08ac5ca..5932fe8 100644 --- a/src/pineagents/app/routers/__init__.py +++ b/src/pineagents/app/routers/__init__.py @@ -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: diff --git a/src/pineagents/app/routers/agent_gate.py b/src/pineagents/app/routers/agent_gate.py index ec0c744..3b07899 100644 --- a/src/pineagents/app/routers/agent_gate.py +++ b/src/pineagents/app/routers/agent_gate.py @@ -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 diff --git a/src/pineagents/app/routers/opc.py b/src/pineagents/app/routers/opc.py index 2d26949..5859395 100644 --- a/src/pineagents/app/routers/opc.py +++ b/src/pineagents/app/routers/opc.py @@ -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