198 lines
7.6 KiB
Python
198 lines
7.6 KiB
Python
# -*- 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}
|