Files
server-core/app/api/routers/rbac_agents.py
T

198 lines
7.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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}