# -*- coding: utf-8 -*- """智能体管理端点(运营端):官方预置智能体 + 全局最高优先级提示词(fixed_soul)。 - 官方预置智能体:由管理后台配置(official_agents 表,全局共享、非 per-user), 替代原硬编码 ``AGENT_SEED``;桌面端每次登录经 ``/agent-bootstrap`` 拉取并同步。 - 全局提示词:存 ``system_configs``(agent.fixed_soul.zh / agent.fixed_soul.en), ``/agent-templates`` 优先返回该配置(未配置时回退服务端默认模板文件)。 全部要求 operator 角色 + ``menu:admin_agents`` 权限,并写审计日志。 """ from __future__ import annotations import json as _json from fastapi import APIRouter, Depends, HTTPException, Request from pydantic import BaseModel from ..dependencies import get_db from ...rbac import require_permission, write_audit from ...infrastructure.repositories import Database, new_id from .templates import FIXED_SOUL_CONFIG_PREFIX router = APIRouter(prefix="/admin/agents", tags=["admin-agents"]) TEMPLATE_TYPES = ("common", "qa", "local") # 身份级别(控制下发到哪些端):opc=桌面端 / park=园区端 / operator=运营端 / all=全部 SCOPES = ("opc", "park", "operator", "all") DOC_KEYS = ("soul", "profile", "heartbeat", "memory") def _normalize_scope(scope: str) -> str: """规范化 scope:逗号分隔去重,非法值抛 400。""" parts = [] for raw in scope.split(","): s = raw.strip() if not s: continue if s not in SCOPES: raise HTTPException( status_code=400, detail=f"scope 取值须为 {SCOPES} 之一(逗号分隔多选)", ) if s == "all": return "all" if s not in parts: parts.append(s) return ",".join(parts) or "opc" def _dump_json_field(value: str | dict | None, field: str) -> str: """把文档配置字段规范化为 JSON 字符串(仅保留 user/server 两个键)。""" if isinstance(value, str): if not value.strip(): return "{}" try: value = _json.loads(value) except (ValueError, TypeError): raise HTTPException(status_code=400, detail=f"{field} 须为 JSON 对象 {{user, server}}") if value is None: value = {} if not isinstance(value, dict): raise HTTPException(status_code=400, detail=f"{field} 须为 JSON 对象 {{user, server}}") return _json.dumps( { "user": str(value.get("user", "") or ""), "server": str(value.get("server", "") or ""), }, ensure_ascii=False, ) class OfficialAgentUpsertRequest(BaseModel): name: str = "" description: str = "" language: str = "zh" model_name: str = "" template_type: str = "common" deletable: bool = True use_fixed_soul: bool = False scope: str = "opc" soul: str | dict = "{}" profile: str | dict = "{}" heartbeat: str | dict = "{}" memory: str | dict = "{}" enabled: bool = True sort_order: int = 0 class FixedSoulRequest(BaseModel): zh: str = "" en: str = "" # ── 官方预置智能体 ──────────────────────────────────────────────────────── @router.get("/official", summary="官方预置智能体列表") async def list_official_agents( db: Database = Depends(get_db), _u: dict = Depends(require_permission("menu:admin_agents")), ): return await db.official_agents.all() @router.post("/official", summary="创建官方预置智能体") async def create_official_agent( req: OfficialAgentUpsertRequest, request: Request, db: Database = Depends(get_db), actor: dict = Depends(require_permission("menu:admin_agents")), ): if not req.name.strip(): raise HTTPException(status_code=400, detail="智能体名称不能为空") if req.template_type not in TEMPLATE_TYPES: raise HTTPException(status_code=400, detail=f"template_type 须为 {TEMPLATE_TYPES} 之一") scope = _normalize_scope(req.scope) agent = await db.official_agents.create( new_id("official_agent"), req.name, description=req.description, language=req.language, model_name=req.model_name, template_type=req.template_type, deletable=req.deletable, use_fixed_soul=req.use_fixed_soul, scope=scope, soul=_dump_json_field(req.soul, "soul"), profile=_dump_json_field(req.profile, "profile"), heartbeat=_dump_json_field(req.heartbeat, "heartbeat"), memory=_dump_json_field(req.memory, "memory"), enabled=req.enabled, sort_order=req.sort_order, ) await write_audit(db, action="agent.official.create", resource="official_agent", resource_id=agent["id"], detail=req.name, user=actor, request=request) return agent @router.put("/official/{agent_id}", summary="更新官方预置智能体") async def update_official_agent( agent_id: str, req: OfficialAgentUpsertRequest, request: Request, db: Database = Depends(get_db), actor: dict = Depends(require_permission("menu:admin_agents")), ): if req.template_type not in TEMPLATE_TYPES: raise HTTPException(status_code=400, detail=f"template_type 须为 {TEMPLATE_TYPES} 之一") payload = req.model_dump() payload["scope"] = _normalize_scope(req.scope) for k in DOC_KEYS: payload[k] = _dump_json_field(payload.get(k), k) agent = await db.official_agents.update(agent_id, payload) if agent is None: raise HTTPException(status_code=404, detail="官方智能体不存在") await write_audit(db, action="agent.official.update", resource="official_agent", resource_id=agent_id, detail=req.name, user=actor, request=request) return agent @router.delete("/official/{agent_id}", summary="删除官方预置智能体") async def delete_official_agent( agent_id: str, request: Request, db: Database = Depends(get_db), actor: dict = Depends(require_permission("menu:admin_agents")), ): ok = await db.official_agents.delete(agent_id) if not ok: raise HTTPException(status_code=404, detail="官方智能体不存在") await write_audit(db, action="agent.official.delete", resource="official_agent", resource_id=agent_id, detail=agent_id, user=actor, request=request) return {"ok": True} # ── 全局最高优先级提示词(fixed_soul) ───────────────────────────────────── @router.get("/fixed-soul", summary="全局最高优先级提示词") async def get_fixed_soul( db: Database = Depends(get_db), _u: dict = Depends(require_permission("menu:admin_agents")), ): return { "zh": await db.config.get(f"{FIXED_SOUL_CONFIG_PREFIX}.zh") or "", "en": await db.config.get(f"{FIXED_SOUL_CONFIG_PREFIX}.en") or "", } @router.put("/fixed-soul", summary="更新全局最高优先级提示词") async def set_fixed_soul( req: FixedSoulRequest, request: Request, db: Database = Depends(get_db), actor: dict = Depends(require_permission("menu:admin_agents")), ): await db.config.set(f"{FIXED_SOUL_CONFIG_PREFIX}.zh", req.zh, "全局最高优先级提示词(zh)") await db.config.set(f"{FIXED_SOUL_CONFIG_PREFIX}.en", req.en, "全局最高优先级提示词(en)") await write_audit(db, action="agent.fixed_soul.update", resource="system_config", resource_id="agent.fixed_soul", detail="zh/en", user=actor, request=request) return {"ok": True}