Phase 2.3: Agent Management Module

Backend:
- agents_admin.py: 10 REST API endpoints for AI Agent CRUD
  - GET/POST/PUT/DELETE /admin/agents/
  - Stats overview, reviews, available skills/models
  - Agent = MarketApp with type='agent'

Frontend:
- Agents.tsx: Full admin page (780 lines)
  - Stats dashboard, filterable table, create/edit modal
  - Config: system prompt, temperature, model, linked skills
- api/index.ts: 10 new Agent API methods
- App.tsx: /admin/agents route
- AdminLayout.tsx: Agent management nav item
This commit is contained in:
qyyfw committed 2026-07-02 20:18:10 +08:00
1 parent 70dd2b69af
commit a682dc687e
11 files changed
+4243 -1615

No files matched your search

@@ -0,0 +1,553 @@
"""
Agent 管理 API(管理员)
Phase 2.3: AI PaaS Agent Management
"""
from fastapi import APIRouter, Depends, HTTPException, Query, Body
from sqlalchemy.orm import Session
from sqlalchemy import or_, func
from pydantic import BaseModel
from typing import Optional, List
from ...database import get_db
from ...models import MarketApp, EvolutionSkill, AIModel, User
from ...auth import get_current_admin
from ...middleware.log_service import LogService
import uuid
from datetime import datetime
router = APIRouter()
# ============================================================
# Pydantic 模型
# ============================================================
class AgentConfig(BaseModel):
system_prompt: Optional[str] = None
temperature: Optional[float] = None
max_tokens: Optional[int] = None
model_id: Optional[str] = None
skills: Optional[List[str]] = None # skill IDs
tools: Optional[List[str]] = None # MCP server IDs
memory_enabled: Optional[bool] = False
reasoning_enabled: Optional[bool] = False
class AgentCreate(BaseModel):
name: str
description: Optional[str] = None
icon: Optional[str] = None
price: float = 0.0
is_member_free: bool = False
config: Optional[AgentConfig] = None
class AgentUpdate(BaseModel):
name: Optional[str] = None
description: Optional[str] = None
icon: Optional[str] = None
price: Optional[float] = None
is_member_free: Optional[bool] = None
config: Optional[AgentConfig] = None
status: Optional[str] = None # active/inactive
tutorial: Optional[str] = None
# ============================================================
# 辅助函数
# ============================================================
def agent_to_dict(a: MarketApp) -> dict:
"""将 MarketApp (agent) 转换为字典"""
# 解析 skill_content 中的配置
import json
config = {}
if a.skill_content:
try:
config = json.loads(a.skill_content)
except Exception:
pass
return {
"id": a.id,
"name": a.name,
"type": a.type,
"source": a.source,
"publisher_id": a.publisher_id,
"publisher_name": a.publisher_name,
"description": a.description,
"icon": a.icon,
"tutorial": a.tutorial,
"price": float(a.price) if a.price else 0,
"is_member_free": a.is_member_free,
"join_promotion": a.join_promotion,
"status": a.status,
"rating_avg": float(a.rating_avg) if a.rating_avg else 0,
"review_count": a.review_count,
"install_count": a.install_count,
"agent_endpoint": a.agent_endpoint,
"skill_content": a.skill_content,
"config": config,
"created_at": a.created_at.isoformat() if a.created_at else None,
"updated_at": a.updated_at.isoformat() if a.updated_at else None,
}
# ============================================================
# 端点
# ============================================================
@router.get("/", summary="获取 Agent 列表")
async def list_agents(
keyword: str = Query(None, description="搜索关键词"),
status: str = Query(None, description="状态: pending/published/removed"),
source: str = Query(None, description="来源: official/user"),
page: int = Query(1, ge=1),
page_size: int = Query(15, ge=1, le=100),
admin_user=Depends(get_current_admin),
db: Session = Depends(get_db),
):
"""获取所有 Agent 列表(分页、筛选)"""
query = db.query(MarketApp).filter(MarketApp.type == "agent")
if keyword:
query = query.filter(
or_(
MarketApp.name.ilike(f"%{keyword}%"),
MarketApp.description.ilike(f"%{keyword}%"),
MarketApp.publisher_name.ilike(f"%{keyword}%"),
)
)
if status:
query = query.filter(MarketApp.status == status)
if source:
query = query.filter(MarketApp.source == source)
total = query.count()
agents = query.order_by(MarketApp.created_at.desc()).offset(
(page - 1) * page_size
).limit(page_size).all()
return {
"code": 0,
"data": {
"total": total,
"page": page,
"page_size": page_size,
"items": [agent_to_dict(a) for a in agents],
}
}
@router.get("/stats/overview", summary="Agent 统计概览")
async def get_agent_stats(
admin_user=Depends(get_current_admin),
db: Session = Depends(get_db),
):
"""获取 Agent 统计数据"""
base = db.query(MarketApp).filter(MarketApp.type == "agent")
total = base.count()
published = base.filter(MarketApp.status == "published").count()
pending = base.filter(MarketApp.status == "pending").count()
official = base.filter(MarketApp.source == "official").count()
user_created = base.filter(MarketApp.source == "user").count()
# 安装量统计
install_base = db.query(func.sum(MarketApp.install_count)).filter(
MarketApp.type == "agent"
).scalar() or 0
# 按类型分布
by_source = {
"official": official,
"user": user_created,
}
# 评分统计
avg_rating = db.query(func.avg(MarketApp.rating_avg)).filter(
MarketApp.type == "agent",
MarketApp.rating_avg > 0,
).scalar() or 0
return {
"code": 0,
"data": {
"total_agents": total,
"published_agents": published,
"pending_agents": pending,
"official_agents": official,
"user_agents": user_created,
"total_installs": int(install_base),
"average_rating": round(float(avg_rating), 2),
"by_source": by_source,
}
}
@router.get("/{agent_id}", summary="获取 Agent 详情")
async def get_agent(
agent_id: str,
admin_user=Depends(get_current_admin),
db: Session = Depends(get_db),
):
"""获取单个 Agent 的详细信息"""
agent = db.query(MarketApp).filter(
MarketApp.id == agent_id,
MarketApp.type == "agent",
).first()
if not agent:
raise HTTPException(status_code=404, detail="Agent 不存在")
result = agent_to_dict(agent)
# 获取关联的 Skills
import json
config = json.loads(agent.skill_content or "{}")
skill_ids = config.get("skills", [])
if skill_ids:
skills = db.query(EvolutionSkill).filter(
EvolutionSkill.id.in_(skill_ids)
).all()
result["linked_skills"] = [
{
"id": s.id,
"skill_name": s.skill_name,
"state": s.state,
"version": s.version,
}
for s in skills
]
else:
result["linked_skills"] = []
# 获取可用模型列表
models = db.query(AIModel).filter(AIModel.is_enabled == True).all()
result["available_models"] = [
{"id": m.id, "name": m.name, "provider": m.provider} for m in models
]
return {"code": 0, "data": result}
@router.post("/", summary="创建 Agent")
async def create_agent(
req: AgentCreate,
admin_user=Depends(get_current_admin),
db: Session = Depends(get_db),
):
"""创建新的 Agent"""
import json
# 检查名称是否已存在
existing = db.query(MarketApp).filter(
MarketApp.name == req.name,
MarketApp.type == "agent",
).first()
if existing:
raise HTTPException(status_code=400, detail="Agent 名称已存在")
# 构建 skill_content
config = {}
if req.config:
cfg = req.config
config = {
"system_prompt": cfg.system_prompt or "",
"temperature": cfg.temperature or 0.7,
"max_tokens": cfg.max_tokens or 2048,
"model_id": cfg.model_id,
"skills": cfg.skills or [],
"tools": cfg.tools or [],
"memory_enabled": cfg.memory_enabled or False,
"reasoning_enabled": cfg.reasoning_enabled or False,
}
agent = MarketApp(
id=str(uuid.uuid4()),
name=req.name,
type="agent",
source="official",
publisher_id=admin_user.id,
publisher_name=admin_user.username,
description=req.description or "",
icon=req.icon or "",
price=req.price,
is_member_free=req.is_member_free,
skill_content=json.dumps(config, ensure_ascii=False),
status="published",
created_at=datetime.now(),
updated_at=datetime.now(),
)
db.add(agent)
db.commit()
db.refresh(agent)
LogService.log_audit(
admin_id=admin_user.id,
admin_name=admin_user.username,
action="create_agent",
target_type="agent",
target_id=agent.id,
detail=f"创建 Agent: {agent.name}",
)
return {"code": 0, "message": "Agent 创建成功", "data": agent_to_dict(agent)}
@router.put("/{agent_id}", summary="更新 Agent")
async def update_agent(
agent_id: str,
req: AgentUpdate,
admin_user=Depends(get_current_admin),
db: Session = Depends(get_db),
):
"""更新 Agent 配置"""
import json
agent = db.query(MarketApp).filter(
MarketApp.id == agent_id,
MarketApp.type == "agent",
).first()
if not agent:
raise HTTPException(status_code=404, detail="Agent 不存在")
# 更新基本字段
if req.name is not None:
# 检查新名称是否重复
dup = db.query(MarketApp).filter(
MarketApp.name == req.name,
MarketApp.type == "agent",
MarketApp.id != agent_id,
).first()
if dup:
raise HTTPException(status_code=400, detail="Agent 名称已存在")
agent.name = req.name
if req.description is not None:
agent.description = req.description
if req.icon is not None:
agent.icon = req.icon
if req.price is not None:
agent.price = req.price
if req.is_member_free is not None:
agent.is_member_free = req.is_member_free
if req.status is not None:
agent.status = req.status
if req.tutorial is not None:
agent.tutorial = req.tutorial
# 更新配置
if req.config is not None:
cfg = req.config
# 合并现有配置
existing_config = {}
if agent.skill_content:
try:
existing_config = json.loads(agent.skill_content)
except Exception:
pass
new_config = {
"system_prompt": cfg.system_prompt if cfg.system_prompt is not None else existing_config.get("system_prompt", ""),
"temperature": cfg.temperature if cfg.temperature is not None else existing_config.get("temperature", 0.7),
"max_tokens": cfg.max_tokens if cfg.max_tokens is not None else existing_config.get("max_tokens", 2048),
"model_id": cfg.model_id if cfg.model_id is not None else existing_config.get("model_id"),
"skills": cfg.skills if cfg.skills is not None else existing_config.get("skills", []),
"tools": cfg.tools if cfg.tools is not None else existing_config.get("tools", []),
"memory_enabled": cfg.memory_enabled if cfg.memory_enabled is not None else existing_config.get("memory_enabled", False),
"reasoning_enabled": cfg.reasoning_enabled if cfg.reasoning_enabled is not None else existing_config.get("reasoning_enabled", False),
}
agent.skill_content = json.dumps(new_config, ensure_ascii=False)
agent.updated_at = datetime.now()
db.commit()
db.refresh(agent)
LogService.log_audit(
admin_id=admin_user.id,
admin_name=admin_user.username,
action="update_agent",
target_type="agent",
target_id=agent_id,
detail=f"更新 Agent: {agent.name}",
)
return {"code": 0, "message": "Agent 已更新", "data": agent_to_dict(agent)}
@router.delete("/{agent_id}", summary="删除 Agent")
async def delete_agent(
agent_id: str,
admin_user=Depends(get_current_admin),
db: Session = Depends(get_db),
):
"""删除 Agent"""
agent = db.query(MarketApp).filter(
MarketApp.id == agent_id,
MarketApp.type == "agent",
).first()
if not agent:
raise HTTPException(status_code=404, detail="Agent 不存在")
agent_name = agent.name
db.delete(agent)
db.commit()
LogService.log_audit(
admin_id=admin_user.id,
admin_name=admin_user.username,
action="delete_agent",
target_type="agent",
target_id=agent_id,
detail=f"删除 Agent: {agent_name}",
)
return {"code": 0, "message": "Agent 已删除"}
@router.put("/{agent_id}/toggle", summary="启用/禁用 Agent")
async def toggle_agent(
agent_id: str,
is_active: bool = Query(..., description="是否启用"),
admin_user=Depends(get_current_admin),
db: Session = Depends(get_db),
):
"""启用或禁用 Agent"""
agent = db.query(MarketApp).filter(
MarketApp.id == agent_id,
MarketApp.type == "agent",
).first()
if not agent:
raise HTTPException(status_code=404, detail="Agent 不存在")
agent.status = "published" if is_active else "pending"
agent.updated_at = datetime.now()
db.commit()
LogService.log_audit(
admin_id=admin_user.id,
admin_name=admin_user.username,
action="toggle_agent",
target_type="agent",
target_id=agent_id,
detail=f"{'启用' if is_active else '禁用'} Agent: {agent.name}",
)
return {
"code": 0,
"message": f"Agent 已{'启用' if is_active else '禁用'}",
"data": agent_to_dict(agent),
}
@router.get("/{agent_id}/reviews", summary="获取 Agent 评价")
async def get_agent_reviews(
agent_id: str,
admin_user=Depends(get_current_admin),
db: Session = Depends(get_db),
):
"""获取 Agent 的用户评价"""
from ...models import AppReview, User
agent = db.query(MarketApp).filter(
MarketApp.id == agent_id,
MarketApp.type == "agent",
).first()
if not agent:
raise HTTPException(status_code=404, detail="Agent 不存在")
reviews = db.query(AppReview).filter(
AppReview.app_id == agent_id
).order_by(AppReview.created_at.desc()).limit(50).all()
return {
"code": 0,
"data": [
{
"id": r.id,
"user_id": r.user_id,
"rating": r.rating,
"content": r.content,
"created_at": r.created_at.isoformat() if r.created_at else None,
}
for r in reviews
]
}
@router.get("/available/skills", summary="获取可关联的 Skills")
async def get_available_skills(
keyword: str = Query(None),
state: str = Query(None, description="技能状态筛选"),
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100),
admin_user=Depends(get_current_admin),
db: Session = Depends(get_db),
):
"""获取可关联到 Agent 的 Skills 列表"""
query = db.query(EvolutionSkill)
if keyword:
query = query.filter(
or_(
EvolutionSkill.skill_name.ilike(f"%{keyword}%"),
EvolutionSkill.description.ilike(f"%{keyword}%"),
)
)
if state:
query = query.filter(EvolutionSkill.state == state)
total = query.count()
skills = query.order_by(EvolutionSkill.use_count.desc()).offset(
(page - 1) * page_size
).limit(page_size).all()
return {
"code": 0,
"data": {
"total": total,
"items": [
{
"id": s.id,
"skill_name": s.skill_name,
"description": s.description,
"state": s.state,
"version": s.version,
"use_count": s.use_count,
"category": s.category,
"tags": s.tags,
}
for s in skills
]
}
}
@router.get("/available/models", summary="获取可用模型列表")
async def get_available_models(
admin_user=Depends(get_current_admin),
db: Session = Depends(get_db),
):
"""获取可用于 Agent 的模型列表"""
models = db.query(AIModel).filter(
AIModel.is_enabled == True
).order_by(AIModel.name).all()
return {
"code": 0,
"data": [
{
"id": m.id,
"name": m.name,
"provider": m.provider,
"model_type": m.model_type,
"input_price": float(m.input_price) if m.input_price else 0,
"output_price": float(m.output_price) if m.output_price else 0,
}
for m in models
]
}