# -*- coding: utf-8 -*- """智能体互通编排(server-core 权威)。 三层权限模型(全部在服务端集中校验,客户端不可绕过): - **L1 企业管理员无条件调用**:企业管理员(company_members.is_admin/member_type∈{admin,owner}) 可调用**本企业任意成员**的智能体,无需授权记录。 - **L2 企业内部智能体互调**:同一企业内的账号可互相调用彼此的智能体(企业=信任域)。 - **L3 成员开放调用(临时授权)**:成员 A 通过 agent_access_grants 临时授权成员 B 调用 A 的智能体(限时 / 限次 / 限 prompt 前缀 scope)。 通用约束:目标 agent 的 ``external_callable=False`` 时拒绝一切外部调用(agent 归属用户 可完全关闭外部调用);``call_mode=confirm`` 时由客户端在执行前向归属用户请求确认。 """ from __future__ import annotations import logging from datetime import datetime, timezone from ..infrastructure.repositories import Database log = logging.getLogger("agent_gate") AGENT_TOPIC_PREFIX = "opc/agent/v1" # 归属用户本人调用自己的智能体,永远允许 SELF_ALLOWED = True def parse_agent_addr(addr: str) -> tuple[str, str] | None: """解析 ``{user_id}.{agent_id}`` → (user_id, agent_id);非法返回 None。""" if not addr or "." not in addr: return None user_id, agent_id = addr.split(".", 1) if not user_id or not agent_id: return None return user_id, agent_id def _is_expired(expires_at: str) -> bool: if not expires_at: return False try: exp = datetime.fromisoformat(str(expires_at).replace("Z", "+00:00")) if exp.tzinfo is None: exp = exp.replace(tzinfo=timezone.utc) return exp < datetime.now(timezone.utc) except Exception: # noqa: BLE001 return True # 无法解析视为已过期(保守) def _scope_allows(scope: str, prompt: str) -> bool: """scope 为 prompt 前缀白名单(每行一条前缀);空=全部允许。""" if not scope: return True for line in scope.splitlines(): prefix = line.strip() if prefix and prompt.startswith(prefix): return True return False async def authorize_call(db: Database, caller_user_id: str, target_addr: str, prompt: str = "") -> tuple[bool, str]: """校验调用权限。返回 (ok, reason)。 优先级:自己 → L1 管理员 → L2 同企业 → L3 显式授权 → 拒绝。 """ parsed = parse_agent_addr(target_addr) if parsed is None: return False, "目标智能体地址无效" target_user_id, agent_id = parsed # 自己调自己的智能体 if SELF_ALLOWED and caller_user_id == target_user_id: return True, "" # 目标 agent 是否允许外部调用 agent = await db.agents.get(agent_id, target_user_id, port="opc") if agent is None: # 未注册到平台(可能未登录过桌面端),视为不可调用 return False, "目标智能体不存在或未启用" if not agent.get("external_callable", True): return False, "该智能体已关闭外部调用" caller_companies = await db.company_members.list_by_user(caller_user_id) if not caller_companies: return False, "无调用权限(非企业成员)" target_companies = await db.company_members.list_by_user(target_user_id) target_company_ids = {c["company_id"] for c in target_companies if c.get("status", "active") == "active"} for cm in caller_companies: if cm.get("status", "active") != "active": continue company_id = cm["company_id"] # L1:企业管理员 → 本企业成员 agent is_admin = bool(cm.get("is_admin")) or cm.get("member_type") in ("admin", "owner") if is_admin and company_id in target_company_ids: return True, "" # L2:同一企业互调 if company_id in target_company_ids: return True, "" # L3:显式授权 grant = await db.agent_access_grants.get_active( grantor_user_id=target_user_id, target_agent_addr=target_addr, grantee_user_id=caller_user_id, ) if grant is None: return False, "无调用权限(需要企业管理员或临时授权)" if _is_expired(grant.expires_at): return False, "授权已过期" if grant.max_calls > 0 and (grant.used_calls or 0) >= grant.max_calls: return False, "授权调用次数已用完" if not _scope_allows(grant.scope, prompt): return False, "调用内容超出授权范围" # 计数(派发即计数) await db.agent_access_grants.consume_call(grant) return True, "" async def handle_agent_task_event( db: Database, im, *, conv_id: str, sender_id: str, sender_name: str, content: str, metadata: dict | None, ) -> dict: """处理 im-service 的 agent_task 消息事件(管理员/成员在 IM 里发起调用)。 流程:解析地址 → 权限校验 → 目标 agent 存在性 → 在线查询 → 在线则派发 invoke(im 发布 MQTT),离线则回写 offline 回执。 """ metadata = metadata or {} target_addr = str(metadata.get("target_agent_addr") or metadata.get("target_addr") or "").strip() if not target_addr: return {"ok": False, "reason": "缺少目标智能体地址(target_agent_addr)"} parsed = parse_agent_addr(target_addr) if parsed is None: return {"ok": False, "reason": "目标智能体地址无效"} target_user_id, agent_id = parsed prompt = str(content or metadata.get("prompt") or "").strip() if not prompt: return {"ok": False, "reason": "缺少调用内容"} ok, reason = await authorize_call(db, sender_id, target_addr, prompt) if not ok: # 无权限:向会话回写一条 agent_reply 状态消息 try: await im.internal_agent_reply(conv_id, "", "error", f"调用被拒绝:{reason}") except Exception: # noqa: BLE001 log.warning("回写拒绝消息失败 conv=%s", conv_id) return {"ok": False, "reason": reason} # 在线查询 online_info: dict = {} try: online_info = await im.internal_agent_online(target_addr) except Exception: # noqa: BLE001 log.warning("agent 在线查询失败 addr=%s", target_addr) online = bool(online_info.get("online")) if not online: try: await im.internal_agent_reply( conv_id, "", "offline", f"成员智能体当前不在线,无法执行(目标:{target_addr})", ) except Exception: # noqa: BLE001 log.warning("回写离线消息失败 conv=%s", conv_id) return {"ok": False, "reason": "offline"} # 组装 invoke payload(复用 inter-agent 协议) payload = { "v": 1, "caller": { "user_id": sender_id, "agent_id": metadata.get("caller_agent_id", ""), "name": sender_name or sender_id, }, "session_id": metadata.get("session_id", "") or "", "prompt": prompt, "context": { "root_session_id": metadata.get("root_session_id", ""), "company_id": metadata.get("company_id", ""), }, "mode": metadata.get("mode", "auto"), } try: result = await im.internal_agent_invoke(target_addr, payload, conv_id) except Exception as exc: # noqa: BLE001 log.warning("agent invoke 派发失败 addr=%s: %s", target_addr, exc) try: await im.internal_agent_reply(conv_id, "", "error", f"任务派发失败:{exc}") except Exception: # noqa: BLE001 pass return {"ok": False, "reason": str(exc)} return {"ok": True, "task_id": result.get("task_id", "")}