From a682dc687e315b16818ad365906bccf650d5b150 Mon Sep 17 00:00:00 2001 From: qyyfw Date: Thu, 2 Jul 2026 20:18:10 +0800 Subject: [PATCH] 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 --- backend/python/app/api/admin/agents_admin.py | 553 +++++++++ backend/python/app/api/admin/models.py | 152 +++ backend/python/app/api/admin/skills_admin.py | 773 ++++++++++++ backend/python/main.py | 429 +++---- frontend/src/App.tsx | 244 ++-- frontend/src/api/index.ts | 1035 +++++++++-------- .../src/components/layout/AdminLayout.tsx | 250 ++-- frontend/src/pages/admin/Agents.tsx | 702 +++++++++++ frontend/src/pages/admin/Models.tsx | 1034 ++++++---------- frontend/src/pages/admin/Skills.tsx | 667 +++++++++++ frontend/src/types/index.ts | 19 + 11 files changed, 4243 insertions(+), 1615 deletions(-) create mode 100644 backend/python/app/api/admin/agents_admin.py create mode 100644 backend/python/app/api/admin/skills_admin.py create mode 100644 frontend/src/pages/admin/Agents.tsx create mode 100644 frontend/src/pages/admin/Skills.tsx diff --git a/backend/python/app/api/admin/agents_admin.py b/backend/python/app/api/admin/agents_admin.py new file mode 100644 index 0000000..6efdb67 --- /dev/null +++ b/backend/python/app/api/admin/agents_admin.py @@ -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 + ] + } diff --git a/backend/python/app/api/admin/models.py b/backend/python/app/api/admin/models.py index 3711ac1..1fa74ce 100644 --- a/backend/python/app/api/admin/models.py +++ b/backend/python/app/api/admin/models.py @@ -600,3 +600,155 @@ async def get_model_stats( "total_calls": int(total_calls), "total_tokens": int(total_tokens), } + + +# ============================================================ +# 模型连接测试 API (Phase 2) +# ============================================================ + +@router.post("/{model_id}/test-connection") +async def test_model_connection( + model_id: str, + admin_user=Depends(get_current_admin), + db: Session = Depends(get_db), +): + """ + 测试指定模型的连接是否正常 + 支持从模型记录或提供商记录读取配置 + """ + logger = logging.getLogger(__name__) + logger.info(f"Testing connection for model_id: {model_id}") + + # 优先从 AIModel 读取 + ai_model = db.query(AIModel).filter(AIModel.id == model_id).first() + if not ai_model: + raise HTTPException(status_code=404, detail="模型不存在") + + provider = db.query(ModelProvider).filter(ModelProvider.id == ai_model.provider_id).first() + if not provider: + raise HTTPException(status_code=404, detail="模型供应商不存在") + + # 确定最终配置 + api_key = ai_model.api_key or (provider.api_key if provider else None) + api_url = ai_model.endpoint + + # 如果 endpoint 为空,尝试构造 + if not api_url and provider and provider.api_base_url: + base = provider.api_base_url.rstrip('/') + if provider.protocol == 'openai_compatible': + api_url = f"{base}/v1/chat/completions" + else: + api_url = f"{base}/v1/chat/completions" + + if not api_key: + return { + "success": False, + "message": "未配置 API Key,请在模型或供应商设置中添加", + "latency_ms": None, + } + if not api_url: + return { + "success": False, + "message": "未配置 API 地址,请在模型或供应商设置中添加", + "latency_ms": None, + } + + timeout = ai_model.timeout or 60 + import time + start = time.time() + + try: + async with httpx.AsyncClient(timeout=timeout) as client: + headers = { + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json", + } + # 使用 minimal payload 测试连接 + payload = { + "model": ai_model.name, + "messages": [{"role": "user", "content": "hi"}], + "max_tokens": 5, + } + resp = await client.post(api_url, json=payload, headers=headers) + latency = int((time.time() - start) * 1000) + + if resp.status_code == 200: + try: + data = resp.json() + model_used = data.get("model", ai_model.name) + return { + "success": True, + "message": f"连接成功,使用模型: {model_used}", + "latency_ms": latency, + "model_in_response": model_used, + } + except Exception: + return { + "success": True, + "message": f"连接成功 (HTTP {resp.status_code})", + "latency_ms": latency, + } + elif resp.status_code == 401: + return { + "success": False, + "message": "认证失败:API Key 无效或已过期", + "latency_ms": latency, + } + elif resp.status_code == 403: + return { + "success": False, + "message": "访问被拒绝:API Key 权限不足", + "latency_ms": latency, + } + elif resp.status_code == 429: + return { + "success": False, + "message": "触发限流:请稍后重试", + "latency_ms": latency, + } + else: + try: + err_data = resp.json() + err_msg = err_data.get("error", {}).get("message", resp.text[:200]) + except Exception: + err_msg = resp.text[:200] + return { + "success": False, + "message": f"API 错误 (HTTP {resp.status_code}): {err_msg}", + "latency_ms": latency, + } + except httpx.TimeoutException: + latency = int((time.time() - start) * 1000) + return { + "success": False, + "message": f"连接超时({timeout}s):请检查 API 地址是否正确,或服务是否可访问", + "latency_ms": latency, + } + except Exception as e: + latency = int((time.time() - start) * 1000) + logger.error(f"Model connection test failed: {e}") + return { + "success": False, + "message": f"连接失败: {str(e)}", + "latency_ms": latency, + } + + + timeout: int = 60 + status: str = "paid" + price_per_million_tokens: float = 0.0 + is_enabled: bool = True + + +class UpdateModelRequest(BaseModel): + name: Optional[str] = None + api_key: Optional[str] = None + endpoint: Optional[str] = None + capabilities: Optional[List[str]] = None + timeout: Optional[int] = None + status: Optional[str] = None + price_per_million_tokens: Optional[float] = None + is_enabled: Optional[bool] = None + + +from typing import Optional diff --git a/backend/python/app/api/admin/skills_admin.py b/backend/python/app/api/admin/skills_admin.py new file mode 100644 index 0000000..855759a --- /dev/null +++ b/backend/python/app/api/admin/skills_admin.py @@ -0,0 +1,773 @@ +""" +技能管理系统 API(管理员) +全平台技能管理:列表/创建/编辑/删除/状态管理/版本历史/发布市场 +""" +import os +import json +import uuid +import logging +from datetime import datetime +from typing import Optional, List + +from fastapi import APIRouter, Depends, HTTPException, Query +from sqlalchemy.orm import Session +from sqlalchemy import desc, func, or_, and_ + +from ...database import get_db +from ...models import ( + User, EvolutionSkill, SkillVersion, SkillPublishRecord, + EvolutionEvent, TeamSharedSkill +) +from ...auth import get_current_admin +from ...middleware.log_service import LogService +from ...config import UPLOAD_DIR + +router = APIRouter() +logger = logging.getLogger(__name__) + +# ============================================================ +# 技能状态定义 +# ============================================================ + +SKILL_STATES = { + "baby": {"label": "萌芽期", "color": "#9CA3AF", "description": "刚创建的技能,需要继续培养"}, + "main": {"label": "主力期", "color": "#10B981", "description": "已成熟可正常使用"}, + "staging": {"label": "灰度期", "color": "#F59E0B", "description": "正在灰度测试中"}, + "frozen": {"label": "已冻结", "color": "#EF4444", "description": "已停止使用"}, +} + +SKILL_CATEGORIES = [ + {"id": "writing", "label": "文字创作"}, + {"id": "coding", "label": "代码能力"}, + {"id": "data_analysis", "label": "数据分析"}, + {"id": "qa", "label": "知识问答"}, + {"id": "automation", "label": "流程自动化"}, + {"id": "creativity", "label": "创意生成"}, + {"id": "communication", "label": "沟通协作"}, + {"id": "reasoning", "label": "逻辑推理"}, + {"id": "other", "label": "其他"}, +] + + +# ============================================================ +# 辅助函数 +# ============================================================ + +def skill_to_dict(s: EvolutionSkill, include_md: bool = False) -> dict: + """将技能ORM对象转换为字典""" + result = { + "id": s.id, + "user_id": s.user_id, + "skill_name": s.skill_name, + "description": s.description, + "tags": s.tags or [], + "category": s.category, + "category_label": next((c["label"] for c in SKILL_CATEGORIES if c["id"] == s.category), s.category or "其他"), + "state": s.state, + "state_info": SKILL_STATES.get(s.state, SKILL_STATES["baby"]), + "use_count": s.use_count or 0, + "version": s.version or 1, + "skill_md_path": s.skill_md_path, + "git_commit_sha": s.git_commit_sha, + "last_used_at": s.last_used_at.isoformat() if s.last_used_at else None, + "created_at": s.created_at.isoformat() if s.created_at else None, + "updated_at": s.updated_at.isoformat() if s.updated_at else None, + } + + # 读取 SKILL.md 内容 + if include_md and s.skill_md_path and os.path.exists(s.skill_md_path): + try: + with open(s.skill_md_path, 'r', encoding='utf-8') as f: + result["skill_md_content"] = f.read() + except Exception: + result["skill_md_content"] = None + else: + result["skill_md_content"] = None + + return result + + +def version_to_dict(v: SkillVersion) -> dict: + return { + "id": v.id, + "user_id": v.user_id, + "skill_name": v.skill_name, + "version": v.version, + "skill_md_content": v.skill_md_content, + "git_commit_sha": v.git_commit_sha, + "description": v.description, + "created_at": v.created_at.isoformat() if v.created_at else None, + } + + +# ============================================================ +# 管理员技能列表 +# ============================================================ + +@router.get("/") +async def list_skills( + state: str = Query(None, description="技能状态过滤: baby/main/staging/frozen"), + category: str = Query(None, description="技能分类过滤"), + keyword: str = Query(None, description="关键词搜索(名称/描述)"), + user_id: str = Query(None, description="按用户ID筛选"), + sort_by: str = Query("updated_at", description="排序字段"), + order: str = Query("desc", description="排序方向: asc/desc"), + page: int = Query(1, ge=1, description="页码"), + page_size: int = Query(20, ge=1, le=100, description="每页数量"), + admin_user=Depends(get_current_admin), + db: Session = Depends(get_db), +): + """获取全平台技能列表(支持分页/过滤/搜索)""" + logger.info(f"[Admin Skills] {admin_user.username} 列出技能: state={state}, keyword={keyword}") + + query = db.query(EvolutionSkill) + + if state: + query = query.filter(EvolutionSkill.state == state) + if category: + query = query.filter(EvolutionSkill.category == category) + if keyword: + query = query.filter( + or_( + EvolutionSkill.skill_name.contains(keyword), + EvolutionSkill.description.contains(keyword) + ) + ) + if user_id: + query = query.filter(EvolutionSkill.user_id == user_id) + + # 总数 + total = query.count() + + # 排序 + sort_field = getattr(EvolutionSkill, sort_by, EvolutionSkill.updated_at) + if order == "desc": + query = query.order_by(desc(sort_field)) + else: + query = query.order_by(sort_field) + + # 分页 + skills = query.offset((page - 1) * page_size).limit(page_size).all() + + # 补充用户名 + user_ids = list(set(s.user_id for s in skills)) + users_map = { + u.id: u.username + for u in db.query(User.id, User.username).filter(User.id.in_(user_ids)).all() + } + + result = [] + for s in skills: + item = skill_to_dict(s) + item["owner_username"] = users_map.get(s.user_id, s.user_id) + result.append(item) + + return { + "code": 0, + "data": { + "items": result, + "total": total, + "page": page, + "page_size": page_size, + "pages": (total + page_size - 1) // page_size, + } + } + + +# ============================================================ +# 获取单个技能详情 +# ============================================================ + +@router.get("/{skill_id}") +async def get_skill( + skill_id: str, + admin_user=Depends(get_current_admin), + db: Session = Depends(get_db), +): + """获取技能详情(包含 SKILL.md 内容)""" + skill = db.query(EvolutionSkill).filter(EvolutionSkill.id == skill_id).first() + if not skill: + raise HTTPException(status_code=404, detail="技能不存在") + + result = skill_to_dict(skill, include_md=True) + + # 获取所属用户信息 + user = db.query(User).filter(User.id == skill.user_id).first() + if user: + result["owner_username"] = user.username + result["owner_email"] = user.email + + # 获取发布记录 + pub_records = db.query(SkillPublishRecord).filter( + SkillPublishRecord.skill_id == skill_id + ).order_by(desc(SkillPublishRecord.created_at)).limit(10).all() + result["publish_records"] = [{ + "id": p.id, + "price": float(p.price) if p.price else 0, + "market_description": p.market_description, + "status": p.status, + "published_at": p.published_at.isoformat() if p.published_at else None, + "created_at": p.created_at.isoformat() if p.created_at else None, + } for p in pub_records] + + # 获取最新版本号 + latest_version = db.query(func.max(SkillVersion.version)).filter( + SkillVersion.skill_id == skill_id + ).scalar() + result["latest_version"] = latest_version or skill.version + + return {"code": 0, "data": result} + + +# ============================================================ +# 创建技能 +# ============================================================ + +class CreateSkillRequest: + def __init__(self, skill_name: str = None, description: str = None, + tags: List[str] = None, category: str = None, + skill_md_content: str = None, user_id: str = None): + self.skill_name = skill_name + self.description = description + self.tags = tags or [] + self.category = category + self.skill_md_content = skill_md_content + self.user_id = user_id + + +from pydantic import BaseModel, Field + + +class CreateSkillSchema(BaseModel): + skill_name: str = Field(..., min_length=1, max_length=200, description="技能名称") + user_id: str = Field(..., description="所属用户ID") + description: Optional[str] = Field(None, description="技能描述") + tags: Optional[List[str]] = Field(default_factory=list, description="标签列表") + category: Optional[str] = Field(None, description="技能分类") + skill_md_content: Optional[str] = Field(None, description="SKILL.md 内容") + + +@router.post("/") +async def create_skill( + req: CreateSkillSchema, + admin_user=Depends(get_current_admin), + db: Session = Depends(get_db), +): + """管理员创建技能""" + logger.info(f"[Admin Skills] {admin_user.username} 创建技能: {req.skill_name}") + + # 检查技能名是否已存在 + existing = db.query(EvolutionSkill).filter( + EvolutionSkill.user_id == req.user_id, + EvolutionSkill.skill_name == req.skill_name + ).first() + if existing: + raise HTTPException(status_code=400, detail="该用户已存在同名技能") + + # 验证用户存在 + user = db.query(User).filter(User.id == req.user_id).first() + if not user: + raise HTTPException(status_code=400, detail="用户不存在") + + # 生成技能ID + skill_id = str(uuid.uuid4()) + + # 保存 SKILL.md 文件 + skill_md_path = None + if req.skill_md_content: + skill_dir = os.path.join(UPLOAD_DIR, "skills", req.user_id) + os.makedirs(skill_dir, exist_ok=True) + safe_name = req.skill_name.replace("/", "_").replace("\\", "_") + skill_md_path = os.path.join(skill_dir, f"{safe_name}.md") + try: + with open(skill_md_path, 'w', encoding='utf-8') as f: + f.write(req.skill_md_content) + except Exception as e: + logger.warning(f"保存 SKILL.md 失败: {e}") + + # 创建技能记录 + skill = EvolutionSkill( + id=skill_id, + user_id=req.user_id, + skill_name=req.skill_name, + description=req.description, + tags=req.tags or [], + category=req.category, + state="baby", + use_count=0, + version=1, + skill_md_path=skill_md_path, + git_commit_sha=None, + ) + db.add(skill) + + # 记录初始版本 + if req.skill_md_content: + sv = SkillVersion( + id=str(uuid.uuid4()), + user_id=req.user_id, + skill_name=req.skill_name, + version=1, + skill_md_content=req.skill_md_content, + git_commit_sha=None, + description="初始版本", + ) + db.add(sv) + + # 记录事件 + ev = EvolutionEvent( + id=str(uuid.uuid4()), + user_id=req.user_id, + skill_name=req.skill_name, + event_type="created", + description=f"管理员 {admin_user.username} 创建技能 {req.skill_name}", + version_before=None, + version_after=1, + ) + db.add(ev) + + db.commit() + db.refresh(skill) + + LogService.log_operation( + user_id=admin_user.id, + username=admin_user.username, + action="create_skill", + target="evolution_skills", + detail=f"创建技能 {req.skill_name}(归属用户: {user.username})", + ) + + return {"code": 0, "data": skill_to_dict(skill, include_md=True)} + + +# ============================================================ +# 更新技能 +# ============================================================ + +class UpdateSkillSchema(BaseModel): + skill_name: Optional[str] = Field(None, min_length=1, max_length=200) + description: Optional[str] = None + tags: Optional[List[str]] = None + category: Optional[str] = None + skill_md_content: Optional[str] = None + + +@router.put("/{skill_id}") +async def update_skill( + skill_id: str, + req: UpdateSkillSchema, + admin_user=Depends(get_current_admin), + db: Session = Depends(get_db), +): + """更新技能信息""" + skill = db.query(EvolutionSkill).filter(EvolutionSkill.id == skill_id).first() + if not skill: + raise HTTPException(status_code=404, detail="技能不存在") + + old_version = skill.version + update_data = req.model_dump(exclude_none=True) + + # 处理 SKILL.md 内容 + if "skill_md_content" in update_data: + content = update_data.pop("skill_md_content") + if content is not None: + skill_dir = os.path.join(UPLOAD_DIR, "skills", skill.user_id) + os.makedirs(skill_dir, exist_ok=True) + safe_name = skill.skill_name.replace("/", "_").replace("\\", "_") + skill_md_path = os.path.join(skill_dir, f"{safe_name}.md") + try: + with open(skill_md_path, 'w', encoding='utf-8') as f: + f.write(content) + skill.skill_md_path = skill_md_path + except Exception as e: + logger.warning(f"保存 SKILL.md 失败: {e}") + + # 记录新版本 + new_version = old_version + 1 + sv = SkillVersion( + id=str(uuid.uuid4()), + user_id=skill.user_id, + skill_name=skill.skill_name, + version=new_version, + skill_md_content=content, + git_commit_sha=None, + description="管理员更新", + ) + db.add(sv) + skill.version = new_version + + # 记录事件 + ev = EvolutionEvent( + id=str(uuid.uuid4()), + user_id=skill.user_id, + skill_name=skill.skill_name, + event_type="edited", + description=f"管理员 {admin_user.username} 更新技能 {skill.skill_name}", + version_before=old_version, + version_after=new_version, + ) + db.add(ev) + + # 更新其他字段 + for key, value in update_data.items(): + if key not in ("skill_md_content",) and hasattr(skill, key): + setattr(skill, key, value) + + skill.updated_at = datetime.now() + db.commit() + db.refresh(skill) + + LogService.log_operation( + user_id=admin_user.id, + username=admin_user.username, + action="update_skill", + target="evolution_skills", + detail=f"更新技能 {skill.skill_name}(版本 {old_version} → {skill.version})", + ) + + return {"code": 0, "data": skill_to_dict(skill, include_md=True)} + + +# ============================================================ +# 删除技能 +# ============================================================ + +@router.delete("/{skill_id}") +async def delete_skill( + skill_id: str, + admin_user=Depends(get_current_admin), + db: Session = Depends(get_db), +): + """删除技能""" + skill = db.query(EvolutionSkill).filter(EvolutionSkill.id == skill_id).first() + if not skill: + raise HTTPException(status_code=404, detail="技能不存在") + + skill_name = skill.skill_name + user_id = skill.user_id + + # 记录事件 + ev = EvolutionEvent( + id=str(uuid.uuid4()), + user_id=user_id, + skill_name=skill_name, + event_type="deleted", + description=f"管理员 {admin_user.username} 删除技能 {skill_name}", + ) + db.add(ev) + + # 删除版本历史 + db.query(SkillVersion).filter(SkillVersion.skill_name == skill_name).delete() + + # 删除发布记录 + db.query(SkillPublishRecord).filter(SkillPublishRecord.skill_id == skill_id).delete() + + # 删除团队共享记录 + db.query(TeamSharedSkill).filter(TeamSharedSkill.skill_id == skill_id).delete() + + db.delete(skill) + db.commit() + + LogService.log_operation( + user_id=admin_user.id, + username=admin_user.username, + action="delete_skill", + target="evolution_skills", + detail=f"删除技能 {skill_name}", + ) + + return {"code": 0, "message": f"技能 {skill_name} 已删除"} + + +# ============================================================ +# 修改技能状态 +# ============================================================ + +class ChangeStateSchema(BaseModel): + state: str = Field(..., description="目标状态: baby/main/staging/frozen") + + +@router.put("/{skill_id}/state") +async def change_skill_state( + skill_id: str, + req: ChangeStateSchema, + admin_user=Depends(get_current_admin), + db: Session = Depends(get_db), +): + """修改技能状态""" + if req.state not in SKILL_STATES: + raise HTTPException(status_code=400, detail=f"无效状态,可选值: {list(SKILL_STATES.keys())}") + + skill = db.query(EvolutionSkill).filter(EvolutionSkill.id == skill_id).first() + if not skill: + raise HTTPException(status_code=404, detail="技能不存在") + + old_state = skill.state + skill.state = req.state + skill.updated_at = datetime.now() + + # 记录状态变更事件 + ev_type = "promoted" if req.state in ("main", "staging") else "frozen" if req.state == "frozen" else "unfrozen" + ev = EvolutionEvent( + id=str(uuid.uuid4()), + user_id=skill.user_id, + skill_name=skill.skill_name, + event_type=ev_type, + description=f"管理员 {admin_user.username} 将技能状态从 {old_state} 变更为 {req.state}", + ) + db.add(ev) + db.commit() + db.refresh(skill) + + LogService.log_operation( + user_id=admin_user.id, + username=admin_user.username, + action="change_skill_state", + target="evolution_skills", + detail=f"修改技能 {skill.skill_name} 状态: {old_state} → {req.state}", + ) + + return {"code": 0, "data": skill_to_dict(skill)} + + +# ============================================================ +# 获取版本历史 +# ============================================================ + +@router.get("/{skill_id}/versions") +async def get_skill_versions( + skill_id: str, + admin_user=Depends(get_current_admin), + db: Session = Depends(get_db), +): + """获取技能版本历史""" + skill = db.query(EvolutionSkill).filter(EvolutionSkill.id == skill_id).first() + if not skill: + raise HTTPException(status_code=404, detail="技能不存在") + + versions = db.query(SkillVersion).filter( + SkillVersion.skill_name == skill.skill_name, + SkillVersion.user_id == skill.user_id, + ).order_by(desc(SkillVersion.version)).all() + + return { + "code": 0, + "data": { + "skill_id": skill.id, + "skill_name": skill.skill_name, + "current_version": skill.version, + "versions": [version_to_dict(v) for v in versions], + } + } + + +# ============================================================ +# 回滚到指定版本 +# ============================================================ + +class RollbackSchema(BaseModel): + version: int = Field(..., ge=1, description="目标版本号") + + +@router.post("/{skill_id}/rollback") +async def rollback_skill_version( + skill_id: str, + req: RollbackSchema, + admin_user=Depends(get_current_admin), + db: Session = Depends(get_db), +): + """回滚技能到指定版本""" + skill = db.query(EvolutionSkill).filter(EvolutionSkill.id == skill_id).first() + if not skill: + raise HTTPException(status_code=404, detail="技能不存在") + + target_version = db.query(SkillVersion).filter( + SkillVersion.skill_name == skill.skill_name, + SkillVersion.user_id == skill.user_id, + SkillVersion.version == req.version, + ).first() + + if not target_version: + raise HTTPException(status_code=404, detail=f"版本 {req.version} 不存在") + + old_version = skill.version + + # 更新 SKILL.md 文件 + if target_version.skill_md_content: + if skill.skill_md_path: + try: + with open(skill.skill_md_path, 'w', encoding='utf-8') as f: + f.write(target_version.skill_md_content) + except Exception as e: + logger.warning(f"回滚保存 SKILL.md 失败: {e}") + + # 记录新版本(基于目标版本) + new_version = old_version + 1 + sv = SkillVersion( + id=str(uuid.uuid4()), + user_id=skill.user_id, + skill_name=skill.skill_name, + version=new_version, + skill_md_content=target_version.skill_md_content, + git_commit_sha=None, + description=f"回滚到版本 {req.version}(管理员操作)", + ) + db.add(sv) + + skill.version = new_version + skill.updated_at = datetime.now() + + ev = EvolutionEvent( + id=str(uuid.uuid4()), + user_id=skill.user_id, + skill_name=skill.skill_name, + event_type="edited", + description=f"管理员 {admin_user.username} 回滚技能 {skill.skill_name} 从版本 {old_version} 到 {req.version}(新版本 {new_version})", + version_before=old_version, + version_after=new_version, + ) + db.add(ev) + + db.commit() + db.refresh(skill) + + LogService.log_operation( + user_id=admin_user.id, + username=admin_user.username, + action="rollback_skill", + target="evolution_skills", + detail=f"回滚技能 {skill.skill_name} 到版本 {req.version}(新版本 {new_version})", + ) + + return {"code": 0, "data": skill_to_dict(skill, include_md=True)} + + +# ============================================================ +# 发布到应用市场 +# ============================================================ + +class PublishSkillSchema(BaseModel): + price: float = Field(0.0, ge=0, description="定价(0表示免费)") + market_description: str = Field("", description="市场描述") + + +@router.post("/{skill_id}/publish") +async def publish_skill( + skill_id: str, + req: PublishSkillSchema, + admin_user=Depends(get_current_admin), + db: Session = Depends(get_db), +): + """发布技能到应用市场""" + skill = db.query(EvolutionSkill).filter(EvolutionSkill.id == skill_id).first() + if not skill: + raise HTTPException(status_code=404, detail="技能不存在") + + # 检查是否已发布 + existing = db.query(SkillPublishRecord).filter( + SkillPublishRecord.skill_id == skill_id, + SkillPublishRecord.status == "published" + ).first() + + if existing: + # 更新现有发布 + existing.price = req.price + existing.market_description = req.market_description + msg = "更新发布信息" + else: + # 创建发布记录 + record = SkillPublishRecord( + id=str(uuid.uuid4()), + skill_id=skill_id, + skill_name=skill.skill_name, + user_id=skill.user_id, + price=req.price, + market_description=req.market_description, + status="published", + ) + db.add(record) + msg = "发布成功" + + db.commit() + + LogService.log_operation( + user_id=admin_user.id, + username=admin_user.username, + action="publish_skill", + target="skill_publish_records", + detail=f"{msg}: {skill.skill_name},定价: ¥{req.price}", + ) + + return {"code": 0, "message": msg, "data": {"skill_id": skill_id, "price": req.price}} + + +# ============================================================ +# 下架技能 +# ============================================================ + +@router.post("/{skill_id}/unpublish") +async def unpublish_skill( + skill_id: str, + admin_user=Depends(get_current_admin), + db: Session = Depends(get_db), +): + """从应用市场下架技能""" + skill = db.query(EvolutionSkill).filter(EvolutionSkill.id == skill_id).first() + if not skill: + raise HTTPException(status_code=404, detail="技能不存在") + + updated = db.query(SkillPublishRecord).filter( + SkillPublishRecord.skill_id == skill_id, + SkillPublishRecord.status == "published" + ).update({"status": "unpublished"}) + db.commit() + + if updated == 0: + raise HTTPException(status_code=400, detail="该技能未发布到市场") + + LogService.log_operation( + user_id=admin_user.id, + username=admin_user.username, + action="unpublish_skill", + target="skill_publish_records", + detail=f"下架技能: {skill.skill_name}", + ) + + return {"code": 0, "message": f"技能 {skill.skill_name} 已下架"} + + +# ============================================================ +# 获取统计数据 +# ============================================================ + +@router.get("/stats/overview") +async def get_skills_stats( + admin_user=Depends(get_current_admin), + db: Session = Depends(get_db), +): + """获取技能统计概览""" + total = db.query(EvolutionSkill).count() + by_state = dict( + db.query(EvolutionSkill.state, func.count(EvolutionSkill.id)) + .group_by(EvolutionSkill.state).all() + ) + by_category = dict( + db.query(EvolutionSkill.category, func.count(EvolutionSkill.id)) + .filter(EvolutionSkill.category.isnot(None)) + .group_by(EvolutionSkill.category).all() + ) + total_published = db.query(SkillPublishRecord).filter( + SkillPublishRecord.status == "published" + ).count() + total_versions = db.query(SkillVersion).count() + + return { + "code": 0, + "data": { + "total_skills": total, + "by_state": {k: by_state.get(k, 0) for k in SKILL_STATES}, + "by_category": by_category, + "total_published": total_published, + "total_versions": total_versions, + } + } diff --git a/backend/python/main.py b/backend/python/main.py index 663d5c8..0f37df0 100644 --- a/backend/python/main.py +++ b/backend/python/main.py @@ -1,214 +1,215 @@ -""" -Qihu-AIFlow FastAPI 主应用 -完整后端API,监听18080端口,所有接口以 /app 为前缀 -""" -import sys -import os -import time -import uuid -import logging -from contextlib import asynccontextmanager -from fastapi import FastAPI, Request -from fastapi.middleware.cors import CORSMiddleware -from starlette.middleware.base import BaseHTTPMiddleware -from datetime import datetime - -sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) - -from app.api import auth, chat, channels, knowledge, market, token_usage, payment, profile, notifications, assets, evolution -from app.api.admin import dashboard, users, models, settings, payment_admin, services, membership_admin, ads, market_admin, logs -from app.config import setup_file_logging, SLOW_REQUEST_THRESHOLD -from app.database import SessionLocal -from app.models import OperationLog as OperationLogModel, TokenUsageLog as TokenUsageLogModel - -# 导入平台适配器触发注册(AstrBot借鉴优化方案 P1) -import app.platform_adapters # noqa: F401 - 触发所有平台适配器装饰器注册 - -# 初始化文件日志(按天轮转) -access_logger = setup_file_logging() -logger = logging.getLogger("aiflow") - - -class APITrackingMiddleware(BaseHTTPMiddleware): - """API调用统计中间件 - 记录请求耗时,标记慢请求""" - - async def dispatch(self, request: Request, call_next): - request_id = str(uuid.uuid4()) - start_time = time.time() - path = request.url.path - method = request.method - - # 跳过健康检查和静态资源 - skip_paths = ["/app/api/health", "/favicon.ico"] - should_skip = any(path.startswith(p) for p in skip_paths) - - try: - response = await call_next(request) - elapsed = time.time() - start_time - status_code = response.status_code - - if not should_skip: - access_logger.info( - f"[{request_id}] {method} {path} -> {status_code} ({elapsed:.3f}s)" - ) - - # 异步记录API调用统计 - try: - db = SessionLocal() - try: - _record_api_call(db, method, path, status_code, elapsed) - finally: - db.close() - except Exception as e: - logger.warning(f"API统计记录失败: {e}") - - return response - except Exception as e: - elapsed = time.time() - start_time - if not should_skip: - access_logger.info( - f"[{request_id}] {method} {path} -> ERROR ({elapsed:.3f}s): {str(e)}" - ) - raise - - async def __call__(self, scope, receive, send): - if scope["type"] != "http": - await self.app(scope, receive, send) - return - await super().__call__(scope, receive, send) - - -def _record_api_call(db, method: str, path: str, status_code: int, elapsed: float): - """记录API调用到数据库统计表""" - # 简化路径以减少基数(去掉动态ID) - parts = path.split("/") - simplified_parts = [] - for part in parts: - # 将UUID样式的路径段替换为{id} - if len(part) == 36 and part.count("-") == 4: - simplified_parts.append("{id}") - # 将纯数字路径段替换为{id} - elif part.isdigit(): - simplified_parts.append("{id}") - else: - simplified_parts.append(part) - simplified_path = "/".join(simplified_parts) - - # 统计记录通过数据库实现,简化版记录到日志 - is_error = status_code >= 400 - is_slow = elapsed >= SLOW_REQUEST_THRESHOLD - - if is_slow: - logger.warning( - f"慢请求: {method} {simplified_path} -> {status_code} ({elapsed:.3f}s, 阈值: {SLOW_REQUEST_THRESHOLD}s)" - ) - - -@asynccontextmanager -async def lifespan(app: FastAPI): - logger.info("Qihu-AIFlow v2.0.0 启动中...") - - # 启动后台 Evolution Pipeline(知识库图谱已存在的用户配置自动恢复运行) - try: - from app.services.evolution_service import EvolutionWatcherManager - from app.models import UserEvolutionConfig - db = SessionLocal() - try: - configs = db.query(UserEvolutionConfig).filter( - UserEvolutionConfig.watcher_status == "running" - ).all() - if configs: - watcher = EvolutionWatcherManager() - for cfg in configs: - try: - watcher.start_user_evolution(cfg.user_id, lambda: SessionLocal()) - logger.info(f"[Evolution] 恢复用户 {cfg.user_id} 的进化Pipeline") - except Exception as e: - logger.warning(f"[Evolution] 恢复用户 {cfg.user_id} 失败: {e}") - logger.info(f"[Evolution] 已恢复 {len(configs)} 个用户的进化Pipeline") - else: - logger.info("[Evolution] 无待恢复的进化配置") - finally: - db.close() - except Exception as e: - logger.warning(f"[Evolution] Pipeline自动启动异常(不影响正常服务): {e}") - - # 启动Redis事件总线 - try: - from app.event_bus import get_event_bus - event_bus = get_event_bus() - event_bus.start(num_consumers=2) - logger.info("[EventBus] Redis事件总线已启动") - except Exception as e: - logger.warning(f"[EventBus] 启动失败(不影响正常服务): {e}") - - logger.info("Qihu-AIFlow v2.0.0 启动完成") - - yield - - # 停止Redis事件总线 - try: - from app.event_bus import get_event_bus - get_event_bus().stop() - logger.info("[EventBus] Redis事件总线已停止") - except Exception: - pass - - # 优雅关闭:停止所有 Evolution Pipeline - try: - from app.services.evolution_service import EvolutionWatcherManager - EvolutionWatcherManager().shutdown_all() - logger.info("[Evolution] 所有Pipeline已停止") - except Exception: - pass - - logger.info("Qihu-AIFlow 正在关闭...") - - -app = FastAPI( - title="Qihu-AIFlow - 智能自动化PaaS平台", - version="1.0.0", - lifespan=lifespan, -) - -# API调用追踪中间件 -app.add_middleware(APITrackingMiddleware) - -# CORS配置 -app.add_middleware( - CORSMiddleware, - allow_origins=["*"], - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) - -# 前台 API -app.include_router(auth.router, prefix="/app/api/auth", tags=["认证"]) -app.include_router(chat.router, prefix="/app/api/chat", tags=["AI聊天"]) -app.include_router(channels.router, prefix="/app/api/channels", tags=["频道"]) -app.include_router(knowledge.router, prefix="/app/api/knowledge", tags=["知识库"]) -app.include_router(market.router, prefix="/app/api/market", tags=["应用市场"]) -app.include_router(token_usage.router, prefix="/app/api/token", tags=["Token驾驶舱"]) -app.include_router(payment.router, prefix="/app/api/payment", tags=["付费中心"]) -app.include_router(profile.router, prefix="/app/api/profile", tags=["个人中心"]) -app.include_router(notifications.router, prefix="/app/api/notifications", tags=["消息通知"]) -app.include_router(assets.router, prefix="/app/api/assets", tags=["资产管理"]) -app.include_router(evolution.router, prefix="/app/api/evolution", tags=["AI自进化"]) - -# 后台 API -app.include_router(dashboard.router, prefix="/app/api/admin/dashboard", tags=["后台-仪表盘"]) -app.include_router(users.router, prefix="/app/api/admin/users", tags=["后台-用户管理"]) -app.include_router(models.router, prefix="/app/api/admin/models", tags=["后台-模型管理"]) -app.include_router(settings.router, prefix="/app/api/admin/settings", tags=["后台-网站设置"]) -app.include_router(payment_admin.router, prefix="/app/api/admin/payment", tags=["后台-支付配置"]) -app.include_router(services.router, prefix="/app/api/admin/services", tags=["后台-增值服务"]) -app.include_router(membership_admin.router, prefix="/app/api/admin/membership", tags=["后台-会员管理"]) -app.include_router(ads.router, prefix="/app/api/admin/ads", tags=["后台-广告管理"]) -app.include_router(market_admin.router, prefix="/app/api/admin/market", tags=["后台-应用审核"]) -app.include_router(logs.router, prefix="/app/api/admin/logs", tags=["后台-日志风控"]) - - -@app.get("/app/api/health") -async def health_check(): - return {"status": "ok", "service": "Qihu-AIFlow"} +""" +Qihu-AIFlow FastAPI 主应用 +完整后端API,监听18080端口,所有接口以 /app 为前缀 +""" +import sys +import os +import time +import uuid +import logging +from contextlib import asynccontextmanager +from fastapi import FastAPI, Request +from fastapi.middleware.cors import CORSMiddleware +from starlette.middleware.base import BaseHTTPMiddleware +from datetime import datetime + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from app.api import auth, chat, channels, knowledge, market, token_usage, payment, profile, notifications, assets, evolution +from app.api.admin import dashboard, users, models, settings, payment_admin, services, membership_admin, ads, market_admin, logs, skills_admin +from app.config import setup_file_logging, SLOW_REQUEST_THRESHOLD +from app.database import SessionLocal +from app.models import OperationLog as OperationLogModel, TokenUsageLog as TokenUsageLogModel + +# 导入平台适配器触发注册(AstrBot借鉴优化方案 P1) +import app.platform_adapters # noqa: F401 - 触发所有平台适配器装饰器注册 + +# 初始化文件日志(按天轮转) +access_logger = setup_file_logging() +logger = logging.getLogger("aiflow") + + +class APITrackingMiddleware(BaseHTTPMiddleware): + """API调用统计中间件 - 记录请求耗时,标记慢请求""" + + async def dispatch(self, request: Request, call_next): + request_id = str(uuid.uuid4()) + start_time = time.time() + path = request.url.path + method = request.method + + # 跳过健康检查和静态资源 + skip_paths = ["/app/api/health", "/favicon.ico"] + should_skip = any(path.startswith(p) for p in skip_paths) + + try: + response = await call_next(request) + elapsed = time.time() - start_time + status_code = response.status_code + + if not should_skip: + access_logger.info( + f"[{request_id}] {method} {path} -> {status_code} ({elapsed:.3f}s)" + ) + + # 异步记录API调用统计 + try: + db = SessionLocal() + try: + _record_api_call(db, method, path, status_code, elapsed) + finally: + db.close() + except Exception as e: + logger.warning(f"API统计记录失败: {e}") + + return response + except Exception as e: + elapsed = time.time() - start_time + if not should_skip: + access_logger.info( + f"[{request_id}] {method} {path} -> ERROR ({elapsed:.3f}s): {str(e)}" + ) + raise + + async def __call__(self, scope, receive, send): + if scope["type"] != "http": + await self.app(scope, receive, send) + return + await super().__call__(scope, receive, send) + + +def _record_api_call(db, method: str, path: str, status_code: int, elapsed: float): + """记录API调用到数据库统计表""" + # 简化路径以减少基数(去掉动态ID) + parts = path.split("/") + simplified_parts = [] + for part in parts: + # 将UUID样式的路径段替换为{id} + if len(part) == 36 and part.count("-") == 4: + simplified_parts.append("{id}") + # 将纯数字路径段替换为{id} + elif part.isdigit(): + simplified_parts.append("{id}") + else: + simplified_parts.append(part) + simplified_path = "/".join(simplified_parts) + + # 统计记录通过数据库实现,简化版记录到日志 + is_error = status_code >= 400 + is_slow = elapsed >= SLOW_REQUEST_THRESHOLD + + if is_slow: + logger.warning( + f"慢请求: {method} {simplified_path} -> {status_code} ({elapsed:.3f}s, 阈值: {SLOW_REQUEST_THRESHOLD}s)" + ) + + +@asynccontextmanager +async def lifespan(app: FastAPI): + logger.info("Qihu-AIFlow v2.0.0 启动中...") + + # 启动后台 Evolution Pipeline(知识库图谱已存在的用户配置自动恢复运行) + try: + from app.services.evolution_service import EvolutionWatcherManager + from app.models import UserEvolutionConfig + db = SessionLocal() + try: + configs = db.query(UserEvolutionConfig).filter( + UserEvolutionConfig.watcher_status == "running" + ).all() + if configs: + watcher = EvolutionWatcherManager() + for cfg in configs: + try: + watcher.start_user_evolution(cfg.user_id, lambda: SessionLocal()) + logger.info(f"[Evolution] 恢复用户 {cfg.user_id} 的进化Pipeline") + except Exception as e: + logger.warning(f"[Evolution] 恢复用户 {cfg.user_id} 失败: {e}") + logger.info(f"[Evolution] 已恢复 {len(configs)} 个用户的进化Pipeline") + else: + logger.info("[Evolution] 无待恢复的进化配置") + finally: + db.close() + except Exception as e: + logger.warning(f"[Evolution] Pipeline自动启动异常(不影响正常服务): {e}") + + # 启动Redis事件总线 + try: + from app.event_bus import get_event_bus + event_bus = get_event_bus() + event_bus.start(num_consumers=2) + logger.info("[EventBus] Redis事件总线已启动") + except Exception as e: + logger.warning(f"[EventBus] 启动失败(不影响正常服务): {e}") + + logger.info("Qihu-AIFlow v2.0.0 启动完成") + + yield + + # 停止Redis事件总线 + try: + from app.event_bus import get_event_bus + get_event_bus().stop() + logger.info("[EventBus] Redis事件总线已停止") + except Exception: + pass + + # 优雅关闭:停止所有 Evolution Pipeline + try: + from app.services.evolution_service import EvolutionWatcherManager + EvolutionWatcherManager().shutdown_all() + logger.info("[Evolution] 所有Pipeline已停止") + except Exception: + pass + + logger.info("Qihu-AIFlow 正在关闭...") + + +app = FastAPI( + title="Qihu-AIFlow - 智能自动化PaaS平台", + version="1.0.0", + lifespan=lifespan, +) + +# API调用追踪中间件 +app.add_middleware(APITrackingMiddleware) + +# CORS配置 +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# 前台 API +app.include_router(auth.router, prefix="/app/api/auth", tags=["认证"]) +app.include_router(chat.router, prefix="/app/api/chat", tags=["AI聊天"]) +app.include_router(channels.router, prefix="/app/api/channels", tags=["频道"]) +app.include_router(knowledge.router, prefix="/app/api/knowledge", tags=["知识库"]) +app.include_router(market.router, prefix="/app/api/market", tags=["应用市场"]) +app.include_router(token_usage.router, prefix="/app/api/token", tags=["Token驾驶舱"]) +app.include_router(payment.router, prefix="/app/api/payment", tags=["付费中心"]) +app.include_router(profile.router, prefix="/app/api/profile", tags=["个人中心"]) +app.include_router(notifications.router, prefix="/app/api/notifications", tags=["消息通知"]) +app.include_router(assets.router, prefix="/app/api/assets", tags=["资产管理"]) +app.include_router(evolution.router, prefix="/app/api/evolution", tags=["AI自进化"]) + +# 后台 API +app.include_router(dashboard.router, prefix="/app/api/admin/dashboard", tags=["后台-仪表盘"]) +app.include_router(users.router, prefix="/app/api/admin/users", tags=["后台-用户管理"]) +app.include_router(models.router, prefix="/app/api/admin/models", tags=["后台-模型管理"]) +app.include_router(settings.router, prefix="/app/api/admin/settings", tags=["后台-网站设置"]) +app.include_router(payment_admin.router, prefix="/app/api/admin/payment", tags=["后台-支付配置"]) +app.include_router(services.router, prefix="/app/api/admin/services", tags=["后台-增值服务"]) +app.include_router(membership_admin.router, prefix="/app/api/admin/membership", tags=["后台-会员管理"]) +app.include_router(ads.router, prefix="/app/api/admin/ads", tags=["后台-广告管理"]) +app.include_router(market_admin.router, prefix="/app/api/admin/market", tags=["后台-应用审核"]) +app.include_router(logs.router, prefix="/app/api/admin/logs", tags=["后台-日志风控"]) +app.include_router(skills_admin.router, prefix="/app/api/admin/skills", tags=["后台-技能管理"]) + + +@app.get("/app/api/health") +async def health_check(): + return {"status": "ok", "service": "Qihu-AIFlow"} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 43e07c4..32905ad 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,121 +1,123 @@ -import React, { useState, useEffect } from 'react'; -import { Routes, Route, Navigate } from 'react-router-dom'; -import type { User } from '@/types'; -import { AuthContext, AuthContextType } from '@/contexts/AuthContext'; -import { authAPI } from '@/api'; - -// Layouts -import FrontLayout from '@/components/layout/FrontLayout'; -import AdminLayout from '@/components/layout/AdminLayout'; - -// Front Pages -import LandingPage from '@/pages/front/LandingPage'; -import LoginPage from '@/pages/front/LoginPage'; -import ChatPage from '@/pages/front/ChatPage'; -import ChannelPage from '@/pages/front/ChannelPage'; -import KnowledgePage from '@/pages/front/KnowledgePage'; -import MarketPage from '@/pages/front/MarketPage'; -import TokenDashboardPage from '@/pages/front/TokenDashboardPage'; -import PricingPage from '@/pages/front/PricingPage'; -import ProfilePage from '@/pages/front/ProfilePage'; -import NotificationPage from '@/pages/front/NotificationPage'; -import AssetsPage from '@/pages/front/AssetsPage'; -import NovelPreviewPage from '@/pages/front/NovelPreviewPage'; -import EvolutionDashboard from '@/pages/front/EvolutionDashboard'; - -// Admin Pages -import AdminDashboard from '@/pages/admin/Dashboard'; -import AdminUsers from '@/pages/admin/Users'; -import AdminModels from '@/pages/admin/Models'; -import AdminSiteSettings from '@/pages/admin/SiteSettings'; -import AdminPayment from '@/pages/admin/Payment'; -import AdminServices from '@/pages/admin/Services'; -import AdminMembership from '@/pages/admin/Membership'; -import AdminAds from '@/pages/admin/Ads'; -import AdminMarket from '@/pages/admin/Market'; -import AdminLogs from '@/pages/admin/Logs'; - -const App: React.FC = () => { - const [user, setUser] = useState(null); - const [loading, setLoading] = useState(true); - - // 初始化:从localStorage恢复登录状态 - useEffect(() => { - const token = localStorage.getItem('aiflow_token'); - const cachedUser = localStorage.getItem('aiflow_user'); - if (token && cachedUser) { - try { - setUser(JSON.parse(cachedUser)); - } catch { - localStorage.removeItem('aiflow_token'); - localStorage.removeItem('aiflow_user'); - } - } - setLoading(false); - }, []); - - const login = (u: User) => { - setUser(u); - localStorage.setItem('aiflow_user', JSON.stringify(u)); - }; - const logout = () => { - setUser(null); - localStorage.removeItem('aiflow_token'); - localStorage.removeItem('aiflow_user'); - }; - const isAdmin = user?.role === 'admin'; - - if (loading) { - return
加载中...
; - } - - return ( - - - {/* Login */} - : ) : - } /> - - {/* Root - Landing for guests, redirect for authenticated users */} - : ) : - } /> - - {/* Front Routes (authenticated only) */} - }> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - - {/* Admin Routes */} - }> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - - } /> - - - ); -}; - -export default App; +import React, { useState, useEffect } from 'react'; +import { Routes, Route, Navigate } from 'react-router-dom'; +import type { User } from '@/types'; +import { AuthContext, AuthContextType } from '@/contexts/AuthContext'; +import { authAPI } from '@/api'; + +// Layouts +import FrontLayout from '@/components/layout/FrontLayout'; +import AdminLayout from '@/components/layout/AdminLayout'; + +// Front Pages +import LandingPage from '@/pages/front/LandingPage'; +import LoginPage from '@/pages/front/LoginPage'; +import ChatPage from '@/pages/front/ChatPage'; +import ChannelPage from '@/pages/front/ChannelPage'; +import KnowledgePage from '@/pages/front/KnowledgePage'; +import MarketPage from '@/pages/front/MarketPage'; +import TokenDashboardPage from '@/pages/front/TokenDashboardPage'; +import PricingPage from '@/pages/front/PricingPage'; +import ProfilePage from '@/pages/front/ProfilePage'; +import NotificationPage from '@/pages/front/NotificationPage'; +import AssetsPage from '@/pages/front/AssetsPage'; +import NovelPreviewPage from '@/pages/front/NovelPreviewPage'; +import EvolutionDashboard from '@/pages/front/EvolutionDashboard'; + +// Admin Pages +import AdminDashboard from '@/pages/admin/Dashboard'; +import AdminUsers from '@/pages/admin/Users'; +import AdminModels from '@/pages/admin/Models'; +import AdminSiteSettings from '@/pages/admin/SiteSettings'; +import AdminPayment from '@/pages/admin/Payment'; +import AdminServices from '@/pages/admin/Services'; +import AdminMembership from '@/pages/admin/Membership'; +import AdminAds from '@/pages/admin/Ads'; +import AdminMarket from '@/pages/admin/Market'; +import AdminLogs from '@/pages/admin/Logs'; +import AdminSkills from '@/pages/admin/Skills'; + +const App: React.FC = () => { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + + // 初始化:从localStorage恢复登录状态 + useEffect(() => { + const token = localStorage.getItem('aiflow_token'); + const cachedUser = localStorage.getItem('aiflow_user'); + if (token && cachedUser) { + try { + setUser(JSON.parse(cachedUser)); + } catch { + localStorage.removeItem('aiflow_token'); + localStorage.removeItem('aiflow_user'); + } + } + setLoading(false); + }, []); + + const login = (u: User) => { + setUser(u); + localStorage.setItem('aiflow_user', JSON.stringify(u)); + }; + const logout = () => { + setUser(null); + localStorage.removeItem('aiflow_token'); + localStorage.removeItem('aiflow_user'); + }; + const isAdmin = user?.role === 'admin'; + + if (loading) { + return
加载中...
; + } + + return ( + + + {/* Login */} + : ) : + } /> + + {/* Root - Landing for guests, redirect for authenticated users */} + : ) : + } /> + + {/* Front Routes (authenticated only) */} + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + {/* Admin Routes */} + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + } /> + + } /> + + + ); +}; + +export default App; diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index 0757bd2..13a9a34 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -1,507 +1,528 @@ -/** - * AIFlow API 客户端 - * 统一管理所有前后端API请求 - */ -const API_BASE = '/app/api'; - -// 从localStorage获取token -function getToken(): string | null { - return localStorage.getItem('aiflow_token'); -} - -// 通用请求方法 -async function request( - endpoint: string, - options: RequestInit = {} -): Promise<{ code: number; data: T; message: string }> { - const token = getToken(); - const headers: Record = { - 'Content-Type': 'application/json', - ...(options.headers as Record || {}), - }; - if (token) { - headers['Authorization'] = `Bearer ${token}`; - } - const res = await fetch(`${API_BASE}${endpoint}`, { - ...options, - headers, - }); - if (res.status === 401) { - // 登录接口的401需要精确提示(用户名不存在 / 密码错误) - if (endpoint === '/auth/login') { - const err = await res.json().catch(() => ({ detail: '登录失败' })); - const error: any = new Error(err.detail || '登录失败'); - error.status = 401; - error.detail = err.detail; - throw error; - } - // 其他接口的401 = token过期/未登录,清除状态并跳转 - localStorage.removeItem('aiflow_token'); - localStorage.removeItem('aiflow_user'); - window.location.hash = '#/login'; - throw new Error('未登录'); - } - if (!res.ok) { - const err = await res.json().catch(() => ({ detail: '请求失败' })); - const error: any = new Error(err.detail || err.message || '请求失败'); - error.status = res.status; - error.response = { status: res.status, data: err }; - error.detail = err.detail; - throw error; - } - return res.json(); -} - -// GET请求 -function get(endpoint: string): Promise<{ code: number; data: T; message: string }> { - return request(endpoint); -} - -// POST请求 -function post(endpoint: string, body?: any): Promise<{ code: number; data: T; message: string }> { - return request(endpoint, { - method: 'POST', - body: body ? JSON.stringify(body) : undefined, - }); -} - -// PUT请求 -function put(endpoint: string, body?: any): Promise<{ code: number; data: T; message: string }> { - return request(endpoint, { - method: 'PUT', - body: body ? JSON.stringify(body) : undefined, - }); -} - -// DELETE请求 -function del(endpoint: string): Promise<{ code: number; data: T; message: string }> { - return request(endpoint, { method: 'DELETE' }); -} - -// ============================================================ -// 认证相关API -// ============================================================ -export const authAPI = { - login: (username: string, password: string) => - post('/auth/login', { username, password }), - register: (data: { username: string; password: string; email?: string; phone?: string }) => - post('/auth/register', data), - getMe: () => get('/auth/me'), - logout: () => post('/auth/logout'), - getSettings: () => get('/auth/settings'), -}; - -// ============================================================ -// 聊天相关API -// ============================================================ -export const chatAPI = { - getConversations: (status?: string) => - get(`/chat/conversations${status ? `?status=${status}` : ''}`), - createConversation: (data: { title: string; model_id?: string }) => - post('/chat/conversations', data), - updateConversation: (id: string, data: any) => - put(`/chat/conversations/${id}`, data), - deleteConversation: (id: string) => - del(`/chat/conversations/${id}`), - getMessages: (convId: string) => - get(`/chat/conversations/${convId}/messages`), - sendMessage: (convId: string, data: { content: string; model_id?: string; agent_ids?: string[]; skill_ids?: string[]; knowledge_ids?: string[]; use_tools?: boolean }) => - post(`/chat/conversations/${convId}/messages`, data), - clearContext: (convId: string) => - post(`/chat/conversations/${convId}/clear`), - convertToKnowledgeGraph: (convId: string) => - post(`/chat/conversations/${convId}/to-knowledge-graph`), - getModels: () => get('/chat/models'), -}; - -// ============================================================ -// 频道相关API -// ============================================================ -export const channelAPI = { - getChannels: () => get('/channels/'), - createChannel: (data: any) => post('/channels/', data), - getChannel: (id: string) => get(`/channels/${id}`), - updateChannel: (id: string, data: any) => put(`/channels/${id}`, data), - deleteChannel: (id: string) => del(`/channels/${id}`), - connectChannel: (id: string) => post(`/channels/${id}/connect`), - disconnectChannel: (id: string) => post(`/channels/${id}/disconnect`), - getPlatformConfigs: () => get('/channels/platforms/config'), - webhookMessage: (id: string, data: { from_user: string; content: string; message_type?: string }) => - post(`/channels/${id}/webhook`, data), - getMembers: (id: string) => get(`/channels/${id}/members`), - addMember: (id: string, userId: string) => post(`/channels/${id}/members`, { user_id: userId }), - removeMember: (id: string, userId: string) => del(`/channels/${id}/members/${userId}`), - getMessages: (id: string, params?: { limit?: number; offset?: number }) => { - const sp = new URLSearchParams(); - if (params?.limit) sp.set('limit', String(params.limit)); - if (params?.offset) sp.set('offset', String(params.offset)); - const qs = sp.toString(); - return get(`/channels/${id}/messages${qs ? `?${qs}` : ''}`); - }, - sendMessage: (id: string, content: string) => post(`/channels/${id}/messages`, { content }), - authorize: (platform: string) => post('/channels/authorize', { platform }), - scanAuthorize: (platform: string, credentials?: Record) => post(`/channels/authorize/scan?platform=${platform}`, credentials ? { credentials } : {}), - scanAuthorizeStatus: (platform: string) => post(`/channels/authorize/scan/status?platform=${platform}`), -}; - -// ============================================================ -// 知识库相关API -// ============================================================ -export const knowledgeAPI = { - getBases: () => get('/knowledge/bases'), - createBase: (data: any) => post('/knowledge/bases', data), - getBase: (id: string) => get(`/knowledge/bases/${id}`), - updateBase: (id: string, data: any) => put(`/knowledge/bases/${id}`, data), - deleteBase: (id: string) => del(`/knowledge/bases/${id}`), - getDocuments: (baseId: string) => get(`/knowledge/bases/${baseId}/documents`), - getDocument: (baseId: string, docId: string) => get(`/knowledge/bases/${baseId}/documents/${docId}`), - uploadDocument: (baseId: string, data: any) => post(`/knowledge/bases/${baseId}/documents`, data), - deleteDocument: (baseId: string, docId: string) => del(`/knowledge/bases/${baseId}/documents/${docId}`), - getDirectories: () => get('/knowledge/directories'), - createDirectory: (data: any) => post('/knowledge/directories', data), - buildGraph: (baseId: string) => post(`/knowledge/bases/${baseId}/build-graph`), - getGraph: (baseId: string) => get(`/knowledge/bases/${baseId}/graph`), - searchQA: (baseId: string, question: string) => post('/knowledge/qa', { base_id: baseId, question }), - searchDocuments: (baseId: string, keyword: string) => get(`/knowledge/bases/${baseId}/search?keyword=${encodeURIComponent(keyword)}`), -}; - -// ============================================================ -// 应用市场相关API -// ============================================================ -export const marketAPI = { - getApps: (params?: { type?: string; source?: string; search?: string; page?: number; page_size?: number }) => { - const searchParams = new URLSearchParams(); - if (params?.type) searchParams.set('type', params.type); - if (params?.source) searchParams.set('source', params.source); - if (params?.search) searchParams.set('search', params.search); - if (params?.page) searchParams.set('page', String(params.page)); - if (params?.page_size) searchParams.set('page_size', String(params.page_size)); - const qs = searchParams.toString(); - return get(`/market/apps${qs ? `?${qs}` : ''}`); - }, - getApp: (id: string) => get(`/market/apps/${id}`), - createApp: (data: any) => post('/market/apps', data), - purchaseApp: (id: string) => post(`/market/apps/${id}/purchase`), - installApp: (id: string) => post(`/market/apps/${id}/install`), - uninstallApp: (id: string) => del(`/market/apps/${id}/uninstall`), - reviewApp: (id: string, data: { rating: number; content?: string }) => - post(`/market/apps/${id}/review`, data), - getMyApps: () => get('/market/my-apps'), - getReviews: (appId: string) => get(`/market/reviews/${appId}`), -}; - -// ============================================================ -// Token相关API -// ============================================================ -export const tokenAPI = { - getOverview: () => get('/token/overview'), - getRankings: () => get('/token/rankings'), - getLogs: (params?: any) => { - const sp = new URLSearchParams(); - if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); - const qs = sp.toString(); - return get(`/token/logs${qs ? `?${qs}` : ''}`); - }, - getBalance: () => get('/token/balance'), - recharge: (amount: number) => post('/token/recharge', { amount }), -}; - -// ============================================================ -// 付费相关API -// ============================================================ -export const paymentAPI = { - getPlans: () => get('/payment/plans'), - createOrder: (data: any) => post('/payment/orders', data), - getOrders: (status?: string) => get(`/payment/orders${status ? `?status=${status}` : ''}`), - getOrder: (id: string) => get(`/payment/orders/${id}`), - payOrder: (id: string, method?: string) => post(`/payment/orders/${id}/pay`, { payment_method: method }), - getCoupons: () => get('/payment/coupons'), - useCoupon: (code: string) => post('/payment/coupons/use', { code }), - getServices: () => get('/payment/services'), - createCustomOrder: (data: any) => post('/payment/custom-services', data), - createServiceRequest: (data: any) => post('/payment/service-requests', data), -}; - -// ============================================================ -// 个人中心相关API -// ============================================================ -export const profileAPI = { - getProfile: () => get('/profile/'), - updateProfile: (data: any) => put('/profile/', data), - updatePassword: (data: { old_password: string; new_password: string }) => - put('/profile/password', data), - getLoginLogs: () => get('/profile/login-logs'), - getApiKeys: () => get('/profile/api-keys'), - createApiKey: (name?: string) => post('/profile/api-keys', { name }), - updateApiKey: (id: string, status: string) => put(`/profile/api-keys/${id}`, { status }), - getMembership: () => get('/profile/membership'), - getPurchasedApps: () => get('/profile/purchased-apps'), -}; - -// ============================================================ -// 通知相关API -// ============================================================ -export const notificationAPI = { - getNotifications: (params?: { type?: string; page?: number }) => { - const sp = new URLSearchParams(); - if (params?.type) sp.set('type', params.type); - if (params?.page) sp.set('page', String(params.page)); - const qs = sp.toString(); - return get(`/notifications/${qs ? `?${qs}` : ''}`); - }, - markRead: (id: string) => put(`/notifications/${id}/read`), - markAllRead: () => put('/notifications/read-all'), - getUnreadCount: () => get('/notifications/unread-count'), -}; - -// ============================================================ -// 资产管理相关API -// ============================================================ -export const assetsAPI = { - getAssets: (category?: string) => - get(`/assets/${category ? `?category=${category}` : ''}`), - createAsset: (data: any) => post('/assets/', data), - getAsset: (id: string) => get(`/assets/${id}`), - updateAsset: (id: string, data: any) => put(`/assets/${id}`, data), - deleteAsset: (id: string) => del(`/assets/${id}`), - getCategories: () => get('/assets/categories'), - getNovelChapters: (novelId: string) => get(`/assets/novels/${novelId}/chapters`), - addChapter: (novelId: string, data: any) => post(`/assets/novels/${novelId}/chapters`, data), -}; - -// ============================================================ -// 后台管理API -// ============================================================ -export const adminAPI = { - // 仪表盘 - getStats: () => get('/admin/dashboard/stats'), - getRecentLogs: () => get('/admin/dashboard/recent-logs'), - getRecentAudit: () => get('/admin/dashboard/recent-audit'), - // 用户管理 - getUsers: (params?: any) => { - const sp = new URLSearchParams(); - if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); - return get(`/admin/users/${sp.toString() ? `?${sp.toString()}` : ''}`); - }, - getUser: (id: string) => get(`/admin/users/${id}`), - banUser: (id: string) => put(`/admin/users/${id}/ban`), - unbanUser: (id: string) => put(`/admin/users/${id}/unban`), - getRegisterSettings: () => get('/admin/users/register-settings'), - updateRegisterSettings: (data: any) => put('/admin/users/register-settings', data), - // 模型管理 - getProviders: () => get('/admin/models/providers'), - addProvider: (data: any) => post('/admin/models/providers', data), - updateProvider: (id: string, data: any) => put(`/admin/models/providers/${id}`, data), - deleteProvider: (id: string) => del(`/admin/models/providers/${id}`), - fetchModels: (id: string) => post(`/admin/models/providers/${id}/fetch-models`), - addModels: (id: string, modelIds: string[]) => post(`/admin/models/providers/${id}/add-models`, { model_ids: modelIds }), - getProtocols: () => get('/admin/models/protocols'), - getAdminModels: (params?: any) => { - const sp = new URLSearchParams(); - if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); - return get(`/admin/models/${sp.toString() ? `?${sp.toString()}` : ''}`); - }, - addModel: (data: any) => post('/admin/models/', data), - updateModel: (id: string, data: any) => put(`/admin/models/${id}`, data), - deleteModel: (id: string) => del(`/admin/models/${id}`), - toggleModel: (id: string, isEnabled: boolean) => put(`/admin/models/${id}/toggle?is_enabled=${isEnabled}`), - setDefaultModel: (id: string) => put(`/admin/models/${id}/set-default`), - getModelStats: () => get('/admin/models/stats'), - // 网站设置 - getSiteSettings: () => get('/admin/settings/site'), - updateSiteSettings: (data: any) => put('/admin/settings/site', data), - getEmailConfig: () => get('/admin/settings/email'), - updateEmailConfig: (data: any) => put('/admin/settings/email', data), - testEmail: (params?: { test_to?: string; test_subject?: string; test_body?: string }) => { - const qs = new URLSearchParams(); - if (params?.test_to) qs.set('test_to', params.test_to); - if (params?.test_subject) qs.set('test_subject', params.test_subject); - if (params?.test_body) qs.set('test_body', params.test_body); - const qstr = qs.toString(); - return post(`/admin/settings/email/test${qstr ? `?${qstr}` : ''}`); - }, - generateEmailContent: (topic: string) => post(`/admin/settings/email/generate-content?topic=${encodeURIComponent(topic)}`), - getSmsConfig: () => get('/admin/settings/sms'), - updateSmsConfig: (data: any) => put('/admin/settings/sms', data), - testSms: () => post('/admin/settings/sms/test'), - // 支付配置 - getPaymentConfigs: () => get('/admin/payment/configs'), - updatePaymentConfig: (id: string, data: any) => put(`/admin/payment/configs/${id}`, data), - getAdminOrders: (params?: any) => { - const sp = new URLSearchParams(); - if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); - return get(`/admin/payment/orders${sp.toString() ? `?${sp.toString()}` : ''}`); - }, - refundOrder: (id: string) => put(`/admin/payment/orders/${id}/refund`), - // 增值服务 - getServices: (params?: any) => { - const sp = new URLSearchParams(); - if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); - return get(`/admin/services/${sp.toString() ? `?${sp.toString()}` : ''}`); - }, - addService: (data: any) => post('/admin/services/', data), - updateService: (id: string, data: any) => put(`/admin/services/${id}`, data), - deleteService: (id: string) => del(`/admin/services/${id}`), - getCustomOrders: (status?: string) => get(`/admin/services/custom-orders${status ? `?status=${status}` : ''}`), - updateCustomOrder: (id: string, data: any) => put(`/admin/services/custom-orders/${id}`, data), - getCustomRequests: (params?: any) => { - const sp = new URLSearchParams(); - if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); - return get(`/admin/services/custom-requests${sp.toString() ? `?${sp.toString()}` : ''}`); - }, - updateCustomRequest: (id: string, data: any) => put(`/admin/services/custom-requests/${id}`, data), - getAvailableAgents: () => get('/admin/services/agents'), - assignAgentToService: (id: string, agentId: string) => put(`/admin/services/${id}/agent?agent_id=${agentId}`), - // 会员管理 - getPlans: () => get('/admin/membership/plans'), - addPlan: (data: any) => post('/admin/membership/plans', data), - updatePlan: (id: string, data: any) => put(`/admin/membership/plans/${id}`, data), - deletePlan: (id: string) => del(`/admin/membership/plans/${id}`), - getSubscriptions: () => get('/admin/membership/subscriptions'), - getAutomationRules: () => get('/admin/membership/automation-rules'), - // 广告管理 - getAds: () => get('/admin/ads/'), - addAd: (data: any) => post('/admin/ads/', data), - updateAd: (id: string, data: any) => put(`/admin/ads/${id}`, data), - deleteAd: (id: string) => del(`/admin/ads/${id}`), - toggleAd: (id: string) => put(`/admin/ads/${id}/toggle`), - // 应用市场审核 - getPendingApps: () => get('/admin/market/apps/pending'), - approveApp: (id: string) => put(`/admin/market/apps/${id}/approve`), - rejectApp: (id: string) => put(`/admin/market/apps/${id}/reject`), - getAdminApps: () => get('/admin/market/apps'), - updateApp: (id: string, data: any) => put(`/admin/market/apps/${id}`, data), - getCoupons: () => get('/admin/market/coupons'), - addCoupon: (data: any) => post('/admin/market/coupons', data), - updateCoupon: (id: string, data: any) => put(`/admin/market/coupons/${id}`, data), - deleteCoupon: (id: string) => del(`/admin/market/coupons/${id}`), - assignCoupon: (id: string, userId: string) => post(`/admin/market/coupons/${id}/assign`, { user_id: userId }), - // 日志与风控 - getOperationLogs: (params?: any) => { - const sp = new URLSearchParams(); - if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); - return get(`/admin/logs/operations${sp.toString() ? `?${sp.toString()}` : ''}`); - }, - getTokenLogs: (params?: any) => { - const sp = new URLSearchParams(); - if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); - return get(`/admin/logs/token-logs${sp.toString() ? `?${sp.toString()}` : ''}`); - }, - getPaymentLogs: (params?: any) => { - const sp = new URLSearchParams(); - if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); - return get(`/admin/logs/payment-logs${sp.toString() ? `?${sp.toString()}` : ''}`); - }, - getAuditLogs: (params?: any) => { - const sp = new URLSearchParams(); - if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); - return get(`/admin/logs/audit-logs${sp.toString() ? `?${sp.toString()}` : ''}`); - }, - // 新增:日志统计 - getLogStats: () => get('/admin/logs/stats'), - // 新增:API调用统计 - getApiStats: (params?: any) => { - const sp = new URLSearchParams(); - if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); - return get(`/admin/logs/api-stats${sp.toString() ? `?${sp.toString()}` : ''}`); - }, - // 新增:慢请求记录 - getSlowRequests: (params?: any) => { - const sp = new URLSearchParams(); - if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); - return get(`/admin/logs/slow-requests${sp.toString() ? `?${sp.toString()}` : ''}`); - }, - // 新增:操作类型枚举 - getActionTypes: () => get('/admin/logs/action-types'), - // 新增:审计日志导出 - exportAuditLogs: (params?: any) => { - const sp = new URLSearchParams(); - if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); - return get(`/admin/logs/export/audit-logs${sp.toString() ? `?${sp.toString()}` : ''}`); - }, - exportOperationLogs: (params?: any) => { - const sp = new URLSearchParams(); - if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); - return get(`/admin/logs/export/operations${sp.toString() ? `?${sp.toString()}` : ''}`); - }, - exportTokenLogs: (params?: any) => { - const sp = new URLSearchParams(); - if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); - return get(`/admin/logs/export/token-logs${sp.toString() ? `?${sp.toString()}` : ''}`); - }, - getRiskRules: () => get('/admin/logs/risk-rules'), - addRiskRule: (data: any) => post('/admin/logs/risk-rules', data), - updateRiskRule: (id: string, data: any) => put(`/admin/logs/risk-rules/${id}`, data), - deleteRiskRule: (id: string) => del(`/admin/logs/risk-rules/${id}`), -}; - -// ===== AI自进化API ===== -export const evolutionAPI = { - // 驾驶舱面板 - getSkillTree: () => get('/evolution/dashboard/skill-tree'), - getTimeline: (eventType?: string) => - get('/evolution/dashboard/timeline' + (eventType ? `?event_type=${eventType}` : '')), - getStatistics: () => get('/evolution/dashboard/statistics'), - getProfile: () => get('/evolution/dashboard/profile'), - - // 技能管理 - getSkills: (params?: { state?: string; keyword?: string; sort_by?: string; order?: string }) => { - const query = new URLSearchParams(); - if (params?.state) query.append('state', params.state); - if (params?.keyword) query.append('keyword', params.keyword); - if (params?.sort_by) query.append('sort_by', params.sort_by); - if (params?.order) query.append('order', params.order); - const qs = query.toString(); - return get('/evolution/skills' + (qs ? `?${qs}` : '')); - }, - getSkillDetail: (name: string) => get(`/evolution/skills/${encodeURIComponent(name)}`), - updateSkill: (name: string, data: { description?: string; tags?: string[]; category?: string }) => - put(`/evolution/skills/${encodeURIComponent(name)}`, data), - deleteSkill: (name: string) => del(`/evolution/skills/${encodeURIComponent(name)}`), - freezeSkill: (name: string) => post(`/evolution/skills/${encodeURIComponent(name)}/freeze`), - unfreezeSkill: (name: string) => post(`/evolution/skills/${encodeURIComponent(name)}/unfreeze`), - getSkillHistory: (name: string) => get(`/evolution/skills/${encodeURIComponent(name)}/history`), - getSkillTrajectories: (name: string) => get(`/evolution/skills/${encodeURIComponent(name)}/trajectories`), - - // 事件 - getEvents: (params?: { event_type?: string; skill_name?: string; limit?: number }) => { - const query = new URLSearchParams(); - if (params?.event_type) query.append('event_type', params.event_type); - if (params?.skill_name) query.append('skill_name', params.skill_name); - if (params?.limit) query.append('limit', String(params.limit)); - const qs = query.toString(); - return get('/evolution/events' + (qs ? `?${qs}` : '')); - }, - - // 轨迹提交 - submitTrajectory: (data: { conversation_id: string; messages: any[] }) => - post('/evolution/trajectory/submit', data), - - // v2: 技能回滚 - rollbackSkill: (skillName: string, version: number) => - post(`/evolution/skills/${encodeURIComponent(skillName)}/rollback/${version}`), - - // v2: 发布到市场 - publishSkill: (skillName: string, data: { price: number; market_description: string }) => - post(`/evolution/skills/${encodeURIComponent(skillName)}/publish`, data), - - // v2: 团队共享 - shareToTeam: (skillName: string, teamId: string) => - post(`/evolution/skills/${encodeURIComponent(skillName)}/share-to-team`, { team_id: teamId }), - - // v2: 流水线状态 - getPipelineStatus: () => get('/evolution/pipeline/status'), - - // v2: 手动触发流水线 - triggerPipeline: () => post('/evolution/pipeline/trigger'), - - // v2: Watcher状态 - getWatcherStatus: () => get('/evolution/watcher/status'), - - // v2: 进化报告 - getReports: (reportType?: string) => { - const qs = reportType ? `?report_type=${reportType}` : ''; - return get(`/evolution/reports${qs}`); - }, - getReportDetail: (reportId: string) => get(`/evolution/reports/${reportId}`), -}; +/** + * AIFlow API 客户端 + * 统一管理所有前后端API请求 + */ +const API_BASE = '/app/api'; + +// 从localStorage获取token +function getToken(): string | null { + return localStorage.getItem('aiflow_token'); +} + +// 通用请求方法 +async function request( + endpoint: string, + options: RequestInit = {} +): Promise<{ code: number; data: T; message: string }> { + const token = getToken(); + const headers: Record = { + 'Content-Type': 'application/json', + ...(options.headers as Record || {}), + }; + if (token) { + headers['Authorization'] = `Bearer ${token}`; + } + const res = await fetch(`${API_BASE}${endpoint}`, { + ...options, + headers, + }); + if (res.status === 401) { + // 登录接口的401需要精确提示(用户名不存在 / 密码错误) + if (endpoint === '/auth/login') { + const err = await res.json().catch(() => ({ detail: '登录失败' })); + const error: any = new Error(err.detail || '登录失败'); + error.status = 401; + error.detail = err.detail; + throw error; + } + // 其他接口的401 = token过期/未登录,清除状态并跳转 + localStorage.removeItem('aiflow_token'); + localStorage.removeItem('aiflow_user'); + window.location.hash = '#/login'; + throw new Error('未登录'); + } + if (!res.ok) { + const err = await res.json().catch(() => ({ detail: '请求失败' })); + const error: any = new Error(err.detail || err.message || '请求失败'); + error.status = res.status; + error.response = { status: res.status, data: err }; + error.detail = err.detail; + throw error; + } + return res.json(); +} + +// GET请求 +function get(endpoint: string): Promise<{ code: number; data: T; message: string }> { + return request(endpoint); +} + +// POST请求 +function post(endpoint: string, body?: any): Promise<{ code: number; data: T; message: string }> { + return request(endpoint, { + method: 'POST', + body: body ? JSON.stringify(body) : undefined, + }); +} + +// PUT请求 +function put(endpoint: string, body?: any): Promise<{ code: number; data: T; message: string }> { + return request(endpoint, { + method: 'PUT', + body: body ? JSON.stringify(body) : undefined, + }); +} + +// DELETE请求 +function del(endpoint: string): Promise<{ code: number; data: T; message: string }> { + return request(endpoint, { method: 'DELETE' }); +} + +// ============================================================ +// 认证相关API +// ============================================================ +export const authAPI = { + login: (username: string, password: string) => + post('/auth/login', { username, password }), + register: (data: { username: string; password: string; email?: string; phone?: string }) => + post('/auth/register', data), + getMe: () => get('/auth/me'), + logout: () => post('/auth/logout'), + getSettings: () => get('/auth/settings'), +}; + +// ============================================================ +// 聊天相关API +// ============================================================ +export const chatAPI = { + getConversations: (status?: string) => + get(`/chat/conversations${status ? `?status=${status}` : ''}`), + createConversation: (data: { title: string; model_id?: string }) => + post('/chat/conversations', data), + updateConversation: (id: string, data: any) => + put(`/chat/conversations/${id}`, data), + deleteConversation: (id: string) => + del(`/chat/conversations/${id}`), + getMessages: (convId: string) => + get(`/chat/conversations/${convId}/messages`), + sendMessage: (convId: string, data: { content: string; model_id?: string; agent_ids?: string[]; skill_ids?: string[]; knowledge_ids?: string[]; use_tools?: boolean }) => + post(`/chat/conversations/${convId}/messages`, data), + clearContext: (convId: string) => + post(`/chat/conversations/${convId}/clear`), + convertToKnowledgeGraph: (convId: string) => + post(`/chat/conversations/${convId}/to-knowledge-graph`), + getModels: () => get('/chat/models'), +}; + +// ============================================================ +// 频道相关API +// ============================================================ +export const channelAPI = { + getChannels: () => get('/channels/'), + createChannel: (data: any) => post('/channels/', data), + getChannel: (id: string) => get(`/channels/${id}`), + updateChannel: (id: string, data: any) => put(`/channels/${id}`, data), + deleteChannel: (id: string) => del(`/channels/${id}`), + connectChannel: (id: string) => post(`/channels/${id}/connect`), + disconnectChannel: (id: string) => post(`/channels/${id}/disconnect`), + getPlatformConfigs: () => get('/channels/platforms/config'), + webhookMessage: (id: string, data: { from_user: string; content: string; message_type?: string }) => + post(`/channels/${id}/webhook`, data), + getMembers: (id: string) => get(`/channels/${id}/members`), + addMember: (id: string, userId: string) => post(`/channels/${id}/members`, { user_id: userId }), + removeMember: (id: string, userId: string) => del(`/channels/${id}/members/${userId}`), + getMessages: (id: string, params?: { limit?: number; offset?: number }) => { + const sp = new URLSearchParams(); + if (params?.limit) sp.set('limit', String(params.limit)); + if (params?.offset) sp.set('offset', String(params.offset)); + const qs = sp.toString(); + return get(`/channels/${id}/messages${qs ? `?${qs}` : ''}`); + }, + sendMessage: (id: string, content: string) => post(`/channels/${id}/messages`, { content }), + authorize: (platform: string) => post('/channels/authorize', { platform }), + scanAuthorize: (platform: string, credentials?: Record) => post(`/channels/authorize/scan?platform=${platform}`, credentials ? { credentials } : {}), + scanAuthorizeStatus: (platform: string) => post(`/channels/authorize/scan/status?platform=${platform}`), +}; + +// ============================================================ +// 知识库相关API +// ============================================================ +export const knowledgeAPI = { + getBases: () => get('/knowledge/bases'), + createBase: (data: any) => post('/knowledge/bases', data), + getBase: (id: string) => get(`/knowledge/bases/${id}`), + updateBase: (id: string, data: any) => put(`/knowledge/bases/${id}`, data), + deleteBase: (id: string) => del(`/knowledge/bases/${id}`), + getDocuments: (baseId: string) => get(`/knowledge/bases/${baseId}/documents`), + getDocument: (baseId: string, docId: string) => get(`/knowledge/bases/${baseId}/documents/${docId}`), + uploadDocument: (baseId: string, data: any) => post(`/knowledge/bases/${baseId}/documents`, data), + deleteDocument: (baseId: string, docId: string) => del(`/knowledge/bases/${baseId}/documents/${docId}`), + getDirectories: () => get('/knowledge/directories'), + createDirectory: (data: any) => post('/knowledge/directories', data), + buildGraph: (baseId: string) => post(`/knowledge/bases/${baseId}/build-graph`), + getGraph: (baseId: string) => get(`/knowledge/bases/${baseId}/graph`), + searchQA: (baseId: string, question: string) => post('/knowledge/qa', { base_id: baseId, question }), + searchDocuments: (baseId: string, keyword: string) => get(`/knowledge/bases/${baseId}/search?keyword=${encodeURIComponent(keyword)}`), +}; + +// ============================================================ +// 应用市场相关API +// ============================================================ +export const marketAPI = { + getApps: (params?: { type?: string; source?: string; search?: string; page?: number; page_size?: number }) => { + const searchParams = new URLSearchParams(); + if (params?.type) searchParams.set('type', params.type); + if (params?.source) searchParams.set('source', params.source); + if (params?.search) searchParams.set('search', params.search); + if (params?.page) searchParams.set('page', String(params.page)); + if (params?.page_size) searchParams.set('page_size', String(params.page_size)); + const qs = searchParams.toString(); + return get(`/market/apps${qs ? `?${qs}` : ''}`); + }, + getApp: (id: string) => get(`/market/apps/${id}`), + createApp: (data: any) => post('/market/apps', data), + purchaseApp: (id: string) => post(`/market/apps/${id}/purchase`), + installApp: (id: string) => post(`/market/apps/${id}/install`), + uninstallApp: (id: string) => del(`/market/apps/${id}/uninstall`), + reviewApp: (id: string, data: { rating: number; content?: string }) => + post(`/market/apps/${id}/review`, data), + getMyApps: () => get('/market/my-apps'), + getReviews: (appId: string) => get(`/market/reviews/${appId}`), +}; + +// ============================================================ +// Token相关API +// ============================================================ +export const tokenAPI = { + getOverview: () => get('/token/overview'), + getRankings: () => get('/token/rankings'), + getLogs: (params?: any) => { + const sp = new URLSearchParams(); + if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); + const qs = sp.toString(); + return get(`/token/logs${qs ? `?${qs}` : ''}`); + }, + getBalance: () => get('/token/balance'), + recharge: (amount: number) => post('/token/recharge', { amount }), +}; + +// ============================================================ +// 付费相关API +// ============================================================ +export const paymentAPI = { + getPlans: () => get('/payment/plans'), + createOrder: (data: any) => post('/payment/orders', data), + getOrders: (status?: string) => get(`/payment/orders${status ? `?status=${status}` : ''}`), + getOrder: (id: string) => get(`/payment/orders/${id}`), + payOrder: (id: string, method?: string) => post(`/payment/orders/${id}/pay`, { payment_method: method }), + getCoupons: () => get('/payment/coupons'), + useCoupon: (code: string) => post('/payment/coupons/use', { code }), + getServices: () => get('/payment/services'), + createCustomOrder: (data: any) => post('/payment/custom-services', data), + createServiceRequest: (data: any) => post('/payment/service-requests', data), +}; + +// ============================================================ +// 个人中心相关API +// ============================================================ +export const profileAPI = { + getProfile: () => get('/profile/'), + updateProfile: (data: any) => put('/profile/', data), + updatePassword: (data: { old_password: string; new_password: string }) => + put('/profile/password', data), + getLoginLogs: () => get('/profile/login-logs'), + getApiKeys: () => get('/profile/api-keys'), + createApiKey: (name?: string) => post('/profile/api-keys', { name }), + updateApiKey: (id: string, status: string) => put(`/profile/api-keys/${id}`, { status }), + getMembership: () => get('/profile/membership'), + getPurchasedApps: () => get('/profile/purchased-apps'), +}; + +// ============================================================ +// 通知相关API +// ============================================================ +export const notificationAPI = { + getNotifications: (params?: { type?: string; page?: number }) => { + const sp = new URLSearchParams(); + if (params?.type) sp.set('type', params.type); + if (params?.page) sp.set('page', String(params.page)); + const qs = sp.toString(); + return get(`/notifications/${qs ? `?${qs}` : ''}`); + }, + markRead: (id: string) => put(`/notifications/${id}/read`), + markAllRead: () => put('/notifications/read-all'), + getUnreadCount: () => get('/notifications/unread-count'), +}; + +// ============================================================ +// 资产管理相关API +// ============================================================ +export const assetsAPI = { + getAssets: (category?: string) => + get(`/assets/${category ? `?category=${category}` : ''}`), + createAsset: (data: any) => post('/assets/', data), + getAsset: (id: string) => get(`/assets/${id}`), + updateAsset: (id: string, data: any) => put(`/assets/${id}`, data), + deleteAsset: (id: string) => del(`/assets/${id}`), + getCategories: () => get('/assets/categories'), + getNovelChapters: (novelId: string) => get(`/assets/novels/${novelId}/chapters`), + addChapter: (novelId: string, data: any) => post(`/assets/novels/${novelId}/chapters`, data), +}; + +// ============================================================ +// 后台管理API +// ============================================================ +export const adminAPI = { + // 仪表盘 + getStats: () => get('/admin/dashboard/stats'), + getRecentLogs: () => get('/admin/dashboard/recent-logs'), + getRecentAudit: () => get('/admin/dashboard/recent-audit'), + // 用户管理 + getUsers: (params?: any) => { + const sp = new URLSearchParams(); + if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); + return get(`/admin/users/${sp.toString() ? `?${sp.toString()}` : ''}`); + }, + getUser: (id: string) => get(`/admin/users/${id}`), + banUser: (id: string) => put(`/admin/users/${id}/ban`), + unbanUser: (id: string) => put(`/admin/users/${id}/unban`), + getRegisterSettings: () => get('/admin/users/register-settings'), + updateRegisterSettings: (data: any) => put('/admin/users/register-settings', data), + // 模型管理 + getProviders: () => get('/admin/models/providers'), + addProvider: (data: any) => post('/admin/models/providers', data), + updateProvider: (id: string, data: any) => put(`/admin/models/providers/${id}`, data), + deleteProvider: (id: string) => del(`/admin/models/providers/${id}`), + fetchModels: (id: string) => post(`/admin/models/providers/${id}/fetch-models`), + addModels: (id: string, modelIds: string[]) => post(`/admin/models/providers/${id}/add-models`, { model_ids: modelIds }), + getProtocols: () => get('/admin/models/protocols'), + getAdminModels: (params?: any) => { + const sp = new URLSearchParams(); + if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); + return get(`/admin/models/${sp.toString() ? `?${sp.toString()}` : ''}`); + }, + addModel: (data: any) => post('/admin/models/', data), + updateModel: (id: string, data: any) => put(`/admin/models/${id}`, data), + deleteModel: (id: string) => del(`/admin/models/${id}`), + toggleModel: (id: string, isEnabled: boolean) => put(`/admin/models/${id}/toggle?is_enabled=${isEnabled}`), + setDefaultModel: (id: string) => put(`/admin/models/${id}/set-default`), + // Phase 2: Enhanced model management + testModelConnection: (id: string) => post(`/admin/models/${id}/test-connection`), + fetchProviderModels: (id: string) => post(`/admin/models/providers/${id}/fetch-models`), + createAdminModel: (data: any) => post('/admin/models/', data), + updateAdminModel: (id: string, data: any) => put(`/admin/models/${id}`, data), + deleteAdminModel: (id: string) => del(`/admin/models/${id}`), + getModelStats: () => get('/admin/models/stats'), + // 网站设置 + getSiteSettings: () => get('/admin/settings/site'), + updateSiteSettings: (data: any) => put('/admin/settings/site', data), + getEmailConfig: () => get('/admin/settings/email'), + updateEmailConfig: (data: any) => put('/admin/settings/email', data), + testEmail: (params?: { test_to?: string; test_subject?: string; test_body?: string }) => { + const qs = new URLSearchParams(); + if (params?.test_to) qs.set('test_to', params.test_to); + if (params?.test_subject) qs.set('test_subject', params.test_subject); + if (params?.test_body) qs.set('test_body', params.test_body); + const qstr = qs.toString(); + return post(`/admin/settings/email/test${qstr ? `?${qstr}` : ''}`); + }, + generateEmailContent: (topic: string) => post(`/admin/settings/email/generate-content?topic=${encodeURIComponent(topic)}`), + getSmsConfig: () => get('/admin/settings/sms'), + updateSmsConfig: (data: any) => put('/admin/settings/sms', data), + testSms: () => post('/admin/settings/sms/test'), + // 支付配置 + getPaymentConfigs: () => get('/admin/payment/configs'), + updatePaymentConfig: (id: string, data: any) => put(`/admin/payment/configs/${id}`, data), + getAdminOrders: (params?: any) => { + const sp = new URLSearchParams(); + if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); + return get(`/admin/payment/orders${sp.toString() ? `?${sp.toString()}` : ''}`); + }, + refundOrder: (id: string) => put(`/admin/payment/orders/${id}/refund`), + // 增值服务 + getServices: (params?: any) => { + const sp = new URLSearchParams(); + if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); + return get(`/admin/services/${sp.toString() ? `?${sp.toString()}` : ''}`); + }, + addService: (data: any) => post('/admin/services/', data), + updateService: (id: string, data: any) => put(`/admin/services/${id}`, data), + deleteService: (id: string) => del(`/admin/services/${id}`), + getCustomOrders: (status?: string) => get(`/admin/services/custom-orders${status ? `?status=${status}` : ''}`), + updateCustomOrder: (id: string, data: any) => put(`/admin/services/custom-orders/${id}`, data), + getCustomRequests: (params?: any) => { + const sp = new URLSearchParams(); + if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); + return get(`/admin/services/custom-requests${sp.toString() ? `?${sp.toString()}` : ''}`); + }, + updateCustomRequest: (id: string, data: any) => put(`/admin/services/custom-requests/${id}`, data), + getAvailableAgents: () => get('/admin/services/agents'), + assignAgentToService: (id: string, agentId: string) => put(`/admin/services/${id}/agent?agent_id=${agentId}`), + // 会员管理 + getPlans: () => get('/admin/membership/plans'), + addPlan: (data: any) => post('/admin/membership/plans', data), + updatePlan: (id: string, data: any) => put(`/admin/membership/plans/${id}`, data), + deletePlan: (id: string) => del(`/admin/membership/plans/${id}`), + getSubscriptions: () => get('/admin/membership/subscriptions'), + getAutomationRules: () => get('/admin/membership/automation-rules'), + // 广告管理 + getAds: () => get('/admin/ads/'), + addAd: (data: any) => post('/admin/ads/', data), + updateAd: (id: string, data: any) => put(`/admin/ads/${id}`, data), + deleteAd: (id: string) => del(`/admin/ads/${id}`), + toggleAd: (id: string) => put(`/admin/ads/${id}/toggle`), + // 应用市场审核 + getPendingApps: () => get('/admin/market/apps/pending'), + approveApp: (id: string) => put(`/admin/market/apps/${id}/approve`), + rejectApp: (id: string) => put(`/admin/market/apps/${id}/reject`), + getAdminApps: () => get('/admin/market/apps'), + updateApp: (id: string, data: any) => put(`/admin/market/apps/${id}`, data), + getCoupons: () => get('/admin/market/coupons'), + addCoupon: (data: any) => post('/admin/market/coupons', data), + updateCoupon: (id: string, data: any) => put(`/admin/market/coupons/${id}`, data), + deleteCoupon: (id: string) => del(`/admin/market/coupons/${id}`), + assignCoupon: (id: string, userId: string) => post(`/admin/market/coupons/${id}/assign`, { user_id: userId }), + // 日志与风控 + getOperationLogs: (params?: any) => { + const sp = new URLSearchParams(); + if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); + return get(`/admin/logs/operations${sp.toString() ? `?${sp.toString()}` : ''}`); + }, + getTokenLogs: (params?: any) => { + const sp = new URLSearchParams(); + if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); + return get(`/admin/logs/token-logs${sp.toString() ? `?${sp.toString()}` : ''}`); + }, + getPaymentLogs: (params?: any) => { + const sp = new URLSearchParams(); + if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); + return get(`/admin/logs/payment-logs${sp.toString() ? `?${sp.toString()}` : ''}`); + }, + getAuditLogs: (params?: any) => { + const sp = new URLSearchParams(); + if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); + return get(`/admin/logs/audit-logs${sp.toString() ? `?${sp.toString()}` : ''}`); + }, + // 新增:日志统计 + getLogStats: () => get('/admin/logs/stats'), + // 新增:API调用统计 + getApiStats: (params?: any) => { + const sp = new URLSearchParams(); + if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); + return get(`/admin/logs/api-stats${sp.toString() ? `?${sp.toString()}` : ''}`); + }, + // 新增:慢请求记录 + getSlowRequests: (params?: any) => { + const sp = new URLSearchParams(); + if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); + return get(`/admin/logs/slow-requests${sp.toString() ? `?${sp.toString()}` : ''}`); + }, + // 新增:操作类型枚举 + getActionTypes: () => get('/admin/logs/action-types'), + // 新增:审计日志导出 + exportAuditLogs: (params?: any) => { + const sp = new URLSearchParams(); + if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); + return get(`/admin/logs/export/audit-logs${sp.toString() ? `?${sp.toString()}` : ''}`); + }, + exportOperationLogs: (params?: any) => { + const sp = new URLSearchParams(); + if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); + return get(`/admin/logs/export/operations${sp.toString() ? `?${sp.toString()}` : ''}`); + }, + exportTokenLogs: (params?: any) => { + const sp = new URLSearchParams(); + if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); + return get(`/admin/logs/export/token-logs${sp.toString() ? `?${sp.toString()}` : ''}`); + }, + getRiskRules: () => get('/admin/logs/risk-rules'), + addRiskRule: (data: any) => post('/admin/logs/risk-rules', data), + updateRiskRule: (id: string, data: any) => put(`/admin/logs/risk-rules/${id}`, data), + deleteRiskRule: (id: string) => del(`/admin/logs/risk-rules/${id}`), + // 技能管理 (Phase 2.2) + getAdminSkills: (params?: any) => { + const sp = new URLSearchParams(); + if (params) Object.entries(params).forEach(([k, v]) => { if (v !== undefined && v !== null && v !== '') sp.set(k, String(v)); }); + return get(`/admin/skills/${sp.toString() ? `?${sp.toString()}` : ''}`); + }, + getAdminSkill: (id: string) => get(`/admin/skills/${id}`), + createAdminSkill: (data: any) => post('/admin/skills/', data), + updateAdminSkill: (id: string, data: any) => put(`/admin/skills/${id}`, data), + deleteAdminSkill: (id: string) => del(`/admin/skills/${id}`), + changeAdminSkillState: (id: string, data: any) => put(`/admin/skills/${id}/state`, data), + getAdminSkillVersions: (id: string) => get(`/admin/skills/${id}/versions`), + rollbackAdminSkill: (id: string, data: any) => post(`/admin/skills/${id}/rollback`, data), + publishAdminSkill: (id: string, data: any) => post(`/admin/skills/${id}/publish`, data), + unpublishAdminSkill: (id: string) => post(`/admin/skills/${id}/unpublish`), + getAdminSkillsStats: () => get('/admin/skills/stats/overview'), +}; +// ===== AI自进化API ===== +export const evolutionAPI = { + // 驾驶舱面板 + getSkillTree: () => get('/evolution/dashboard/skill-tree'), + getTimeline: (eventType?: string) => + get('/evolution/dashboard/timeline' + (eventType ? `?event_type=${eventType}` : '')), + getStatistics: () => get('/evolution/dashboard/statistics'), + getProfile: () => get('/evolution/dashboard/profile'), + + // 技能管理 + getSkills: (params?: { state?: string; keyword?: string; sort_by?: string; order?: string }) => { + const query = new URLSearchParams(); + if (params?.state) query.append('state', params.state); + if (params?.keyword) query.append('keyword', params.keyword); + if (params?.sort_by) query.append('sort_by', params.sort_by); + if (params?.order) query.append('order', params.order); + const qs = query.toString(); + return get('/evolution/skills' + (qs ? `?${qs}` : '')); + }, + getSkillDetail: (name: string) => get(`/evolution/skills/${encodeURIComponent(name)}`), + updateSkill: (name: string, data: { description?: string; tags?: string[]; category?: string }) => + put(`/evolution/skills/${encodeURIComponent(name)}`, data), + deleteSkill: (name: string) => del(`/evolution/skills/${encodeURIComponent(name)}`), + freezeSkill: (name: string) => post(`/evolution/skills/${encodeURIComponent(name)}/freeze`), + unfreezeSkill: (name: string) => post(`/evolution/skills/${encodeURIComponent(name)}/unfreeze`), + getSkillHistory: (name: string) => get(`/evolution/skills/${encodeURIComponent(name)}/history`), + getSkillTrajectories: (name: string) => get(`/evolution/skills/${encodeURIComponent(name)}/trajectories`), + + // 事件 + getEvents: (params?: { event_type?: string; skill_name?: string; limit?: number }) => { + const query = new URLSearchParams(); + if (params?.event_type) query.append('event_type', params.event_type); + if (params?.skill_name) query.append('skill_name', params.skill_name); + if (params?.limit) query.append('limit', String(params.limit)); + const qs = query.toString(); + return get('/evolution/events' + (qs ? `?${qs}` : '')); + }, + + // 轨迹提交 + submitTrajectory: (data: { conversation_id: string; messages: any[] }) => + post('/evolution/trajectory/submit', data), + + // v2: 技能回滚 + rollbackSkill: (skillName: string, version: number) => + post(`/evolution/skills/${encodeURIComponent(skillName)}/rollback/${version}`), + + // v2: 发布到市场 + publishSkill: (skillName: string, data: { price: number; market_description: string }) => + post(`/evolution/skills/${encodeURIComponent(skillName)}/publish`, data), + + // v2: 团队共享 + shareToTeam: (skillName: string, teamId: string) => + post(`/evolution/skills/${encodeURIComponent(skillName)}/share-to-team`, { team_id: teamId }), + + // v2: 流水线状态 + getPipelineStatus: () => get('/evolution/pipeline/status'), + + // v2: 手动触发流水线 + triggerPipeline: () => post('/evolution/pipeline/trigger'), + + // v2: Watcher状态 + getWatcherStatus: () => get('/evolution/watcher/status'), + + // v2: 进化报告 + getReports: (reportType?: string) => { + const qs = reportType ? `?report_type=${reportType}` : ''; + return get(`/evolution/reports${qs}`); + }, + getReportDetail: (reportId: string) => get(`/evolution/reports/${reportId}`), +}; diff --git a/frontend/src/components/layout/AdminLayout.tsx b/frontend/src/components/layout/AdminLayout.tsx index 36bcb8c..e3cedbd 100644 --- a/frontend/src/components/layout/AdminLayout.tsx +++ b/frontend/src/components/layout/AdminLayout.tsx @@ -1,124 +1,126 @@ -import React, { useState } from 'react'; -import { Outlet, NavLink, useNavigate } from 'react-router-dom'; -import { useAuth } from '@/contexts/AuthContext'; -import './AdminLayout.css'; - -const adminNavItems = [ - { path: '/admin', icon: 'dashboard', label: '平台驾驶舱', exact: true }, - { path: '/admin/users', icon: 'users', label: '用户管理' }, - { path: '/admin/models', icon: 'models', label: '大模型库管理' }, - { path: '/admin/settings', icon: 'settings', label: '网站基础设置' }, - { path: '/admin/payment', icon: 'payment', label: '支付渠道配置' }, - { path: '/admin/services', icon: 'services', label: '增值服务管理' }, - { path: '/admin/membership', icon: 'membership', label: '会员订阅体系' }, - { path: '/admin/ads', icon: 'ads', label: '广告位管理' }, - { path: '/admin/market', icon: 'market', label: '应用市场审核' }, - { path: '/admin/logs', icon: 'logs', label: '日志与风控' }, -]; - -const adminIconPaths: Record = { - dashboard: , - users: , - models: , - settings: , - payment: , - services: , - membership: , - ads: , - market: , - logs: , -}; - -const AdminLayout: React.FC = () => { - const { user, logout } = useAuth(); - const navigate = useNavigate(); - const [collapsed, setCollapsed] = useState(false); - - if (!user || user.role !== 'admin') { - navigate('/login'); - return null; - } - - return ( -
- {/* Admin Sidebar */} - - - {/* Admin Main Content */} -
-
-
- 欢迎,{user.username} -
-
- - - 返回前台 - -
-
- {user.username.charAt(0).toUpperCase()} -
- {user.username} -
-
-
-
- -
-
-
- ); -}; - -export default AdminLayout; +import React, { useState } from 'react'; +import { Outlet, NavLink, useNavigate } from 'react-router-dom'; +import { useAuth } from '@/contexts/AuthContext'; +import './AdminLayout.css'; + +const adminNavItems = [ + { path: '/admin', icon: 'dashboard', label: '平台驾驶舱', exact: true }, + { path: '/admin/users', icon: 'users', label: '用户管理' }, + { path: '/admin/models', icon: 'models', label: '大模型库管理' }, + { path: '/admin/settings', icon: 'settings', label: '网站基础设置' }, + { path: '/admin/payment', icon: 'payment', label: '支付渠道配置' }, + { path: '/admin/services', icon: 'services', label: '增值服务管理' }, + { path: '/admin/membership', icon: 'membership', label: '会员订阅体系' }, + { path: '/admin/ads', icon: 'ads', label: '广告位管理' }, + { path: '/admin/market', icon: 'market', label: '应用市场审核' }, + { path: '/admin/logs', icon: 'logs', label: '日志与风控' }, + + { path: '/admin/skills', icon: 'skills', label: '技能管理' },]; + +const adminIconPaths: Record = { + dashboard: , + users: , + models: , + settings: , + payment: , + services: , + membership: , + ads: , + market: , + logs: , + + skills: ,}; + +const AdminLayout: React.FC = () => { + const { user, logout } = useAuth(); + const navigate = useNavigate(); + const [collapsed, setCollapsed] = useState(false); + + if (!user || user.role !== 'admin') { + navigate('/login'); + return null; + } + + return ( +
+ {/* Admin Sidebar */} + + + {/* Admin Main Content */} +
+
+
+ 欢迎,{user.username} +
+
+ + + 返回前台 + +
+
+ {user.username.charAt(0).toUpperCase()} +
+ {user.username} +
+
+
+
+ +
+
+
+ ); +}; + +export default AdminLayout; diff --git a/frontend/src/pages/admin/Agents.tsx b/frontend/src/pages/admin/Agents.tsx new file mode 100644 index 0000000..a2bc333 --- /dev/null +++ b/frontend/src/pages/admin/Agents.tsx @@ -0,0 +1,702 @@ +import React, { useState, useEffect } from 'react'; +import { adminAPI } from '@/api'; + +interface Agent { + id: string; + name: string; + type: string; + source: string; + publisher_id: string; + publisher_name?: string; + description?: string; + icon?: string; + tutorial?: string; + price: number; + is_member_free: boolean; + join_promotion: boolean; + status: string; + rating_avg: number; + review_count: number; + install_count: number; + skill_content?: string; + agent_endpoint?: string; + config?: AgentConfig; + linked_skills?: LinkedSkill[]; + created_at?: string; + updated_at?: string; +} + +interface AgentConfig { + system_prompt?: string; + temperature?: number; + max_tokens?: number; + model_id?: string; + skills?: string[]; + tools?: string[]; + memory_enabled?: boolean; + reasoning_enabled?: boolean; +} + +interface LinkedSkill { + id: string; + skill_name: string; + state: string; + version: number; +} + +interface AgentStats { + total_agents: number; + published_agents: number; + pending_agents: number; + official_agents: number; + user_agents: number; + total_installs: number; + average_rating: number; + by_source: Record; +} + +interface AvailableModel { + id: string; + name: string; + provider: string; + model_type?: string; +} + +const AGENT_TYPES = [ + { id: 'skill', label: '技能' }, + { id: 'agent', label: 'Agent' }, + { id: 'mcp', label: 'MCP' }, +]; + +const Agents: React.FC = () => { + const [agents, setAgents] = useState([]); + const [total, setTotal] = useState(0); + const [page, setPage] = useState(1); + const [loading, setLoading] = useState(false); + const [stats, setStats] = useState(null); + + // Filters + const [keyword, setKeyword] = useState(''); + const [filterStatus, setFilterStatus] = useState(''); + const [filterSource, setFilterSource] = useState(''); + + // Modal + const [showModal, setShowModal] = useState(false); + const [editingAgent, setEditingAgent] = useState(null); + const [form, setForm] = useState({ + name: '', + description: '', + icon: '', + price: '0', + is_member_free: false, + system_prompt: '', + temperature: '0.7', + max_tokens: '2048', + model_id: '', + skills: [] as string[], + memory_enabled: false, + reasoning_enabled: false, + }); + + // Detail drawer + const [showDetail, setShowDetail] = useState(false); + const [detailAgent, setDetailAgent] = useState(null); + + // Available skills for linking + const [availableSkills, setAvailableSkills] = useState([]); + const [availableModels, setAvailableModels] = useState([]); + + // Toast + const [toast, setToast] = useState<{ msg: string; type: 'success' | 'error' } | null>(null); + const showToast = (msg: string, type: 'success' | 'error' = 'success') => { + setToast({ msg, type }); + setTimeout(() => setToast(null), 3000); + }; + + // Load agents + const loadAgents = async () => { + setLoading(true); + try { + const params: any = { page, page_size: 15 }; + if (keyword) params.keyword = keyword; + if (filterStatus) params.status = filterStatus; + if (filterSource) params.source = filterSource; + const result = await adminAPI.getAdminAgents(params) as any; + const data = result.data || result; + setAgents(data.items || []); + setTotal(data.total || 0); + } catch (e: any) { + showToast(`加载失败: ${e.message || e}`, 'error'); + } finally { + setLoading(false); + } + }; + + // Load stats + const loadStats = async () => { + try { + const result = await adminAPI.getAdminAgentsStats() as any; + const data = result.data || result; + setStats(data); + } catch {} + }; + + // Load available skills & models + const loadAvailable = async () => { + try { + const [skillsResult, modelsResult] = await Promise.all([ + adminAPI.getAdminAvailableSkills({ page_size: 50 }) as any, + adminAPI.getAdminAvailableModels() as any, + ]); + const skills = (skillsResult.data || skillsResult).items || []; + const models = (modelsResult.data || modelsResult) || []; + setAvailableSkills(skills); + setAvailableModels(models); + } catch {} + }; + + useEffect(() => { + loadAgents(); + }, [page, keyword, filterStatus, filterSource]); + + useEffect(() => { + loadStats(); + }, []); + + // Search + const handleSearch = () => { + setPage(1); + loadAgents(); + }; + + // Open create + const openCreate = async () => { + setEditingAgent(null); + await loadAvailable(); + setForm({ + name: '', + description: '', + icon: '', + price: '0', + is_member_free: false, + system_prompt: '你是一个智能助手。', + temperature: '0.7', + max_tokens: '2048', + model_id: '', + skills: [], + memory_enabled: false, + reasoning_enabled: false, + }); + setShowModal(true); + }; + + // Open edit + const openEdit = async (agent: Agent) => { + setEditingAgent(agent); + await loadAvailable(); + + const cfg = agent.config || {}; + setForm({ + name: agent.name, + description: agent.description || '', + icon: agent.icon || '', + price: String(agent.price), + is_member_free: agent.is_member_free, + system_prompt: cfg.system_prompt || '', + temperature: String(cfg.temperature || 0.7), + max_tokens: String(cfg.max_tokens || 2048), + model_id: cfg.model_id || '', + skills: cfg.skills || [], + memory_enabled: cfg.memory_enabled || false, + reasoning_enabled: cfg.reasoning_enabled || false, + }); + setShowModal(true); + }; + + // Open detail + const openDetail = async (agent: Agent) => { + setDetailAgent(agent); + setShowDetail(true); + }; + + // Save agent + const handleSave = async () => { + if (!form.name.trim()) { + showToast('Agent 名称不能为空', 'error'); + return; + } + + const payload: any = { + name: form.name, + description: form.description, + icon: form.icon || undefined, + price: parseFloat(form.price) || 0, + is_member_free: form.is_member_free, + config: { + system_prompt: form.system_prompt, + temperature: parseFloat(form.temperature) || 0.7, + max_tokens: parseInt(form.max_tokens) || 2048, + model_id: form.model_id || undefined, + skills: form.skills, + memory_enabled: form.memory_enabled, + reasoning_enabled: form.reasoning_enabled, + }, + }; + + try { + if (editingAgent) { + await adminAPI.updateAdminAgent(editingAgent.id, payload); + showToast('Agent 已更新'); + } else { + await adminAPI.createAdminAgent(payload); + showToast('Agent 创建成功'); + } + setShowModal(false); + loadAgents(); + loadStats(); + } catch (e: any) { + showToast(`保存失败: ${e.message || e}`, 'error'); + } + }; + + // Toggle agent + const handleToggle = async (agent: Agent) => { + const isActive = agent.status === 'published'; + try { + await adminAPI.toggleAdminAgent(agent.id, !isActive); + showToast(`Agent 已${isActive ? '禁用' : '启用'}`); + loadAgents(); + loadStats(); + } catch (e: any) { + showToast(`操作失败: ${e.message || e}`, 'error'); + } + }; + + // Delete agent + const handleDelete = async (agent: Agent) => { + if (!confirm(`确定要删除 Agent "${agent.name}" 吗?此操作不可恢复。`)) return; + try { + await adminAPI.deleteAdminAgent(agent.id); + showToast('Agent 已删除'); + loadAgents(); + loadStats(); + } catch (e: any) { + showToast(`删除失败: ${e.message || e}`, 'error'); + } + }; + + // Toggle skill in selection + const toggleSkill = (skillId: string) => { + setForm(f => ({ + ...f, + skills: f.skills.includes(skillId) + ? f.skills.filter(s => s !== skillId) + : [...f.skills, skillId], + })); + }; + + const PAGE_SIZE = 15; + const totalPages = Math.ceil(total / PAGE_SIZE); + + const statusColor = (status: string) => { + if (status === 'published') return { bg: '#D1FAE5', color: '#10B981' }; + if (status === 'pending') return { bg: '#FEF3C7', color: '#F59E0B' }; + return { bg: '#FEE2E2', color: '#EF4444' }; + }; + + return ( +
+ {/* Toast */} + {toast && ( +
+ {toast.msg} +
+ )} + + {/* Header */} +
+
+

Agent 管理

+

+ 管理 AI Agent/Skill/MCP,支持配置、关联技能和模型 +

+
+ +
+ + {/* Stats */} + {stats && ( +
+
+
{stats.total_agents}
+
Agent 总数
+
+
+
{stats.published_agents}
+
已发布
+
+
+
{stats.pending_agents}
+
待发布
+
+
+
{stats.total_installs.toLocaleString()}
+
总安装量
+
+
+
{stats.average_rating.toFixed(1)}
+
平均评分
+
+
+
{stats.official_agents} / {stats.user_agents}
+
官方 / 用户
+
+
+ )} + + {/* Filters */} +
+ setKeyword(e.target.value)} + onKeyDown={e => e.key === 'Enter' && handleSearch()} + style={{ maxWidth: 280 }} /> + + + + {(keyword || filterStatus || filterSource) && ( + + )} +
+ + {/* Table */} +
+ + + + + + + + + + + + + + + + {loading ? ( + + ) : agents.length === 0 ? ( + + ) : agents.map(agent => { + const sc = statusColor(agent.status); + return ( + + + + + + + + + + + + ); + })} + +
Agent类型来源状态价格安装量评分更新时间操作
加载中...
暂无 Agent
+
+ {agent.icon ? ( + + ) : ( +
+ 🤖 +
+ )} +
+
{agent.name}
+ {agent.description && ( +
+ {agent.description} +
+ )} +
+
+
+ + {AGENT_TYPES.find(t => t.id === agent.type)?.label || agent.type} + + + + {agent.source === 'official' ? '官方' : '用户'} + + + + {agent.status === 'published' ? '已发布' : agent.status === 'pending' ? '待发布' : '已下架'} + + {agent.price === 0 ? 免费 : `¥${agent.price}`}{agent.install_count.toLocaleString()}{agent.rating_avg > 0 ? `★ ${agent.rating_avg}` : '-'} + {agent.updated_at ? agent.updated_at.slice(0, 16).replace('T', ' ') : '-'} + +
+ + + + +
+
+
+ + {/* Pagination */} + {totalPages > 1 && ( +
+ 共 {total} 条,第 {page}/{totalPages} 页 +
+ + {[...Array(Math.min(5, totalPages))].map((_, i) => { + const p = Math.max(1, Math.min(page - 2, totalPages - 4)) + i; + return p <= totalPages && ( + + ); + })} + +
+
+ )} + + {/* Create/Edit Modal */} + {showModal && ( +
setShowModal(false)}> +
e.stopPropagation()}> +
+

{editingAgent ? '编辑 Agent' : '创建 Agent'}

+ +
+
+
+
+ + setForm(f => ({ ...f, name: e.target.value }))} + placeholder="例如: 智能代码审查助手" /> +
+
+ + setForm(f => ({ ...f, icon: e.target.value }))} + placeholder="https://..." /> +
+
+ + setForm(f => ({ ...f, price: e.target.value }))} /> +
+
+ +
+
+ +