diff --git a/app/backend/python/app/api/admin/achievement_admin.py b/app/backend/python/app/api/admin/achievement_admin.py deleted file mode 100644 index 5a56475..0000000 --- a/app/backend/python/app/api/admin/achievement_admin.py +++ /dev/null @@ -1,327 +0,0 @@ -from fastapi import APIRouter, Depends, HTTPException, Query -from sqlalchemy.orm import Session -from sqlalchemy import text -from typing import Optional, List -from datetime import datetime -import json -import traceback - -from ...database import SessionLocal, get_db -from ...auth import get_current_user -from ...models import User - -router = APIRouter() - -def _execute_raw(db: Session, sql: str, params: dict = None): - """执行原始SQL的辅助函数""" - try: - result = db.execute(text(sql), params or {}) - db.commit() - return result - except Exception as e: - db.rollback() - raise e - -@router.get("/achievements") -async def list_achievements( - category: Optional[str] = Query(None), - is_active: Optional[bool] = Query(True), - db: Session = Depends(get_db), - user = Depends(get_current_user) -): - """列出所有成就(带用户进度)""" - try: - achievements = [] - - # Build query - sql = "SELECT * FROM achievements WHERE 1=1" - params = {} - if category: - sql += " AND category = :category" - params["category"] = category - if is_active is not None: - sql += " AND is_active = :is_active" - params["is_active"] = 1 if is_active else 0 - sql += " ORDER BY display_order ASC" - - result = _execute_raw(db, sql, params) - rows = result.fetchall() - - user_id = user.id if hasattr(user, 'id') else None - - for row in rows: - ach = { - "id": row[0], - "name": row[1], - "description": row[2], - "icon": row[3], - "category": row[4], - "criteria_type": row[5], - "criteria_key": row[6], - "criteria_value": row[7], - "reward_type": row[8], - "reward_value": float(row[9]) if row[9] else 0, - "badge_icon": row[10], - "is_active": bool(row[11]) if row[11] is not None else True, - "display_order": row[12], - "user_progress": 0, - "is_completed": False, - "reward_claimed": False - } - - # Get user progress if user is logged in - if user_id: - try: - progress_result = _execute_raw(db, - "SELECT progress, is_completed, reward_claimed FROM user_achievements WHERE user_id = :uid AND achievement_id = :aid", - {"uid": user_id, "aid": ach["id"]}) - prog_row = progress_result.fetchone() - if prog_row: - ach["user_progress"] = prog_row[0] or 0 - ach["is_completed"] = bool(prog_row[1]) if prog_row[1] is not None else False - ach["reward_claimed"] = bool(prog_row[2]) if prog_row[2] is not None else False - except: - pass - - achievements.append(ach) - - return {"code": 0, "data": {"items": achievements, "total": len(achievements)}} - except Exception as e: - traceback.print_exc() - return {"code": 0, "data": {"items": [], "total": 0}} - -@router.get("/achievements/{ach_id}") -async def get_achievement_detail( - ach_id: str, - db: Session = Depends(get_db), - user = Depends(get_current_user) -): - """获取成就详情""" - try: - result = _execute_raw(db, "SELECT * FROM achievements WHERE id = :id", {"id": ach_id}) - row = result.fetchone() - if not row: - raise HTTPException(status_code=404, detail="成就不存在") - - achievement = { - "id": row[0], - "name": row[1], - "description": row[2], - "icon": row[3], - "category": row[4], - "criteria_type": row[5], - "criteria_key": row[6], - "criteria_value": row[7], - "reward_type": row[8], - "reward_value": float(row[9]) if row[9] else 0, - "badge_icon": row[10], - "is_active": bool(row[11]) if row[11] is not None else True, - "display_order": row[12] - } - - return {"code": 0, "data": achievement} - except HTTPException: - raise - except Exception as e: - traceback.print_exc() - raise HTTPException(status_code=500, detail=str(e)) - -@router.post("/achievements") -async def create_achievement( - name: str = Query(...), - category: str = Query(...), - criteria_type: str = Query(...), - criteria_value: int = Query(...), - description: Optional[str] = Query(""), - icon: Optional[str] = Query(""), - criteria_key: Optional[str] = Query(None), - reward_type: str = Query("points"), - reward_value: float = Query(0), - db: Session = Depends(get_db), - user = Depends(get_current_user) -): - """创建新成就(管理员)""" - try: - import uuid - ach_id = str(uuid.uuid4()) - - sql = """INSERT INTO achievements - (id, name, description, icon, category, criteria_type, criteria_key, - criteria_value, reward_type, reward_value, is_active, display_order) - VALUES (:id, :name, :desc, :icon, :cat, :ctype, :ckey, - :cvalue, :rtype, :rvalue, 1, 0)""" - - _execute_raw(db, sql, { - "id": ach_id, - "name": name, - "desc": description, - "icon": icon, - "cat": category, - "ctype": criteria_type, - "ckey": criteria_key, - "cvalue": criteria_value, - "rtype": reward_type, - "rvalue": reward_value - }) - - return {"code": 0, "message": "成就创建成功", "data": {"id": ach_id}} - except Exception as e: - traceback.print_exc() - raise HTTPException(status_code=500, detail=str(e)) - -@router.put("/achievements/{ach_id}") -async def update_achievement( - ach_id: str, - name: Optional[str] = Query(None), - description: Optional[str] = Query(None), - is_active: Optional[bool] = Query(None), - reward_value: Optional[float] = Query(None), - db: Session = Depends(get_db), - user = Depends(get_current_user) -): - """更新成就(管理员)""" - try: - updates = [] - params = {"id": ach_id} - - if name is not None: - updates.append("name = :name") - params["name"] = name - if description is not None: - updates.append("description = :desc") - params["desc"] = description - if is_active is not None: - updates.append("is_active = :active") - params["active"] = 1 if is_active else 0 - if reward_value is not None: - updates.append("reward_value = :rvalue") - params["rvalue"] = reward_value - - if updates: - sql = f"UPDATE achievements SET {', '.join(updates)} WHERE id = :id" - _execute_raw(db, sql, params) - - return {"code": 0, "message": "更新成功"} - except Exception as e: - traceback.print_exc() - raise HTTPException(status_code=500, detail=str(e)) - -@router.delete("/achievements/{ach_id}") -async def delete_achievement( - ach_id: str, - db: Session = Depends(get_db), - user = Depends(get_current_user) -): - """删除成就(管理员)""" - try: - _execute_raw(db, "DELETE FROM achievements WHERE id = :id", {"id": ach_id}) - return {"code": 0, "message": "删除成功"} - except Exception as e: - traceback.print_exc() - raise HTTPException(status_code=500, detail=str(e)) - -@router.get("/user/achievements") -async def get_my_achievements( - db: Session = Depends(get_db), - user = Depends(get_current_user) -): - """获取当前用户的成就进度""" - try: - user_id = user.id if hasattr(user, 'id') else None - if not user_id: - return {"code": 0, "data": {"items": [], "total": 0}} - - # Get all achievements with user progress - result = _execute_raw(db, "SELECT a.*, ua.progress, ua.is_completed, ua.reward_claimed, ua.completed_at FROM achievements a LEFT JOIN user_achievements ua ON a.id = ua.achievement_id AND ua.user_id = :uid WHERE a.is_active = 1 ORDER BY a.display_order ASC", {"uid": user_id}) - - rows = result.fetchall() - achievements = [] - for row in rows: - achievements.append({ - "id": row[0], - "name": row[1], - "description": row[2], - "icon": row[3], - "category": row[4], - "criteria_type": row[5], - "criteria_value": row[7], - "reward_type": row[8], - "reward_value": float(row[9]) if row[9] else 0, - "progress": row[15] or 0, - "is_completed": bool(row[16]) if row[16] is not None else False, - "reward_claimed": bool(row[17]) if row[17] is not None else False, - "completed_at": str(row[18]) if row[18] else None - }) - - return {"code": 0, "data": {"items": achievements, "total": len(achievements)}} - except Exception as e: - traceback.print_exc() - return {"code": 0, "data": {"items": [], "total": 0}} - -@router.post("/user/achievements/{ach_id}/claim") -async def claim_achievement_reward( - ach_id: str, - db: Session = Depends(get_db), - user = Depends(get_current_user) -): - """领取成就奖励""" - try: - user_id = user.id if hasattr(user, 'id') else None - if not user_id: - raise HTTPException(status_code=401, detail="未登录") - - # Check if achievement is completed - result = _execute_raw(db, - "SELECT is_completed, reward_claimed FROM user_achievements WHERE user_id = :uid AND achievement_id = :aid", - {"uid": user_id, "aid": ach_id}) - row = result.fetchone() - - if not row or not row[0]: - raise HTTPException(status_code=400, detail="成就未完成") - - if row[1]: - raise HTTPException(status_code=400, detail="奖励已领取") - - # Mark reward as claimed - _execute_raw(db, - "UPDATE user_achievements SET reward_claimed = 1, claimed_at = NOW() WHERE user_id = :uid AND achievement_id = :aid", - {"uid": user_id, "aid": ach_id}) - - return {"code": 0, "message": "奖励领取成功"} - except HTTPException: - raise - except Exception as e: - traceback.print_exc() - raise HTTPException(status_code=500, detail=str(e)) - -@router.post("/check-progress") -async def check_user_progress( - db: Session = Depends(get_db), - user = Depends(get_current_user) -): - """检查并更新用户成就进度""" - try: - user_id = user.id if hasattr(user, 'id') else None - if not user_id: - raise HTTPException(status_code=401, detail="未登录") - - # Get user stats (this would need to be implemented based on actual data) - # For now, just return success - return {"code": 0, "message": "进度检查完成", "data": {"updated": 0}} - except Exception as e: - traceback.print_exc() - raise HTTPException(status_code=500, detail=str(e)) - -@router.get("/leaderboard") -async def get_achievement_leaderboard( - period: str = Query("all"), - db: Session = Depends(get_db) -): - """获取成就排行榜""" - try: - # This would aggregate user achievements and rank them - # For now, return empty - return {"code": 0, "data": {"items": []}} - except Exception as e: - traceback.print_exc() - return {"code": 0, "data": {"items": []}} diff --git a/app/backend/python/app/api/chat.py b/app/backend/python/app/api/chat.py deleted file mode 100644 index 345f83e..0000000 --- a/app/backend/python/app/api/chat.py +++ /dev/null @@ -1,1313 +0,0 @@ -""" -增强版 Chat API — Phase 2.5 -包含: Skill ID统一(MarketApp映射), 真实Skill执行, Agent工作流注入, - 多轮上下文保持, BM25 KB检索, Function Calling循环增强 -""" -from fastapi import APIRouter, Depends, HTTPException, Query -from sqlalchemy.orm import Session -from sqlalchemy import and_, desc, asc, func -from pydantic import BaseModel, Field -from typing import Optional -import uuid -from datetime import datetime, timezone, timedelta -import re -import json -import math -import httpx -import os -import logging - -from ..database import get_db, SessionLocal -from ..models import ( - User, Conversation, Message, AIModel, ModelProvider, TokenUsageLog, - KnowledgeBase, KnowledgeDocument, KnowledgeDocumentChunk, EvolutionSkill, - EvolutionTrajectory, MarketApp, AppPurchase, AppInstallation, - gen_uuid -) -from ..auth import get_current_user -from ..middleware.log_service import LogService - -router = APIRouter() - -logger = logging.getLogger(__name__) - -# CST timezone (UTC+8) for consistent time display -CST_TZ = timezone(timedelta(hours=8)) - - -def cst_now(): - """Get current time in CST timezone""" - return datetime.now(CST_TZ) - - -# ============================================================ -# Request Models -# ============================================================ - -class CreateConversationRequest(BaseModel): - title: str = Field(default="新对话") - model_id: Optional[str] = None - - -class UpdateConversationRequest(BaseModel): - title: Optional[str] = None - status: Optional[str] = None # active, archived - pinned: Optional[bool] = None - agent_ids: Optional[list] = None - skill_ids: Optional[list] = None - knowledge_base_ids: Optional[list] = None - - -class SendMessageRequest(BaseModel): - content: str - model_id: Optional[str] = None - skill_ids: Optional[list] = None - agent_ids: Optional[list] = None - knowledge_base_ids: Optional[list] = None - use_tools: Optional[bool] = False - - -# ============================================================ -# Skill Resolution: MarketApp → Skill Definition -# ============================================================ - -def _resolve_market_skills(db: Session, skill_ids: list, user_id: str) -> list: - """ - Stage A.1: 统一Skill ID空间 - 前端传MarketApp.id, 需要映射为可用的技能定义。 - 返回: [{id, name, description, skill_content, category, func_name}] - """ - if not skill_ids: - return [] - - # 1. 从MarketApp查询 (前端传的是MarketApp.id) - market_skills = db.query(MarketApp).filter( - MarketApp.id.in_(skill_ids), - MarketApp.type == "skill" - ).all() - - results = [] - for ms in market_skills: - skill_content = ms.skill_content or "" - # 尝试从skill_content解析SKILL.md格式 - # SKILL.md格式: ---\nname: ...\ndescription: ...\n---\n内容... - parsed = _parse_skill_content(skill_content) - - func_name = ms.name.replace(" ", "_").replace("-", "_").lower() - results.append({ - "id": ms.id, - "market_app_id": ms.id, - "name": ms.name, - "description": ms.description or parsed.get("description", ""), - "skill_content": skill_content, - "category": parsed.get("category", "general"), - "skill_type": parsed.get("skill_type", "general"), - "func_name": func_name, - "parameters": parsed.get("parameters", {"input": {"type": "string", "description": "用户输入内容"}}), - "instructions": parsed.get("instructions", ""), - }) - - # 2. 如果有未匹配的ID, 尝试查EvolutionSkill (兼容旧的进化技能ID) - matched_ids = {r["id"] for r in results} - unmatched = [sid for sid in skill_ids if sid not in matched_ids] - - if unmatched: - evo_skills = db.query(EvolutionSkill).filter( - EvolutionSkill.id.in_(unmatched), - EvolutionSkill.user_id == user_id - ).all() - - for es in evo_skills: - # 读取SKILL.md内容 - skill_md = "" - if es.skill_md_path and os.path.exists(es.skill_md_path): - try: - with open(es.skill_md_path, "r", encoding="utf-8") as f: - skill_md = f.read() - except Exception: - skill_md = "" - elif es.description: - skill_md = es.description - - parsed = _parse_skill_content(skill_md) - func_name = es.skill_name.replace(" ", "_").replace("-", "_").lower() - - results.append({ - "id": es.id, - "market_app_id": None, - "name": es.skill_name, - "description": es.description or parsed.get("description", ""), - "skill_content": skill_md, - "category": es.category or parsed.get("category", "general"), - "skill_type": parsed.get("skill_type", es.category or "general"), - "func_name": func_name, - "parameters": parsed.get("parameters", {"input": {"type": "string", "description": "用户输入内容"}}), - "instructions": parsed.get("instructions", ""), - }) - - return results - - -def _resolve_market_agents(db: Session, agent_ids: list, user_id: str) -> list: - """ - Stage B.1: 加载Agent配置 - 前端传MarketApp.id(agent类型), 需要解析AgentConfig。 - 返回: [{id, name, config, skills, knowledge_base_ids}] - """ - if not agent_ids: - return [] - - agents = db.query(MarketApp).filter( - MarketApp.id.in_(agent_ids), - MarketApp.type == "agent" - ).all() - - results = [] - for ag in agents: - # AgentConfig在skill_content字段(JSON序列化) - agent_config = {} - try: - if ag.skill_content: - agent_config = json.loads(ag.skill_content) - except Exception: - agent_config = {"role": ag.description or "AI助手", "instructions": ag.description or ""} - - # 提取配置字段 - role_prompt = agent_config.get("role", ag.description or "AI助手") - instructions = agent_config.get("instructions", "") - agent_skill_ids = agent_config.get("skills", []) # 关联的skill MarketApp IDs - agent_kb_ids = agent_config.get("knowledge_base_ids", []) # 关联的知识库IDs - agent_model_id = agent_config.get("model_id", None) # 指定使用的模型 - tools = agent_config.get("tools", []) # MCP server IDs(暂不用) - - results.append({ - "id": ag.id, - "name": ag.name, - "role_prompt": role_prompt, - "instructions": instructions, - "skill_ids": agent_skill_ids, - "knowledge_base_ids": agent_kb_ids, - "model_id": agent_model_id, - "tools": tools, - "description": ag.description or "", - }) - - return results - - -def _parse_skill_content(content: str) -> dict: - """ - 解析SKILL.md格式内容, 提取结构化信息。 - SKILL.md格式: - --- - name: 技能名 - description: 描述 - category: 分类 - skill_type: 类型 - parameters: JSON或简单定义 - --- - 技能指令/规则内容 - """ - result = { - "name": "", - "description": "", - "category": "general", - "skill_type": "general", - "parameters": {}, - "instructions": "", - } - - if not content: - return result - - # 尝试解析YAML front matter - if content.strip().startswith("---"): - parts = content.split("---") - if len(parts) >= 3: - front_matter = parts[1].strip() - body = parts[2].strip() if len(parts) > 2 else "" - - # 简单YAML解析 (不用pyyaml库) - for line in front_matter.split("\n"): - line = line.strip() - if ":" in line: - key, val = line.split(":", 1) - key = key.strip().lower() - val = val.strip() - if key == "name": - result["name"] = val - elif key == "description": - result["description"] = val - elif key == "category": - result["category"] = val - elif key == "skill_type": - result["skill_type"] = val - elif key == "parameters": - try: - result["parameters"] = json.loads(val) - except Exception: - # 简单格式: input:string,query:string - params = {} - for p in val.split(","): - p = p.strip() - if ":" in p: - pname, ptype = p.split(":", 1) - params[pname.strip()] = {"type": ptype.strip(), "description": pname.strip()} - result["parameters"] = params - - result["instructions"] = body - else: - result["instructions"] = content - else: - # 没有front matter, 全部当作instructions - result["instructions"] = content - # 尝试从第一行提取name - first_line = content.split("\n")[0].strip() - if first_line and len(first_line) < 50: - result["name"] = first_line - - return result - - -# ============================================================ -# BM25 Text Search (Stage C.2) -# ============================================================ - -def _bm25_search(query: str, documents: list, top_k: int = 5) -> list: - """ - 简易BM25检索 — 替代朴素的文档拼接。 - 对文档列表进行关键词匹配+TF-IDF排序, 返回最相关的top_k段。 - """ - if not documents or not query: - return documents[:top_k] - - # 中文简单分词(按字符+标点) - def tokenize(text: str) -> list: - # 移除标点, 拆分为2-4字词组 - text = re.sub(r'[^\w\u4e00-\u9fff]', ' ', text.lower()) - tokens = [] - words = text.split() - for w in words: - if len(w) >= 2: - tokens.append(w) - # 也生成2-gram - if len(w) >= 4: - for i in range(len(w) - 2): - tokens.append(w[i:i+2]) - return tokens - - query_tokens = tokenize(query) - if not query_tokens: - return documents[:top_k] - - scored_docs = [] - avg_doc_len = sum(len(tokenize(d.get("content", ""))) for d in documents) / max(len(documents), 1) - - # BM25参数 - k1 = 1.5 - b = 0.75 - - # 计算每个token的IDF - df = {} - for token in query_tokens: - for doc in documents: - if token in tokenize(doc.get("content", "")): - df[token] = df.get(token, 0) + 1 - - N = len(documents) - idf = {} - for token in query_tokens: - idf[token] = math.log((N - df.get(token, 0) + 0.5) / (df.get(token, 0) + 0.5) + 1) - - # 计算每个文档的BM25得分 - for doc in documents: - doc_tokens = tokenize(doc.get("content", "")) - doc_len = len(doc_tokens) - score = 0 - - for token in query_tokens: - tf = doc_tokens.count(token) - numerator = tf * (k1 + 1) - denominator = tf + k1 * (1 - b + b * doc_len / max(avg_doc_len, 1)) - score += idf.get(token, 0) * numerator / denominator - - scored_docs.append((score, doc)) - - # 按得分排序 - scored_docs.sort(key=lambda x: x[0], reverse=True) - return [d for s, d in scored_docs[:top_k]] - - -def _retrieve_kb_context(db: Session, kb_ids: list, query: str, max_chars: int = 3000) -> str: - """ - Stage C: 增强版知识库检索 — 使用BM25排序替代朴素拼接 - 1. 获取每个KB的文档 - 2. 构建文档列表(content+title+metadata) - 3. BM25检索最相关的段落 - 4. 拼接为上下文注入system prompt - """ - if not kb_ids: - return "" - - kbs = db.query(KnowledgeBase).filter(KnowledgeBase.id.in_(kb_ids)).all() - if not kbs: - return "" - - # 收集所有文档段落 - # Stage C.1: 优先使用 KnowledgeDocumentChunk 表的预分段数据 - all_segments = [] - for kb in kbs: - # 先查chunk表 - chunks = db.query(KnowledgeDocumentChunk).filter( - KnowledgeDocumentChunk.knowledge_base_id == kb.id - ).order_by(KnowledgeDocumentChunk.chunk_index).limit(30).all() - - if chunks: - # 使用预分段数据 - for chunk in chunks: - if chunk.content and len(chunk.content.strip()) >= 20: - all_segments.append({ - "content": "[{}/{}/段{}]: {}".format( - kb.name, chunk.document_id[:8], chunk.chunk_index, chunk.content[:500] - ), - "raw_content": chunk.content, - "kb_name": kb.name, - }) - else: - # 退化: 使用原始文档分段 - docs = db.query(KnowledgeDocument).filter( - KnowledgeDocument.knowledge_base_id == kb.id, - KnowledgeDocument.status == "completed" - ).order_by(KnowledgeDocument.updated_at.desc()).limit(20).all() - - for doc in docs: - content = doc.content or "" - if not content: - continue - - # 如果文档较长, 分段处理 (每段~800字) - if len(content) > 800: - paragraphs = re.split(r'\n\n+', content) - for pi, para in enumerate(paragraphs): - if len(para.strip()) >= 20: - all_segments.append({ - "content": "[{}/{}/段{}]: {}".format( - kb.name, doc.name, pi+1, para.strip()[:500] - ), - "raw_content": para.strip(), - "kb_name": kb.name, - "doc_name": doc.name, - }) - else: - all_segments.append({ - "content": "[{}/{}]: {}".format(kb.name, doc.name, content[:500]), - "raw_content": content, - "kb_name": kb.name, - "doc_name": doc.name, - }) - - if not all_segments: - # 退化: 使用原始朴素拼接 - kb_names = ", ".join([k.name for k in kbs]) - return f"参考知识库: {kb_names}" - - # BM25检索最相关的段落 - top_segments = _bm25_search(query, all_segments, top_k=8) - - # 拼接上下文 - context_parts = [seg["content"] for seg in top_segments] - context = "\n".join(context_parts) - - # 限制总长度 - if len(context) > max_chars: - context = context[:max_chars] + "...(内容截断)" - - return context - - -# ============================================================ -# Dynamic System Prompt Builder (Stage B) -# ============================================================ - -def _build_system_prompt( - skills: list, - agents: list, - kb_context: str, - user_content: str, -) -> tuple: - """ - 动态组装system prompt + function definitions。 - 返回: (system_prompt_str, function_definitions_list) - """ - system_parts = ["你是AIFlow平台的AI助手,具备专业的知识储备和工具调用能力。"] - - function_definitions = [] - - # Stage B.1: Agent角色注入 - if agents: - for agent in agents: - role = agent.get("role_prompt", "") - instructions = agent.get("instructions", "") - if role: - system_parts.append(f"当前角色设定: {role}") - if instructions: - system_parts.append(f"角色行为指引: {instructions}") - - # Stage A.2: Skill工具注入 - if skills: - skill_descs = ", ".join([f"{s['name']}({s.get('category', 'general')})" for s in skills]) - system_parts.append(f"可用技能工具: {skill_descs}") - system_parts.append("重要规则: 当用户请求涉及上述技能工具的处理范围时, 你必须优先通过function calling调用对应工具, 而不是直接回复文字。调用后将获得工具执行结果, 基于结果生成最终回复。") - - # 为每个skill构建function definition - for s in skills: - # 从skill_content提取指令注入system prompt - instructions = s.get("instructions", "") - if instructions and len(instructions) < 800: - system_parts.append(f"技能「{s['name']}」的详细指令: {instructions}") - - # 构建参数schema - params = s.get("parameters", {}) - if isinstance(params, dict) and len(params) > 0: - properties = {} - required = [] - for pname, pdef in params.items(): - if isinstance(pdef, dict): - properties[pname] = { - "type": pdef.get("type", "string"), - "description": pdef.get("description", f"传递给{s['name']}的{pname}参数"), - } - if pdef.get("required", True): - required.append(pname) - else: - properties[pname] = {"type": "string", "description": f"传递给{s['name']}的{pname}参数"} - required.append(pname) - - if not properties: - properties = {"input": {"type": "string", "description": f"需要传递给{s['name']}的完整用户输入"}} - required = ["input"] - else: - properties = {"input": {"type": "string", "description": f"需要传递给{s['name']}的完整用户输入"}} - required = ["input"] - - desc = s.get("description", "") - category = s.get("category", "general") - func_desc = f"调用技能「{s['name']}」处理用户请求。使用场景: {desc}。当用户请求涉及{category}类任务时优先调用此工具。" - - function_definitions.append({ - "name": s["func_name"], - "description": func_desc, - "parameters": { - "type": "object", - "properties": properties, - "required": required, - }, - }) - - # KB context injection - if kb_context: - system_parts.append(f"参考以下知识库内容回答用户问题:\n{kb_context}") - - system_prompt = "\n".join(system_parts) - return system_prompt, function_definitions - - -# ============================================================ -# Skill Tool Execution (Stage A.2 rewritten) -# ============================================================ - -def _execute_skill_tool( - db: Session, - func_name: str, - args: dict, - skills: list, - user_content: str, -) -> str: - """ - Stage A.2: 真实Skill执行引擎 — 替代mock实现 - 1. 查找匹配的skill定义 - 2. 读取skill_content(SKILL.md)中的指令 - 3. 根据指令类型组装执行prompt - 4. 返回结构化的执行结果给LLM - """ - # 查找匹配的skill - matched_skill = None - for s in skills: - expected_name = s["func_name"] - if func_name == expected_name or func_name == f"skill_{s['id'][:8]}": - matched_skill = s - break - - if not matched_skill: - # 退化: 返回通用结果 - return f"工具 '{func_name}' 执行结果: 已处理输入内容, 但未找到对应的技能定义。请基于通用知识回复用户。" - - skill_name = matched_skill["name"] - instructions = matched_skill.get("instructions", "") - input_text = args.get("input", user_content) - - # 根据skill_content中的指令生成真实执行结果 - # 核心思路: 将skill的instructions作为"执行逻辑", - # 把input_text传入, 让结果包含指令要求的处理格式 - - if instructions: - # 有明确指令的技能 — 按指令结构化输出 - result_parts = [ - f"技能「{skill_name}」执行结果:", - f"---", - ] - - # 尝试从指令中提取输出格式要求 - # 如果指令包含"输出"或"格式"关键字, 遵循其要求 - output_format = "" - if "输出格式" in instructions or "output format" in instructions.lower(): - # 提取格式描述 - for line in instructions.split("\n"): - if "输出" in line or "格式" in line or "format" in line.lower(): - output_format += line + "\n" - - # 根据skill_type增强处理 - skill_type = matched_skill.get("skill_type", "general") - - if skill_type == "writer" or "写作" in skill_name or "写" in instructions: - result_parts.append(f"基于技能「{skill_name}」的写作指令处理用户输入:") - result_parts.append(f"主题: {input_text[:200]}") - result_parts.append(f"根据技能规则: {instructions[:300]}") - result_parts.append(f"处理完成 — 请基于上述技能指令和输入内容, 生成符合技能要求的写作输出。") - elif skill_type == "analyzer" or "分析" in skill_name or "分析" in instructions: - result_parts.append(f"基于技能「{skill_name}」的分析指令处理用户输入:") - result_parts.append(f"分析对象: {input_text[:200]}") - result_parts.append(f"分析维度(按技能规则): {instructions[:300]}") - result_parts.append(f"关键发现: 识别到{max(1, len(input_text)//50)}个分析要点") - result_parts.append(f"处理完成 — 请基于上述技能指令, 对输入内容进行深入分析并给出结构化的分析报告。") - elif skill_type == "translator" or "翻译" in skill_name or "翻译" in instructions: - result_parts.append(f"基于技能「{skill_name}」的翻译指令处理用户输入:") - result_parts.append(f"原文: {input_text[:200]}") - result_parts.append(f"翻译规则: {instructions[:300]}") - result_parts.append(f"处理完成 — 请基于技能翻译规则, 将输入内容翻译为目标语言。") - elif skill_type == "summarizer" or "总结" in skill_name or "摘要" in instructions: - result_parts.append(f"基于技能「{skill_name}」的摘要指令处理用户输入:") - result_parts.append(f"原文长度: {len(input_text)} 字符") - result_parts.append(f"摘要规则: {instructions[:300]}") - result_parts.append(f"处理完成 — 请基于技能摘要规则, 对输入内容进行精准摘要。") - else: - # 通用技能 — 传递指令让LLM执行 - result_parts.append(f"基于技能「{skill_name}」的指令处理用户请求:") - result_parts.append(f"用户输入: {input_text[:200]}") - result_parts.append(f"技能指令: {instructions[:500]}") - if output_format: - result_parts.append(f"期望输出格式: {output_format[:200]}") - result_parts.append(f"处理完成 — 请严格按照技能指令的规则和要求, 处理用户输入并生成最终回复。") - - result = "\n".join(result_parts) - else: - # 无明确指令的技能 — 提供基本信息让LLM自行处理 - desc = matched_skill.get("description", "") - result = ( - f"技能「{skill_name}」已激活。\n" - f"技能描述: {desc}\n" - f"用户输入: {input_text[:200]}\n" - f"请基于技能「{skill_name}」的专业能力处理上述输入。" - ) - - # 更新使用计数(如果关联了MarketApp) - market_app_id = matched_skill.get("market_app_id") - if market_app_id: - try: - app = db.query(MarketApp).filter(MarketApp.id == market_app_id).first() - if app: - app.install_count = (app.install_count or 0) + 1 - except Exception: - pass - - return result - - -# ============================================================ -# Conversation Context Persistence (Stage B.2) -# ============================================================ - -def _get_conversation_context(db: Session, conv_id: str) -> dict: - """ - Stage B.2: 获取对话的持久化上下文 - 对话级别的agent/skill/kb选择保存在Conversation字段中。 - """ - conv = db.query(Conversation).filter(Conversation.id == conv_id).first() - if not conv: - return {} - - # Conversation目前没有存储agent/skill/kb的JSON字段 - # 我们从第一条消息中获取(如果存在), 或返回空 - # 后续可通过数据库迁移添加Conversation.context字段 - - # 查询该对话中最早一条含有skill_ids/agent_ids/knowledge_ids的消息 - first_tool_msg = db.query(Message).filter( - Message.conversation_id == conv_id, - Message.role == "user", - ).order_by(asc(Message.created_at)).first() - - context = {} - if first_tool_msg: - if first_tool_msg.skill_ids: - context["skill_ids"] = first_tool_msg.skill_ids - if first_tool_msg.agent_ids: - context["agent_ids"] = first_tool_msg.agent_ids - if first_tool_msg.knowledge_ids: - context["knowledge_base_ids"] = first_tool_msg.knowledge_ids - - return context - - -# ============================================================ -# API Endpoints -# ============================================================ - -@router.get("/conversations") -def list_conversations( - status: Optional[str] = Query(None), - search: Optional[str] = Query(None), - db: Session = Depends(get_db), - current_user: User = Depends(get_current_user), -): - query = db.query(Conversation).filter(Conversation.user_id == current_user.id) - - if status: - query = query.filter(Conversation.status == status) - else: - query = query.filter(Conversation.status != "archived") - - if search: - query = query.filter(Conversation.title.ilike(f"%{search}%")) - - query = query.order_by(desc(Conversation.pinned), desc(Conversation.updated_at)) - - conversations = query.all() - result = [] - for conv in conversations: - result.append({ - "id": conv.id, - "title": conv.title, - "model_id": conv.model_id, - "status": conv.status, - "pinned": conv.pinned, - "knowledge_graph_count": conv.knowledge_graph_count or 0, - "created_at": str(conv.created_at) if conv.created_at else None, - "updated_at": str(conv.updated_at) if conv.updated_at else None, - }) - - return {"code": 0, "data": result, "message": "ok"} - - -@router.post("/conversations") -def create_conversation( - req: CreateConversationRequest, - db: Session = Depends(get_db), - current_user: User = Depends(get_current_user), -): - conv = Conversation( - id=gen_uuid(), - user_id=current_user.id, - title=req.title, - model_id=req.model_id, - status="active", - pinned=False, - knowledge_graph_count=0, - created_at=cst_now(), - updated_at=cst_now(), - ) - db.add(conv) - db.commit() - db.refresh(conv) - - return { - "code": 0, - "data": { - "id": conv.id, - "title": conv.title, - "model_id": conv.model_id, - "status": conv.status, - "pinned": conv.pinned, - "knowledge_graph_count": conv.knowledge_graph_count, - "created_at": str(conv.created_at) if conv.created_at else None, - "updated_at": str(conv.updated_at) if conv.updated_at else None, - }, - "message": "创建成功", - } - - -@router.put("/conversations/{conv_id}") -def update_conversation( - conv_id: str, - req: UpdateConversationRequest, - db: Session = Depends(get_db), - current_user: User = Depends(get_current_user), -): - conv = db.query(Conversation).filter( - and_(Conversation.id == conv_id, Conversation.user_id == current_user.id) - ).first() - if not conv: - raise HTTPException(status_code=404, detail="对话不存在") - - if req.title is not None: - conv.title = req.title - if req.status is not None: - conv.status = req.status - if req.pinned is not None: - conv.pinned = req.pinned - # Stage B.2: 保存agent/skill/kb选择到对话上下文 - # (存储在第一条system消息的knowledge_ids/agent_ids/skill_ids字段中) - conv.updated_at = cst_now() - - db.commit() - db.refresh(conv) - - return {"code": 0, "data": { - "id": conv.id, "title": conv.title, "status": conv.status, "pinned": conv.pinned, - "updated_at": str(conv.updated_at), - }, "message": "更新成功"} - - -@router.delete("/conversations/{conv_id}") -def delete_conversation( - conv_id: str, - db: Session = Depends(get_db), - current_user: User = Depends(get_current_user), -): - conv = db.query(Conversation).filter( - and_(Conversation.id == conv_id, Conversation.user_id == current_user.id) - ).first() - if not conv: - raise HTTPException(status_code=404, detail="对话不存在") - - conv.status = "archived" - conv.updated_at = cst_now() - db.commit() - - return {"code": 0, "message": "删除成功"} - - -@router.get("/conversations/{conv_id}/messages") -def get_messages( - conv_id: str, - db: Session = Depends(get_db), - current_user: User = Depends(get_current_user), -): - conv = db.query(Conversation).filter( - and_(Conversation.id == conv_id, Conversation.user_id == current_user.id) - ).first() - if not conv: - raise HTTPException(status_code=404, detail="对话不存在") - - messages = ( - db.query(Message) - .filter(Message.conversation_id == conv_id) - .order_by(asc(Message.created_at)) - .all() - ) - - result = [] - for msg in messages: - result.append({ - "id": msg.id, - "conversation_id": msg.conversation_id, - "role": msg.role, - "content": msg.content, - "token_used": msg.token_used or 0, - "model_id": msg.model_id, - "used_skills": msg.used_skills or [], - "created_at": str(msg.created_at) if msg.created_at else None, - }) - - return {"code": 0, "data": result, "message": "ok"} - - -@router.post("/conversations/{conv_id}/messages") -async def send_message( - conv_id: str, - req: SendMessageRequest, - db: Session = Depends(get_db), - current_user: User = Depends(get_current_user), -): - conv = db.query(Conversation).filter( - and_(Conversation.id == conv_id, Conversation.user_id == current_user.id) - ).first() - if not conv: - raise HTTPException(status_code=404, detail="对话不存在") - - # Stage B.2: 多轮上下文保持 — 如果当前请求没有skill/agent/kb, - # 从对话历史中继承上一次的选择 - effective_skill_ids = req.skill_ids or [] - effective_agent_ids = req.agent_ids or [] - effective_kb_ids = req.knowledge_base_ids or [] - effective_use_tools = req.use_tools or False - - if not effective_skill_ids and not effective_agent_ids and not effective_kb_ids: - # 从对话上下文继承 - prev_context = _get_conversation_context(db, conv_id) - if prev_context: - effective_skill_ids = prev_context.get("skill_ids", []) - effective_agent_ids = prev_context.get("agent_ids", []) - effective_kb_ids = prev_context.get("knowledge_base_ids", []) - if effective_skill_ids or effective_agent_ids: - effective_use_tools = True - - # 保存用户消息(含skill/agent/kb选择用于上下文保持) - user_msg = Message( - id=gen_uuid(), - conversation_id=conv_id, - role="user", - content=req.content, - token_used=len(req.content) // 2, - model_id=req.model_id or conv.model_id, - skill_ids=req.skill_ids if req.skill_ids else None, - agent_ids=req.agent_ids if req.agent_ids else None, - knowledge_ids=req.knowledge_base_ids if req.knowledge_base_ids else None, - created_at=cst_now(), - ) - db.add(user_msg) - - # ===== 核心聊天引擎 ===== - user_id = current_user.id - model_id = req.model_id or conv.model_id - prompt_tokens = 0 - completion_tokens = 0 - total_tokens = 0 - ai_content = "" - - ai_model = None - skills = [] # 初始化避免UnboundLocalError - agents = [] # 初始化避免UnboundLocalError - if model_id: - ai_model = db.query(AIModel).filter(AIModel.id == model_id).first() - - if ai_model and ai_model.endpoint: - provider = db.query(ModelProvider).filter(ModelProvider.id == ai_model.provider_id).first() - api_key = ai_model.api_key or (provider.api_key if provider else None) - api_url = ai_model.endpoint - - if not api_url and provider and provider.api_base_url: - api_url = f"{provider.api_base_url.rstrip('/')}/v1/chat/completions" - - if api_key and api_url: - try: - # Stage A+B: 解析Skills和Agents - skills = _resolve_market_skills(db, effective_skill_ids, user_id) - agents = _resolve_market_agents(db, effective_agent_ids, user_id) - - # Stage B.1: Agent可能自带skill_ids和kb_ids - agent_extra_skill_ids = [] - agent_extra_kb_ids = [] - agent_model_override = None - for agent in agents: - if agent.get("skill_ids"): - agent_extra_skill_ids.extend(agent["skill_ids"]) - if agent.get("knowledge_base_ids"): - agent_extra_kb_ids.extend(agent["knowledge_base_ids"]) - if agent.get("model_id"): - agent_model_override = agent["model_id"] - - # 合并Agent自带的skills和KBs - all_skill_ids = list(set(effective_skill_ids + agent_extra_skill_ids)) - all_kb_ids = list(set(effective_kb_ids + agent_extra_kb_ids)) - - if agent_extra_skill_ids or agent_extra_kb_ids: - # 重新解析合并后的skills - skills = _resolve_market_skills(db, all_skill_ids, user_id) - effective_use_tools = True - - # Stage C: 增强版KB检索 - kb_context = _retrieve_kb_context(db, all_kb_ids, req.content) - - # Stage B: 动态组装system prompt + function definitions - system_prompt, function_definitions = _build_system_prompt( - skills=skills, - agents=agents, - kb_context=kb_context, - user_content=req.content, - ) - - # 构建消息列表 - messages = [{"role": "system", "content": system_prompt}] - - # 加载对话历史(最多10条) - history = db.query(Message).filter( - Message.conversation_id == conv_id - ).order_by(asc(Message.created_at)).limit(10).all() - for h in history: - if h.role == "user" or h.role == "assistant": - messages.append({"role": h.role, "content": h.content}) - - messages.append({"role": "user", "content": req.content}) - - # Stage B.3: Function Calling循环(最多5轮) - max_tool_rounds = 5 - for round_num in range(max_tool_rounds): - request_body = { - "model": ai_model.name, - "messages": messages, - "max_tokens": 2048, - } - if function_definitions and effective_use_tools: - request_body["tools"] = [ - {"type": "function", "function": fd} - for fd in function_definitions - ] - request_body["tool_choice"] = "auto" - - async with httpx.AsyncClient(timeout=ai_model.timeout or 120) as client: - resp = await client.post( - api_url, - headers={ - "Authorization": f"Bearer {api_key}", - "Content-Type": "application/json" - }, - json=request_body - ) - if resp.status_code != 200: - if round_num == 0: - ai_content = f"AI模型调用失败 (HTTP {resp.status_code}), 请检查模型配置。" - break - - data = resp.json() - choice = data.get("choices", [{}])[0] - message = choice.get("message", {}) - - # 记录token使用量 - usage = data.get("usage", {}) - prompt_tokens += usage.get("prompt_tokens", 0) - completion_tokens += usage.get("completion_tokens", 0) - total_tokens += usage.get("total_tokens", 0) - - # 检查是否有tool_calls - tool_calls = message.get("tool_calls", []) - if tool_calls and effective_use_tools: - # 将assistant消息(含tool_calls)加入上下文 - messages.append({ - "role": "assistant", - "content": message.get("content") or "", - "tool_calls": tool_calls, - }) - - # Stage A.2: 真实Skill执行 - for tc in tool_calls: - func_name = tc.get("function", {}).get("name", "") - func_args_str = tc.get("function", {}).get("arguments", "{}") - try: - func_args = json.loads(func_args_str) - except Exception: - func_args = {"input": func_args_str} - - tool_result = _execute_skill_tool( - db, func_name, func_args, skills, req.content - ) - messages.append({ - "role": "tool", - "tool_call_id": tc.get("id", f"call_{round_num}"), - "content": tool_result, - }) - - # 继续下一轮让模型处理tool结果 - continue - else: - # 普通回复 - ai_content = message.get("content", "") - if not ai_content and round_num > 0: - ai_content = "AI模型已处理工具调用结果, 但返回了空响应。" - break - - if not ai_content: - ai_content = "AI模型返回了空响应, 请稍后重试。" - - except Exception as e: - logger.warning(f"AI API调用异常: {e}") - ai_content = "AI服务暂时不可用, 请稍后重试或检查模型API配置。" - else: - ai_content = "该模型缺少API密钥或端点配置, 请在后台管理-大模型库中配置API Key和Endpoint。" - else: - ai_content = "请选择一个已配置的AI模型进行对话。" - else: - ai_content = "请先在聊天设置中选择一个AI模型。" - - ai_token_count = len(ai_content) // 2 - ai_msg = Message( - id=gen_uuid(), - conversation_id=conv_id, - role="assistant", - content=ai_content, - token_used=ai_token_count, - model_id=req.model_id or conv.model_id, - skill_ids=req.skill_ids if req.skill_ids else None, - agent_ids=req.agent_ids if req.agent_ids else None, - knowledge_ids=req.knowledge_base_ids if req.knowledge_base_ids else None, - created_at=cst_now(), - ) - db.add(ai_msg) - - # 更新used_skills - used_skills = [] - if skills: - used_skills = [s["name"] for s in skills] - ai_msg.used_skills = used_skills - - # 更新对话统计 - conv.knowledge_graph_count = (conv.knowledge_graph_count or 0) + 1 - conv.updated_at = cst_now() - - # 记录Token使用 - token_usage = TokenUsageLog( - id=gen_uuid(), - user_id=current_user.id, - model_id=req.model_id or conv.model_id, - model_name=ai_model.name if model_id and ai_model else None, - conversation_id=conv_id, - prompt_tokens=prompt_tokens or len(req.content) // 2, - completion_tokens=completion_tokens or ai_token_count, - total_tokens=total_tokens or (len(req.content) // 2 + ai_token_count), - created_at=cst_now(), - ) - db.add(token_usage) - - # 进化轨迹记录 - try: - existing = db.query(EvolutionTrajectory).filter( - EvolutionTrajectory.conversation_id == conv_id - ).first() - if not existing: - trajectory = EvolutionTrajectory( - id=gen_uuid(), - user_id=current_user.id, - conversation_id=conv_id, - status="discovered", - token_used=ai_token_count, - created_at=cst_now(), - ) - db.add(trajectory) - db.flush() - - try: - from app.services.evolution_service import EvolutionWatcherManager - watcher = EvolutionWatcherManager() - status = watcher.get_user_status(current_user.id, lambda: SessionLocal()) - if not status.get("scheduler_running"): - watcher.start_user_evolution(current_user.id, lambda: SessionLocal()) - logger.info(f"[Evolution] 自动启动用户 {current_user.id} 的进化Pipeline") - except Exception as evolution_err: - logger.warning(f"[Evolution] 自动启动Pipeline失败: {evolution_err}") - except Exception: - pass - - db.commit() - db.refresh(user_msg) - db.refresh(ai_msg) - - return { - "code": 0, - "data": { - "user_message": { - "id": user_msg.id, "role": user_msg.role, "content": user_msg.content, - "token_used": user_msg.token_used, "created_at": str(user_msg.created_at), - }, - "assistant_message": { - "id": ai_msg.id, "role": ai_msg.role, "content": ai_msg.content, - "token_used": ai_msg.token_used, "created_at": str(ai_msg.created_at), - "used_skills": used_skills, - }, - }, - "message": "发送成功", - } - - -@router.post("/conversations/{conv_id}/clear") -def clear_conversation( - conv_id: str, - db: Session = Depends(get_db), - current_user: User = Depends(get_current_user), -): - conv = db.query(Conversation).filter( - and_(Conversation.id == conv_id, Conversation.user_id == current_user.id) - ).first() - if not conv: - raise HTTPException(status_code=404, detail="对话不存在") - - db.query(Message).filter( - Message.conversation_id == conv_id - ).delete() - - conv.knowledge_graph_count = 0 - conv.updated_at = cst_now() - db.commit() - - return {"code": 0, "message": "对话上下文已清空"} - - -@router.post("/conversations/{conv_id}/to-knowledge-graph") -def convert_to_knowledge_graph( - conv_id: str, - db: Session = Depends(get_db), - current_user: User = Depends(get_current_user), -): - """将对话内容转换为知识图谱""" - conv = db.query(Conversation).filter( - and_(Conversation.id == conv_id, Conversation.user_id == current_user.id) - ).first() - if not conv: - raise HTTPException(status_code=404, detail="对话不存在") - - messages = ( - db.query(Message) - .filter(Message.conversation_id == conv_id) - .order_by(asc(Message.created_at)) - .all() - ) - - if not messages: - raise HTTPException(status_code=400, detail="对话中没有消息, 无法生成知识图谱") - - kb_name = f"对话图谱-{conv.title}" - kb = KnowledgeBase( - id=gen_uuid(), - user_id=current_user.id, - name=kb_name, - description=f"从对话「{conv.title}」自动生成的知识图谱", - directory_path="/", - document_count=0, - created_at=cst_now(), - updated_at=cst_now(), - ) - db.add(kb) - db.flush() - - doc_count = 0 - for idx, msg in enumerate(messages): - role_label = "用户" if msg.role == "user" else "AI" - preview = re.sub(r'[\\n\\r\\t]+', ' ', msg.content[:40]).strip() - filename = f"msg_{idx+1:03d}_{role_label}_{preview}.txt" - - doc = KnowledgeDocument( - id=gen_uuid(), - knowledge_base_id=kb.id, - filename=filename, - file_type="txt", - file_size=len(msg.content.encode('utf-8')), - content=msg.content, - status="completed", - chunk_count=max(1, len(msg.content) // 500 + 1), - created_at=cst_now(), - updated_at=cst_now(), - ) - db.add(doc) - doc_count += 1 - - kb.document_count = doc_count - db.flush() - - # 构建知识图谱节点和边 - documents = db.query(KnowledgeDocument).filter( - and_( - KnowledgeDocument.knowledge_base_id == kb.id, - KnowledgeDocument.status == "completed", - ) - ).all() - - graph_nodes = [] - graph_edges = [] - - doc_entity_types = [ - {"type": "concept", "label": "核心概念"}, - {"type": "entity", "label": "关键实体"}, - {"type": "attribute", "label": "属性特征"}, - {"type": "relation", "label": "关联关系"}, - ] - - for idx, doc in enumerate(documents): - doc_node_id = f"doc-{doc.id[:8]}" - graph_nodes.append({ - "id": doc_node_id, "label": doc.filename, "type": "document", "size": 18, "group": "document", - }) - - filename_base = doc.filename.rsplit(".", 1)[0] - words = filename_base.replace("_", " ").replace("-", " ").split() - - for wi, word in enumerate(words): - if len(word) >= 2: - entity_type = doc_entity_types[wi % len(doc_entity_types)] - node_id = f"ent-{doc.id[:8]}-{wi}" - graph_nodes.append({ - "id": node_id, "label": word, "type": entity_type["type"], - "size": 10 + (wi % 8), "group": entity_type["type"], - }) - graph_edges.append({ - "source": doc_node_id, "target": node_id, - "label": entity_type["label"], "weight": 0.5 + (0.5 / (wi + 1)), - }) - - for i in range(len(documents)): - for j in range(i + 1, len(documents)): - fi = documents[i].filename.lower().rsplit(".", 1)[0] - fj = documents[j].filename.lower().rsplit(".", 1)[0] - words_i = set(fi.replace("_", " ").replace("-", " ").split()) - words_j = set(fj.replace("_", " ").replace("-", " ").split()) - common = words_i & words_j - if common and len(common) > 0: - graph_edges.append({ - "source": f"doc-{documents[i].id[:8]}", - "target": f"doc-{documents[j].id[:8]}", - "label": f"共有关键词: {', '.join(list(common)[:3])}", - "weight": min(0.9, 0.3 * len(common)), - }) - - graph_data = { - "nodes": graph_nodes, - "edges": graph_edges, - "stats": { - "total_nodes": len(graph_nodes), "total_edges": len(graph_edges), - "document_count": len(documents), - }, - } - - kb.knowledge_graph_count = len(graph_nodes) - kb.updated_at = cst_now() - conv.knowledge_graph_count = (conv.knowledge_graph_count or 0) + 1 - conv.updated_at = cst_now() - - db.commit() - - LogService.log_operation( - user_id=current_user.id, username=current_user.username, action="convert_to_graph", - target="conversation", target_id=conv_id, - detail=f"将对话 {conv.title} 转换为知识图谱 {kb.name}", - ) - - return { - "code": 0, - "data": { - "knowledge_base_id": kb.id, - "knowledge_base_name": kb.name, - "graph": graph_data, - }, - "message": f"知识图谱构建完成", - } - - -@router.get("/models") -def get_models(db: Session = Depends(get_db)): - models = db.query(AIModel).filter(AIModel.is_enabled == True).all() - result = [] - for m in models: - provider = db.query(ModelProvider).filter(ModelProvider.id == m.provider_id).first() - result.append({ - "id": m.id, - "name": m.name, - "display_name": getattr(m, "display_name", None) or m.name, - "provider_id": m.provider_id, - "provider_name": provider.name if provider else "", - "status": m.status, - "price_per_million_tokens": float(m.price_per_million_tokens or 0), - "is_enabled": m.is_enabled, - "is_default": bool(m.is_default) if hasattr(m, 'is_default') else False, - "total_calls": m.total_calls or 0, - "total_tokens": m.total_tokens or 0, - "created_at": str(m.created_at) if m.created_at else None, - }) - result.sort(key=lambda x: (0 if x["is_default"] else 1, x["name"])) - return {"code": 0, "data": result, "message": "ok"} diff --git a/app/backend/python/app/api/knowledge.py b/app/backend/python/app/api/knowledge.py deleted file mode 100644 index dd1d836..0000000 --- a/app/backend/python/app/api/knowledge.py +++ /dev/null @@ -1,1388 +0,0 @@ -from fastapi import APIRouter, Depends, HTTPException, Query, UploadFile, File, Form -from sqlalchemy.orm import Session -from sqlalchemy import and_, desc, asc -from pydantic import BaseModel, Field -from typing import Optional, List -import uuid -import os -import json -import re -import logging -import httpx -from datetime import datetime - -from ..database import get_db -from ..models import ( - User, KnowledgeBase, KnowledgeDocument, KnowledgeDirectory, Asset, - KnowledgeGraphNode, KnowledgeGraphEdge, KnowledgeGraphBuildLog, - AIModel, ModelProvider, - gen_uuid -) -from ..auth import get_current_user -from ..middleware.log_service import LogService - -router = APIRouter() -logger = logging.getLogger("aiflow") - -# 知识库文件存储根目录 -KNOWLEDGE_STORAGE_DIR = os.environ.get("KNOWLEDGE_STORAGE_DIR", "/data/share/project/data/knowledge") - - -class CreateKnowledgeBaseRequest(BaseModel): - name: str = Field(min_length=1, max_length=200) - description: Optional[str] = None - icon: Optional[str] = None - - -class UpdateKnowledgeBaseRequest(BaseModel): - name: Optional[str] = None - description: Optional[str] = None - icon: Optional[str] = None - status: Optional[str] = None - - -class CreateDocumentRequest(BaseModel): - filename: str - file_size: Optional[int] = None - file_type: Optional[str] = None - - -class CreateDirectoryRequest(BaseModel): - name: str = Field(min_length=1, max_length=200) - parent_id: Optional[str] = None - knowledge_base_id: Optional[str] = None - - -class KnowledgeQARequest(BaseModel): - base_id: str - question: str = Field(min_length=1, max_length=5000) - - -@router.get("/bases") -def list_knowledge_bases( - db: Session = Depends(get_db), - current_user: User = Depends(get_current_user), -): - bases = ( - db.query(KnowledgeBase) - .filter(KnowledgeBase.user_id == current_user.id) - .order_by(desc(KnowledgeBase.updated_at)) - .all() - ) - - # 如果用户没有知识库,自动创建两个默认知识库(每个用户只创建一次) - if not bases: - # 检查是否已存在名为"默认知识库"或"知识图谱库"的知识库(兜底检查) - existing_default = db.query(KnowledgeBase).filter( - and_(KnowledgeBase.user_id == current_user.id, KnowledgeBase.name == "默认知识库") - ).first() - existing_graph = db.query(KnowledgeBase).filter( - and_(KnowledgeBase.user_id == current_user.id, KnowledgeBase.name == "知识图谱库") - ).first() - - if not existing_default: - default_kb = KnowledgeBase( - id=gen_uuid(), - user_id=current_user.id, - name="默认知识库", - description="联网采集、直接上传的内容默认存储", - directory_path="/", - document_count=0, - created_at=datetime.now(), - updated_at=datetime.now(), - ) - db.add(default_kb) - LogService.log_operation( - user_id=current_user.id, username=current_user.username, action="auto_create", - target="knowledge_base", target_id=default_kb.id, - detail="自动创建默认知识库", - ) - - if not existing_graph: - graph_kb = KnowledgeBase( - id=gen_uuid(), - user_id=current_user.id, - name="知识图谱库", - description="对话转知识图谱、构建知识图谱的默认存储", - directory_path="/", - document_count=0, - created_at=datetime.now(), - updated_at=datetime.now(), - ) - db.add(graph_kb) - LogService.log_operation( - user_id=current_user.id, username=current_user.username, action="auto_create", - target="knowledge_base", target_id=graph_kb.id, - detail="自动创建知识图谱库", - ) - - db.commit() - - # 重新查询 - bases = ( - db.query(KnowledgeBase) - .filter(KnowledgeBase.user_id == current_user.id) - .order_by(desc(KnowledgeBase.updated_at)) - .all() - ) - - # 批量查询所有知识库的真实图谱节点数(从 knowledge_graph_nodes 表统计) - kb_ids = [kb.id for kb in bases] - graph_node_counts = {} - if kb_ids: - from sqlalchemy import func as sa_func - node_stats = db.query( - KnowledgeGraphNode.knowledge_base_id, - sa_func.count(KnowledgeGraphNode.id) - ).filter( - KnowledgeGraphNode.knowledge_base_id.in_(kb_ids) - ).group_by(KnowledgeGraphNode.knowledge_base_id).all() - graph_node_counts = {row[0]: row[1] for row in node_stats} - - result = [] - for kb in bases: - # 使用真实图谱数据,如果图谱表中有数据则用它,否则用旧字段(可能残留旧值) - real_node_count = graph_node_counts.get(kb.id) - if real_node_count is not None and real_node_count > 0: - display_graph_count = real_node_count - else: - display_graph_count = 0 # 清零旧残留值 - - result.append({ - "id": kb.id, - "name": kb.name, - "description": kb.description, - "document_count": kb.document_count or 0, - "knowledge_graph_count": display_graph_count, - "created_at": str(kb.created_at) if kb.created_at else None, - "updated_at": str(kb.updated_at) if kb.updated_at else None, - }) - - # 计算存储空间信息 - # 存储已用:所有知识库文档的真实文件大小总和 - if bases: - from sqlalchemy import func as sa_func - total_size_result = db.query(sa_func.sum(KnowledgeDocument.file_size)).filter( - KnowledgeDocument.knowledge_base_id.in_([kb.id for kb in bases]) - ).scalar() - storage_used = total_size_result or 0 - else: - storage_used = 0 - - # 根据用户会员等级计算存储限额 - membership_level = current_user.membership_level or 0 - storage_limits = {0: 1 * 1024 * 1024 * 1024, 2: 10 * 1024 * 1024 * 1024, 3: 100 * 1024 * 1024 * 1024} - storage_limit = storage_limits.get(membership_level, 1 * 1024 * 1024 * 1024) - - return { - "code": 0, - "data": { - "bases": result, - "storage_used": storage_used, - "storage_limit": storage_limit, - "membership_level": membership_level, - }, - "message": "ok", - } - - -@router.post("/bases") -def create_knowledge_base( - req: CreateKnowledgeBaseRequest, - db: Session = Depends(get_db), - current_user: User = Depends(get_current_user), -): - kb = KnowledgeBase( - id=gen_uuid(), - user_id=current_user.id, - name=req.name, - description=req.description, - directory_path="/", - document_count=0, - created_at=datetime.now(), - updated_at=datetime.now(), - ) - db.add(kb) - db.commit() - db.refresh(kb) - - LogService.log_operation( - user_id=current_user.id, username=current_user.username, action="create", - target="knowledge_base", target_id=kb.id, - detail=f"创建知识库 {kb.name}", - ) - - return { - "code": 0, - "data": { - "id": kb.id, "name": kb.name, "description": kb.description, - "created_at": str(kb.created_at), - }, - "message": "创建成功", - } - - -@router.post("/bases/{base_id}/build-graph") -def build_knowledge_graph( - base_id: str, - db: Session = Depends(get_db), - current_user: User = Depends(get_current_user), -): - """构建知识图谱:调用LLM分析文档内容提取实体和关系,持久化到数据库""" - kb = db.query(KnowledgeBase).filter( - and_(KnowledgeBase.id == base_id, KnowledgeBase.user_id == current_user.id) - ).first() - if not kb: - raise HTTPException(status_code=404, detail="知识库不存在") - - # 获取知识库中的已完成文档 - documents = db.query(KnowledgeDocument).filter( - and_( - KnowledgeDocument.knowledge_base_id == base_id, - KnowledgeDocument.status == "completed", - ) - ).all() - - if not documents: - raise HTTPException(status_code=400, detail="知识库中没有已完成的文档,请先上传文档并等待处理完成") - - # 获取LLM配置(获取默认启用的模型及其provider信息) - llm_config = _get_llm_config(db) - if not llm_config: - raise HTTPException(status_code=500, detail="没有可用的AI模型,请先在后台管理中配置模型") - - api_url, api_key, model_name, timeout = llm_config - - # 先清理旧图谱数据 - db.query(KnowledgeGraphEdge).filter( - KnowledgeGraphEdge.knowledge_base_id == base_id - ).delete() - db.query(KnowledgeGraphNode).filter( - KnowledgeGraphNode.knowledge_base_id == base_id - ).delete() - db.query(KnowledgeGraphBuildLog).filter( - KnowledgeGraphBuildLog.knowledge_base_id == base_id - ).delete() - db.flush() - - total_nodes = 0 - total_edges = 0 - - for doc in documents: - # 读取文档文件内容 - file_content = _read_document_content(doc) - - if not file_content or len(file_content.strip()) < 10: - logger.warning(f"文档 {doc.filename} 内容为空或过短,跳过图谱构建") - # 记录构建日志 - build_log = KnowledgeGraphBuildLog( - id=gen_uuid(), - knowledge_base_id=base_id, - document_id=doc.id, - node_count=0, - edge_count=0, - status="completed", - created_at=datetime.now(), - ) - db.add(build_log) - continue - - # 调用LLM提取实体和关系 - entities, relations = _extract_entities_with_llm( - api_url, api_key, model_name, timeout, doc.filename, file_content - ) - - # 将提取结果写入数据库 - doc_node_count, doc_edge_count = _persist_graph_data( - db, base_id, doc.id, entities, relations - ) - total_nodes += doc_node_count - total_edges += doc_edge_count - - # 记录构建日志 - build_log = KnowledgeGraphBuildLog( - id=gen_uuid(), - knowledge_base_id=base_id, - document_id=doc.id, - node_count=doc_node_count, - edge_count=doc_edge_count, - status="completed", - created_at=datetime.now(), - ) - db.add(build_log) - - db.commit() - - # 更新知识库的图谱计数 - kb.knowledge_graph_count = total_nodes - kb.updated_at = datetime.now() - db.commit() - - LogService.log_operation( - user_id=current_user.id, username=current_user.username, action="build_graph", - target="knowledge_base", target_id=base_id, - detail=f"为知识库 {kb.name} 构建知识图谱,共 {total_nodes} 节点、{total_edges} 边", - ) - - # 构建返回数据 - graph_data = _build_graph_response(db, base_id, total_nodes, total_edges, len(documents)) - - return { - "code": 0, - "data": graph_data, - "message": f"知识图谱构建完成,共 {total_nodes} 个节点、{total_edges} 条边", - } - - -def _get_llm_config(db: Session): - """获取LLM配置:优先默认模型,否则使用第一个启用的模型""" - logger.info("[图谱构建] 开始查找LLM配置...") - ai_model = db.query(AIModel).filter( - AIModel.is_default == True, - AIModel.is_enabled == True, - ).first() - - if not ai_model: - ai_model = db.query(AIModel).filter( - AIModel.is_enabled == True, - AIModel.endpoint.isnot(None), - ).first() - logger.info(f"[图谱构建] 无默认模型,尝试使用第一个启用模型: {ai_model.name if ai_model else 'None'}") - - if not ai_model: - logger.error("[图谱构建] 没有可用的AI模型!") - return None - - api_key = ai_model.api_key - api_url = ai_model.endpoint - - # 如果模型本身没有独立的api_key/endpoint,从provider获取 - if (not api_key or not api_url) and ai_model.provider_id: - provider = db.query(ModelProvider).filter(ModelProvider.id == ai_model.provider_id).first() - if provider: - if not api_key: - api_key = provider.api_key - if not api_url and provider.api_base_url: - api_url = f"{provider.api_base_url.rstrip('/')}/v1/chat/completions" - - if not api_url or not api_key: - logger.error(f"[图谱构建] LLM配置不完整: api_url={api_url}, api_key={'***' if api_key else 'None'}") - return None - - logger.info(f"[图谱构建] 找到LLM配置: model={ai_model.name}, url={api_url[:50]}..., timeout={ai_model.timeout or 60}") - return (api_url, api_key, ai_model.name, ai_model.timeout or 60) - - -def _read_document_content(doc: KnowledgeDocument) -> str: - """从磁盘读取文档的实际内容""" - storage_dir = os.environ.get("KNOWLEDGE_STORAGE_DIR", "/data/share/project/data/knowledge") - file_path = os.path.join(storage_dir, doc.knowledge_base_id, f"{doc.id}.{doc.file_type}") - - logger.info(f"[图谱构建] 读取文档: {doc.filename}, id={doc.id}, type={doc.file_type}, path={file_path}, status={doc.status}") - - if not os.path.exists(file_path): - # 尝试其他可能的路径 - alt_path = os.path.join(storage_dir, doc.knowledge_base_id, f"{doc.id}.{doc.file_type.lower()}") - if os.path.exists(alt_path): - file_path = alt_path - logger.info(f"[图谱构建] 使用备用路径: {file_path}") - else: - # 尝试直接读file_path字段 - if doc.file_path and os.path.exists(doc.file_path): - file_path = doc.file_path - logger.info(f"[图谱构建] 使用数据库file_path: {file_path}") - else: - logger.warning(f"[图谱构建] 文档文件不存在: {file_path} (备用: {alt_path}, 数据库file_path: {doc.file_path})") - return "" - - try: - with open(file_path, "r", encoding="utf-8") as f: - content = f.read() - return content - except Exception as e: - logger.error(f"读取文档文件失败: {file_path}, error={e}") - return "" - - -def _extract_entities_with_llm( - api_url: str, api_key: str, model_name: str, timeout: int, - doc_filename: str, file_content: str -): - """调用LLM分析文档内容,提取实体和关系""" - # 限制内容长度(防止token溢出) - max_content_len = 8000 - if len(file_content) > max_content_len: - truncated_content = file_content[:max_content_len] + "\n\n...(内容已截断)" - else: - truncated_content = file_content - - prompt = f"""分析以下文档内容,识别所有命名实体(人物、组织、地点、产品、技术术语等)和它们之间的关系。 - -文档标题:{doc_filename} - -文档内容: -{truncated_content} - -请以JSON格式返回,格式如下(只返回JSON,不要包含其他内容): -{{"entities":[{{"name":"实体名","type":"entity/attribute/concept/event","description":"简介","attributes":{{"key":"value"}}}}],"relations":[{{"source":"源实体名","target":"目标实体名","relation":"关系类型","description":"关系描述"}}]}} - -要求: -1. 实体名称使用文档原文中的表述 -2. 实体类型只能从 entity(命名实体)、attribute(属性特征)、concept(核心概念)、event(事件)中选择 -3. 每个实体尽量给出简短描述 -4. 关系需要明确源实体和目标实体的名称 -5. 不要返回不存在的实体或关系""" - - try: - with httpx.Client(timeout=timeout) as client: - resp = client.post( - api_url, - headers={ - "Authorization": f"Bearer {api_key}", - "Content-Type": "application/json", - }, - json={ - "model": model_name, - "messages": [ - {"role": "system", "content": "你是一个专业的知识图谱构建助手,擅长从文档中提取实体和关系。你只返回严格符合要求的JSON格式数据。"}, - {"role": "user", "content": prompt}, - ], - "max_tokens": 4096, - "temperature": 0.3, - }, - ) - - if resp.status_code != 200: - logger.error(f"LLM API调用失败 (HTTP {resp.status_code}): {resp.text[:500]}") - return [], [] - - data = resp.json() - content = data.get("choices", [{}])[0].get("message", {}).get("content", "") - - # 记录token使用量(可选) - usage = data.get("usage", {}) - logger.info( - f"LLM图谱构建 token使用: prompt={usage.get('prompt_tokens', 0)}, " - f"completion={usage.get('completion_tokens', 0)}, " - f"total={usage.get('total_tokens', 0)}" - ) - - if not content: - logger.error("LLM返回空内容") - return [], [] - - # 解析LLM返回的JSON - entities, relations = _parse_llm_graph_response(content) - return entities, relations - - except httpx.TimeoutException: - logger.error(f"LLM API调用超时: {api_url}") - return [], [] - except httpx.ConnectError: - logger.error(f"LLM API连接失败: {api_url}") - return [], [] - except Exception as e: - logger.error(f"LLM API调用异常: {e}") - return [], [] - - -def _parse_llm_graph_response(content: str): - """解析LLM返回的图谱JSON,做容错处理""" - entities = [] - relations = [] - - # 尝试多种方式提取JSON - # 方式1: 直接解析 - try: - result = json.loads(content) - entities = result.get("entities", []) - relations = result.get("relations", []) - return entities, relations - except json.JSONDecodeError: - pass - - # 方式2: 提取```json ... ```代码块 - json_match = re.search(r'```(?:json)?\s*([\s\S]*?)```', content) - if json_match: - try: - result = json.loads(json_match.group(1)) - entities = result.get("entities", []) - relations = result.get("relations", []) - return entities, relations - except json.JSONDecodeError: - pass - - # 方式3: 提取最外层的{...} - brace_match = re.search(r'\{[\s\S]*\}', content) - if brace_match: - try: - result = json.loads(brace_match.group(0)) - entities = result.get("entities", []) - relations = result.get("relations", []) - return entities, relations - except json.JSONDecodeError: - pass - - logger.warning(f"无法解析LLM返回的JSON: {content[:500]}") - return [], [] - - -def _persist_graph_data(db: Session, base_id: str, doc_id: str, entities: list, relations: list): - """将LLM提取的实体和关系持久化到数据库,返回(节点数, 边数)""" - # 先创建节点,记录name到node_id的映射 - name_to_node_id = {} - node_count = 0 - - for entity in entities: - if not isinstance(entity, dict): - continue - name = entity.get("name", "").strip() - if not name: - continue - entity_type = entity.get("type", "entity") - if entity_type not in ("entity", "attribute", "concept", "event"): - entity_type = "entity" - description = entity.get("description", "") - attributes = entity.get("attributes", {}) - if not isinstance(attributes, dict): - attributes = {} - - node_id = gen_uuid() - node = KnowledgeGraphNode( - id=node_id, - knowledge_base_id=base_id, - source_doc_id=doc_id, - label=name[:500], - type=entity_type, - properties=attributes, - description=description[:65535] if description else None, - created_at=datetime.now(), - ) - db.add(node) - name_to_node_id[name] = node_id - node_count += 1 - - # 必须flush才能获取节点ID用于外键关联 - db.flush() - - # 创建边 - edge_count = 0 - for rel in relations: - if not isinstance(rel, dict): - continue - source_name = rel.get("source", "").strip() - target_name = rel.get("target", "").strip() - if not source_name or not target_name: - continue - source_node_id = name_to_node_id.get(source_name) - target_node_id = name_to_node_id.get(target_name) - if not source_node_id or not target_node_id: - # 如果引用了LLM返回中不存在的实体名,跳过 - logger.debug(f"关系引用了未注册实体: {source_name} -> {target_name}") - continue - - edge = KnowledgeGraphEdge( - id=gen_uuid(), - knowledge_base_id=base_id, - source_node_id=source_node_id, - target_node_id=target_node_id, - relation=rel.get("relation", "关联")[:200], - weight=float(rel.get("weight", 1.0)) if rel.get("weight") else 1.0, - description=rel.get("description", "")[:65535] if rel.get("description") else None, - created_at=datetime.now(), - ) - db.add(edge) - edge_count += 1 - - return node_count, edge_count - - -def _build_graph_response(db: Session, base_id: str, total_nodes: int, total_edges: int, doc_count: int): - """从数据库读取图谱数据并构建响应""" - nodes = db.query(KnowledgeGraphNode).filter( - KnowledgeGraphNode.knowledge_base_id == base_id - ).order_by(KnowledgeGraphNode.created_at.asc()).all() - - edges = db.query(KnowledgeGraphEdge).filter( - KnowledgeGraphEdge.knowledge_base_id == base_id - ).order_by(KnowledgeGraphEdge.created_at.asc()).all() - - graph_nodes = [] - for node in nodes: - graph_nodes.append({ - "id": node.id, - "label": node.label, - "type": node.type, - "size": 10 + (len(node.description) % 10 if node.description else 5), - "group": node.type, - "description": node.description, - "properties": node.properties or {}, - "source_doc_id": node.source_doc_id, - }) - - graph_edges = [] - for edge in edges: - graph_edges.append({ - "id": edge.id, - "source": edge.source_node_id, - "target": edge.target_node_id, - "label": edge.relation, - "weight": edge.weight, - "description": edge.description, - }) - - # 统计各类型节点数量 - entity_types = {} - for node in nodes: - t = node.type - entity_types[t] = entity_types.get(t, 0) + 1 - - return { - "nodes": graph_nodes, - "edges": graph_edges, - "stats": { - "total_nodes": total_nodes, - "total_edges": total_edges, - "document_count": doc_count, - "entity_types": entity_types, - }, - } - - -@router.get("/bases/{base_id}/graph") -def get_knowledge_graph( - base_id: str, - db: Session = Depends(get_db), - current_user: User = Depends(get_current_user), -): - """获取知识图谱数据:从持久化表中读取已构建的图谱""" - kb = db.query(KnowledgeBase).filter( - and_(KnowledgeBase.id == base_id, KnowledgeBase.user_id == current_user.id) - ).first() - if not kb: - raise HTTPException(status_code=404, detail="知识库不存在") - - # 从数据库表读取图谱节点 - nodes = db.query(KnowledgeGraphNode).filter( - KnowledgeGraphNode.knowledge_base_id == base_id - ).order_by(KnowledgeGraphNode.created_at.asc()).all() - - if not nodes: - # 清理旧残留数据:如果 knowledge_graph_count > 0 但图谱表中无节点,清零旧值 - if (kb.knowledge_graph_count or 0) > 0: - kb.knowledge_graph_count = 0 - db.commit() - - # 检查是否有文档但尚未构建图谱 - doc_count = db.query(KnowledgeDocument).filter( - and_( - KnowledgeDocument.knowledge_base_id == base_id, - KnowledgeDocument.status == "completed", - ) - ).count() - - return { - "code": 0, - "data": { - "nodes": [], - "edges": [], - "stats": {"total_nodes": 0, "total_edges": 0, "document_count": doc_count}, - }, - "message": "暂无可用于构建图谱的文档,请先构建知识图谱", - } - - # 从数据库表读取图谱边 - edges = db.query(KnowledgeGraphEdge).filter( - KnowledgeGraphEdge.knowledge_base_id == base_id - ).order_by(KnowledgeGraphEdge.created_at.asc()).all() - - graph_nodes = [] - for node in nodes: - graph_nodes.append({ - "id": node.id, - "label": node.label, - "type": node.type, - "size": 10 + (len(node.description) % 10 if node.description else 5), - "group": node.type, - "description": node.description, - "properties": node.properties or {}, - "source_doc_id": node.source_doc_id, - }) - - graph_edges = [] - for edge in edges: - graph_edges.append({ - "id": edge.id, - "source": edge.source_node_id, - "target": edge.target_node_id, - "label": edge.relation, - "weight": edge.weight, - "description": edge.description, - }) - - # 统计各类型节点数量 - entity_types = {} - for node in nodes: - t = node.type - entity_types[t] = entity_types.get(t, 0) + 1 - - graph_data = { - "nodes": graph_nodes, - "edges": graph_edges, - "stats": { - "total_nodes": len(graph_nodes), - "total_edges": len(graph_edges), - "document_count": len(set(n.source_doc_id for n in nodes if n.source_doc_id)), - "entity_types": entity_types, - }, - } - - return {"code": 0, "data": graph_data, "message": "ok"} - -@router.get("/bases/{base_id}") -def get_knowledge_base( - base_id: str, - db: Session = Depends(get_db), - current_user: User = Depends(get_current_user), -): - kb = db.query(KnowledgeBase).filter( - and_(KnowledgeBase.id == base_id, KnowledgeBase.user_id == current_user.id) - ).first() - if not kb: - raise HTTPException(status_code=404, detail="知识库不存在") - - return { - "code": 0, - "data": { - "id": kb.id, "name": kb.name, "description": kb.description, - "document_count": kb.document_count or 0, - "created_at": str(kb.created_at), "updated_at": str(kb.updated_at), - }, - "message": "ok", - } - - -@router.put("/bases/{base_id}") -def update_knowledge_base( - base_id: str, - req: UpdateKnowledgeBaseRequest, - db: Session = Depends(get_db), - current_user: User = Depends(get_current_user), -): - kb = db.query(KnowledgeBase).filter( - and_(KnowledgeBase.id == base_id, KnowledgeBase.user_id == current_user.id) - ).first() - if not kb: - raise HTTPException(status_code=404, detail="知识库不存在") - - if req.name is not None: - kb.name = req.name - if req.description is not None: - kb.description = req.description - if req.icon is not None: - pass # icon not in model - if req.status is not None: - pass # status not in model - kb.updated_at = datetime.now() - - db.commit() - db.refresh(kb) - - return {"code": 0, "data": { - "id": kb.id, "name": kb.name, "description": kb.description, - }, "message": "更新成功"} - - -@router.delete("/bases/{base_id}") -def delete_knowledge_base( - base_id: str, - db: Session = Depends(get_db), - current_user: User = Depends(get_current_user), -): - kb = db.query(KnowledgeBase).filter( - and_(KnowledgeBase.id == base_id, KnowledgeBase.user_id == current_user.id) - ).first() - if not kb: - raise HTTPException(status_code=404, detail="知识库不存在") - - # 删除知识库下的所有文档 - db.query(KnowledgeDocument).filter(KnowledgeDocument.knowledge_base_id == base_id).delete() - db.delete(kb) - db.commit() - - return {"code": 0, "message": "知识库已删除"} - - -@router.get("/bases/{base_id}/documents") -def get_documents( - base_id: str, - status: Optional[str] = Query(None), - db: Session = Depends(get_db), - current_user: User = Depends(get_current_user), -): - # 先验证知识库归属 - kb = db.query(KnowledgeBase).filter( - and_(KnowledgeBase.id == base_id, KnowledgeBase.user_id == current_user.id) - ).first() - if not kb: - raise HTTPException(status_code=404, detail="知识库不存在") - - query = db.query(KnowledgeDocument).filter(KnowledgeDocument.knowledge_base_id == base_id) - if status: - query = query.filter(KnowledgeDocument.status == status) - - documents = query.order_by(desc(KnowledgeDocument.created_at)).all() - - result = [] - for doc in documents: - result.append({ - "id": doc.id, - "base_id": doc.knowledge_base_id, - "filename": doc.filename, - "file_size": doc.file_size, - "file_type": doc.file_type, - "status": doc.status, - "chunk_count": doc.chunk_count or 0, - "created_at": str(doc.created_at) if doc.created_at else None, - "updated_at": str(doc.updated_at) if doc.updated_at else None, - }) - - return {"code": 0, "data": result, "message": "ok"} - - -@router.post("/bases/{base_id}/documents") -def upload_document( - base_id: str, - req: CreateDocumentRequest, - db: Session = Depends(get_db), - current_user: User = Depends(get_current_user), -): - kb = db.query(KnowledgeBase).filter( - and_(KnowledgeBase.id == base_id, KnowledgeBase.user_id == current_user.id) - ).first() - if not kb: - raise HTTPException(status_code=404, detail="知识库不存在") - - # 确定文件扩展名和存储路径 - file_ext = (req.file_type or "txt").lower() - if file_ext.startswith("."): - file_ext = file_ext[1:] - - # 创建知识库存储目录 - storage_dir = os.path.join(KNOWLEDGE_STORAGE_DIR, base_id) - os.makedirs(storage_dir, exist_ok=True) - - # 生成唯一文件名并存储 - doc_id = gen_uuid() - stored_filename = f"{doc_id}.{file_ext}" - stored_path = os.path.join(storage_dir, stored_filename) - - # 文本解析:根据文件类型生成模拟文档内容 - # 实际生产环境中应使用 PyPDF2/pdfplumber(PDF)、python-docx(Word)、openpyxl(Excel)等库 - file_size = req.file_size or len(req.filename.encode('utf-8')) * 100 - content = _generate_document_content(req.filename, file_ext, file_size) - - # 将内容写入存储文件 - try: - with open(stored_path, "w", encoding="utf-8") as f: - f.write(content) - logger.info(f"知识库文档已写入: {stored_path}, size={len(content)} chars") - except Exception as e: - logger.error(f"知识库文档写入失败: {stored_path}, error={e}") - raise HTTPException(status_code=500, detail=f"文件存储失败: {str(e)}") - - # 文本分块:将内容按段落分割为chunks - chunks = _split_content_into_chunks(content, chunk_size=500) - - doc = KnowledgeDocument( - id=doc_id, - knowledge_base_id=base_id, - filename=req.filename, - file_size=file_size, - file_type=file_ext, - status="completed", - chunk_count=len(chunks), - created_at=datetime.now(), - updated_at=datetime.now(), - ) - db.add(doc) - - # 更新知识库统计 - kb.document_count = (kb.document_count or 0) + 1 - kb.updated_at = datetime.now() - - # 同步创建Asset记录(问题9:资产关联知识库文档用量) - _sync_asset_for_knowledge_doc(db, current_user.id, doc_id, req.filename, file_ext, file_size) - - db.commit() - db.refresh(doc) - - LogService.log_operation( - user_id=current_user.id, username=current_user.username, action="upload", - target="knowledge_document", target_id=doc.id, - detail=f"上传文档 {doc.filename} 到知识库 {kb.name}, 存储路径: {stored_path}, 分块数: {len(chunks)}", - ) - - return { - "code": 0, - "data": { - "id": doc.id, "base_id": doc.knowledge_base_id, "filename": doc.filename, - "file_size": doc.file_size, "file_type": doc.file_type, - "status": "completed", "chunk_count": doc.chunk_count, - "created_at": str(doc.created_at), - "stored_path": stored_path, - }, - "message": "上传成功", - } - - -def _generate_document_content(filename: str, file_ext: str, file_size: int) -> str: - """根据文件类型生成真实的模拟文档内容(文本解析后的结果)""" - name_base = filename.rsplit(".", 1)[0] - paragraphs = [] - - if file_ext in ("txt", "md"): - paragraphs = [ - f"# {name_base}", - f"", - f"## 概述", - f"本文档记录了关于 {name_base} 的详细内容。该文档由用户上传至知识库," - f"经过文本解析引擎处理后,提取了以下关键信息。", - f"", - f"## 核心内容", - f"1. 主题分析:{name_base} 涉及的核心领域包括数据管理、知识工程和智能分析。", - f"2. 关键指标:通过对文档内容的深度解析,识别出 {max(3, file_size // 10000)} 个关键概念节点。", - f"3. 关联关系:文档中各主题之间存在 {max(2, file_size // 20000)} 条语义关联。", - f"", - f"## 详细分析", - f"在 {name_base} 的深入研究中,我们发现以下几点值得关注:", - f"- 数据结构层面的优化空间约为 15-20%", - f"- 知识图谱构建的准确率达到 87.3%", - f"- 检索效率相比传统方法提升了 2.3 倍", - f"", - f"## 结论与建议", - f"基于以上分析,建议在以下几个方面进行改进:", - f"1. 持续优化知识提取的准确度", - f"2. 扩展实体识别覆盖范围", - f"3. 增强跨文档关联分析能力", - f"", - f"---", - f"*本文档由 AIFlow 知识库系统自动解析生成,上载时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*", - ] - elif file_ext == "pdf": - paragraphs = [ - f"PDF文档: {name_base}", - f"", - f"【文档信息】", - f"标题:{name_base}", - f"页数估计:{max(1, file_size // 50000)} 页", - f"创建者:AIFlow 用户", - f"", - f"【提取的文本内容】", - f"第一章 引言", - f"本文档旨在阐述 {name_base} 相关的核心概念和实践方法。", - f"随着人工智能技术的不断发展,知识管理系统在企业数字化转型中扮演着越来越重要的角色。", - f"", - f"第二章 方法论", - f"本研究采用混合方法,结合定量分析与定性评估,对 {name_base} 进行全面探讨。", - f"数据来源包括:内部数据库查询结果、用户行为日志、以及与领域专家的深度访谈。", - f"", - f"第三章 结果分析", - f"通过对收集数据的系统分析,得出以下关键发现:", - f"1. 核心指标X的均值为 78.5,标准差为 12.3", - f"2. 关键因素Y与Z之间存在显著相关性(r=0.72, p<0.001)", - f"3. 85% 的受访者认为 {name_base} 是未来发展的关键方向", - f"", - f"第四章 结论", - f"综合以上分析,{name_base} 具有重要的研究价值和实践意义。", - ] - elif file_ext in ("docx", "doc"): - paragraphs = [ - f"Word文档: {name_base}", - f"", - f"{name_base} - 正式文档", - f"", - f"【摘要】", - f"本报告围绕 {name_base} 展开全面分析,涵盖市场趋势、技术演进和应用场景三个维度。", - f"通过系统性的研究和数据采集,为相关决策提供参考依据。", - f"", - f"【正文】", - f"一、背景介绍", - f"在当前的数字化转型浪潮中,{name_base} 已经成为组织提升竞争力的关键要素。", - f"根据Gartner的报告,到2026年将有超过60%的企业采用AI驱动的知识管理方案。", - f"", - f"二、技术架构", - f"系统采用微服务架构设计,包括数据采集层、处理引擎层、存储层和应用层四个核心模块。", - f"其中处理引擎层集成了NLP自然语言处理、实体识别和关系抽取等多种AI能力。", - f"", - f"三、实施计划", - f"第一阶段(1-2月):完成基础架构搭建和数据迁移", - f"第二阶段(3-4月):部署AI模型并开始增量学习", - f"第三阶段(5-6月):全面上线并进行效果评估", - ] - elif file_ext in ("xlsx", "xls"): - paragraphs = [ - f"Excel表格: {name_base}", - f"", - f"【表格数据概览】", - f"| 指标类别 | Q1 | Q2 | Q3 | Q4 | 年度总计 |", - f"|---------|-----|-----|-----|-----|---------|", - f"| 用户增长 | 12% | 18% | 22% | 28% | 80% |", - f"| 营收增幅 | 8% | 14% | 19% | 25% | 66% |", - f"| 客户满意度 | 85 | 88 | 91 | 93 | 89.3 |", - f"| 市场份额 | 15% | 17% | 20% | 23% | -- |", - f"", - f"【关键指标分析】", - f"1. 用户增长率呈逐季上升趋势,Q4达到峰值28%", - f"2. 营收增长与用户增长高度正相关", - f"3. 客户满意度持续改善,Q4达到93分", - f"4. 市场份额稳步扩大,年度总增长约8个百分点", - f"", - f"【数据来源】内部经营分析系统,数据截止日期:{datetime.now().strftime('%Y-%m-%d')}", - ] - elif file_ext in ("pptx", "ppt"): - paragraphs = [ - f"PPT演示文稿: {name_base}", - f"", - f"【幻灯片结构】", - f"第1页:封面 - {name_base}", - f"第2页:目录与议程", - f"第3页:背景与现状分析", - f"第4页:核心挑战与机遇", - f"第5页:解决方案概述", - f"第6-8页:详细方案展开", - f"第9页:实施路线图", - f"第10页:总结与展望", - f"", - f"【关键要点提取】", - f"- 战略目标:实现 {name_base} 的全面数字化转型", - f"- 核心路径:AI赋能 + 数据驱动 + 敏捷迭代", - f"- 预期成果:效率提升30%、成本降低20%、用户满意度提升15%", - f"- 投入预算:约500万元(分三期投入)", - ] - else: - paragraphs = [ - f"文档: {name_base}", - f"", - f"文件类型:{file_ext.upper()}", - f"文件大小:{file_size} bytes", - f"上传时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}", - f"", - f"【内容摘要】", - f"本文档类型为 {file_ext.upper()} 格式,包含与 {name_base} 相关的内容。", - f"系统已完成基本的文本解析和索引建立,支持关键词检索和语义匹配。", - ] - - return "\n".join(paragraphs) - - -def _split_content_into_chunks(content: str, chunk_size: int = 500) -> list: - """将文档内容分割为文本块(模拟embedding的chunking过程)""" - chunks = [] - paragraphs = content.split("\n\n") - current_chunk = "" - - for para in paragraphs: - if len(current_chunk) + len(para) + 2 <= chunk_size: - current_chunk = (current_chunk + "\n\n" + para).strip() - else: - if current_chunk: - chunks.append(current_chunk) - if len(para) > chunk_size: - # 长段落进一步切分 - for i in range(0, len(para), chunk_size): - chunks.append(para[i:i+chunk_size]) - current_chunk = "" - else: - current_chunk = para - - if current_chunk: - chunks.append(current_chunk) - - return chunks if chunks else [content[:chunk_size]] - - -def _sync_asset_for_knowledge_doc(db: Session, user_id: str, doc_id: str, filename: str, file_type: str, file_size: int): - """将知识库文档同步为资产记录(问题9:资产关联知识库文档用量)""" - try: - asset = Asset( - id=gen_uuid(), - user_id=user_id, - name=filename, - category="doc", - description=f"知识库文档 (ID: {doc_id})", - file_type=file_type, - size=file_size, - status="active", - created_at=datetime.now(), - updated_at=datetime.now(), - ) - db.add(asset) - logger.info(f"已同步知识库文档到资产: {filename} -> asset_id={asset.id}") - except Exception as e: - logger.warning(f"同步知识库文档到资产失败: {e}") - - -@router.delete("/bases/{base_id}/documents/{doc_id}") -def delete_document( - base_id: str, - doc_id: str, - db: Session = Depends(get_db), - current_user: User = Depends(get_current_user), -): - kb = db.query(KnowledgeBase).filter( - and_(KnowledgeBase.id == base_id, KnowledgeBase.user_id == current_user.id) - ).first() - if not kb: - raise HTTPException(status_code=404, detail="知识库不存在") - - doc = db.query(KnowledgeDocument).filter( - and_(KnowledgeDocument.id == doc_id, KnowledgeDocument.knowledge_base_id == base_id) - ).first() - if not doc: - raise HTTPException(status_code=404, detail="文档不存在") - - # 更新知识库统计 - kb.document_count = max(0, (kb.document_count or 0) - 1) - pass # total_size removed - kb.updated_at = datetime.now() - - db.delete(doc) - db.commit() - - return {"code": 0, "message": "文档已删除"} - - -@router.get("/directories") -def list_directories( - base_id: Optional[str] = Query(None), - parent_id: Optional[str] = Query(None), - db: Session = Depends(get_db), - current_user: User = Depends(get_current_user), -): - query = db.query(KnowledgeDirectory).filter( - KnowledgeDirectory.user_id == current_user.id - ) - - if base_id: - query = query.filter(KnowledgeDirectory.knowledge_base_id == base_id) - if parent_id is not None: - query = query.filter(KnowledgeDirectory.parent_id == parent_id) - else: - # 默认查根目录 - query = query.filter(KnowledgeDirectory.parent_id.is_(None)) - - directories = query.order_by(asc(KnowledgeDirectory.name)).all() - - result = [] - for d in directories: - result.append({ - "id": d.id, - "name": d.name, - "parent_id": d.parent_id, - "knowledge_base_id": d.knowledge_base_id, - "created_at": str(d.created_at) if d.created_at else None, - }) - - return {"code": 0, "data": result, "message": "ok"} - - -@router.post("/directories") -def create_directory( - req: CreateDirectoryRequest, - db: Session = Depends(get_db), - current_user: User = Depends(get_current_user), -): - directory = KnowledgeDirectory( - id=gen_uuid(), - user_id=current_user.id, - name=req.name, - parent_id=req.parent_id, - knowledge_base_id=req.knowledge_base_id, - created_at=datetime.now(), - ) - db.add(directory) - db.commit() - db.refresh(directory) - - return { - "code": 0, - "data": { - "id": directory.id, "name": directory.name, - "parent_id": directory.parent_id, "knowledge_base_id": directory.knowledge_base_id, - "created_at": str(directory.created_at), - }, - "message": "创建成功", - } - - -# ============================================================ -# 知识库问答溯源 API -# ============================================================ - -@router.post("/qa") -def knowledge_qa( - req: KnowledgeQARequest, - db: Session = Depends(get_db), - current_user: User = Depends(get_current_user), -): - """知识库问答:在用户的知识库中检索相关文档并生成回答,返回原文引用和来源路径""" - # 验证知识库归属 - kb = db.query(KnowledgeBase).filter( - and_(KnowledgeBase.id == req.base_id, KnowledgeBase.user_id == current_user.id) - ).first() - if not kb: - raise HTTPException(status_code=404, detail="知识库不存在") - - # 检索知识库中的文档 - documents = db.query(KnowledgeDocument).filter( - and_( - KnowledgeDocument.knowledge_base_id == req.base_id, - KnowledgeDocument.status == "completed", - ) - ).all() - - if not documents: - return { - "code": 0, - "data": { - "question": req.question, - "answer": "该知识库中暂无已完成的文档。请先上传并处理文档后再进行问答。", - "sources": [], - "knowledge_base_name": kb.name, - }, - "message": "ok", - } - - # 构建引用来源列表(基于文档关键词匹配模拟检索) - import re - sources = [] - relevant_docs = [] - question_lower = req.question.lower() - - for doc in documents: - filename_lower = doc.filename.lower() - # 简单的关键词匹配检索(实际生产环境应使用向量检索) - keywords = re.split(r'[\s\-\_\.\,\;\:\!\?\(\)\[\]\{\}]+', question_lower) - filename_words = re.split(r'[\s\-\_\.\,\;\:\!\?\(\)\[\]\{\}]+', filename_lower) - match_count = sum(1 for kw in keywords if kw and any(kw in fw for fw in filename_words)) - - if match_count > 0 or len(documents) <= 3: - relevant_docs.append(doc) - # 为每个匹配文档生成模拟原文引用 - sample_content = generate_document_excerpt(doc, req.question) - sources.append({ - "document_id": doc.id, - "document_name": doc.filename, - "file_type": doc.file_type or "", - "file_size": doc.file_size or 0, - "source_path": f"/knowledge/{kb.id}/documents/{doc.id}", - "excerpt": sample_content, - "relevance": min(0.99, max(0.3, match_count * 0.2 + 0.3)) if match_count > 0 else 0.3, - }) - - # 生成模拟回答 - kb_name = kb.name - answer_parts = [f"根据知识库「{kb_name}」中相关文档的内容,针对您的问题回复如下:\n"] - - if sources: - answer_parts.append(f"共检索到 {len(sources)} 篇相关文档,以下为引用分析:\n") - for i, src in enumerate(sources[:5], 1): - answer_parts.append(f"\n【引用 {i}】来源:《{src['document_name']}》") - answer_parts.append(f"原文摘录:\"{src['excerpt']}\"") - answer_parts.append(f"文档类型:{src['file_type'].upper()} | 文档大小:{format_file_size(src['file_size'])}") - answer_parts.append(f"\n---\n综合以上文档内容,我们建议您参考上述引用来源获取详细信息。") - else: - answer_parts.append("未找到与问题高度相关的文档内容,建议上传更多相关文档后重试。") - - final_answer = "\n".join(answer_parts) - - return { - "code": 0, - "data": { - "question": req.question, - "answer": final_answer, - "sources": sources, - "knowledge_base_name": kb_name, - "total_documents": len(documents), - }, - "message": "ok", - } - - -@router.get("/bases/{base_id}/search") -def search_knowledge( - base_id: str, - keyword: str = Query(min_length=1), - db: Session = Depends(get_db), - current_user: User = Depends(get_current_user), -): - """知识库文档检索:按关键词搜索文档并返回引用片段""" - kb = db.query(KnowledgeBase).filter( - and_(KnowledgeBase.id == base_id, KnowledgeBase.user_id == current_user.id) - ).first() - if not kb: - raise HTTPException(status_code=404, detail="知识库不存在") - - documents = db.query(KnowledgeDocument).filter( - and_( - KnowledgeDocument.knowledge_base_id == base_id, - KnowledgeDocument.status == "completed", - KnowledgeDocument.filename.ilike(f"%{keyword}%"), - ) - ).all() - - result = [] - for doc in documents: - result.append({ - "document_id": doc.id, - "document_name": doc.filename, - "file_type": doc.file_type or "", - "file_size": doc.file_size or 0, - "source_path": f"/knowledge/{kb.id}/documents/{doc.id}", - "excerpt": generate_document_excerpt(doc, keyword), - "created_at": str(doc.created_at) if doc.created_at else None, - }) - - return { - "code": 0, - "data": { - "keyword": keyword, - "knowledge_base_name": kb.name, - "results": result, - "total": len(result), - }, - "message": "ok", - } - - -def generate_document_excerpt(doc, query: str) -> str: - """根据文档类型生成模拟的原文摘录""" - excerpts = { - "pdf": f"本文档详细阐述了与「{query}」相关的核心内容。根据第3章第2节的分析,关键指标呈现明显的增长趋势,其中核心参数值在统计周期内提升了约27.3%。进一步的研究表明,该趋势与市场整体发展方向高度一致...", - "docx": f"关于「{query}」的讨论,文档中明确指出:\"从数据层面来看,该领域在过去四个季度中保持了平均15%的复合增长率。这一结论基于对超过2000个样本的综合分析,置信度达到95%以上。建议决策层关注以下三个关键方向...\"", - "xlsx": f"数据表格「{query}」相关统计:\n| 指标 | Q1 | Q2 | Q3 | Q4 |\n|------|----|----|----|----|\n| 用户增长 | 12% | 18% | 22% | 28% |\n| 营收增幅 | 8% | 14% | 19% | 25% |\n数据来源:内部经营分析系统", - "ppt": f"演示文稿第15页「{query}」关键要点:\n• 战略目标:实现年度增长30%\n• 核心路径:优化产品矩阵,拓展海外市场\n• 资源配置:预算投入约500万元\n• 时间节点:Q2完成试点,Q4全面推广\n详细方案见第15-22页", - "md": f"# {query}\n\n## 核心观点\n\n文档明确指出:*\"在当前的行业格局下,企业需要通过技术创新和模式变革来建立竞争壁垒。具体来说,可以从以下三个维度切入:数据驱动决策、自动化流程优化、以及个性化用户体验。\"*\n\n## 关键数据\n\n- 市场规模:预计2026年达到200亿\n- 年复合增长率:35%\n- 头部企业市占率:约40%\n\n> 详细分析参见完整文档。", - "txt": f"文档内容摘录:\n\n\"...针对「{query}」这一课题,我们进行了深入的调研和分析。研究发现,超过78%的受访者认为该领域的创新速度正在加快。同时,技术成熟度曲线显示,相关技术有望在未来12-18个月内进入主流应用阶段。建议前期投入研发资源约50人月,预期投资回报周期为18个月...\"", - } - file_type = (doc.file_type or "txt").lower() - return excerpts.get(file_type, excerpts["txt"]) - - -def format_file_size(size_bytes: int) -> str: - """格式化文件大小""" - if size_bytes >= 1024 * 1024: - return f"{size_bytes / (1024 * 1024):.1f} MB" - elif size_bytes >= 1024: - return f"{size_bytes / 1024:.1f} KB" - return f"{size_bytes} B" - - -# ============================================================ -# 知识图谱构建与查询 API -# ============================================================ - -class BuildGraphRequest(BaseModel): - base_id: str - diff --git a/app/frontend/src/api/index.ts b/app/frontend/src/api/index.ts deleted file mode 100644 index 86f2891..0000000 --- a/app/frontend/src/api/index.ts +++ /dev/null @@ -1,990 +0,0 @@ -/** - * AIFlow API 客户端 - * 统一管理所有前后端API请求 - */ -const API_BASE = '/app/api'; - -// 从localStorage获取token -function getToken(): string | null { - return localStorage.getItem('aiflow_token'); -} - -// 通用请求方法 -export 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_base_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'), - getDailyStats: (days?: number) => get<{ days: number; data: Array<{ date: string; tokens: number }> }>(`/admin/dashboard/daily-stats${days ? `?days=${days}` : ''}`), - // 用户管理 - 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`), - testAllModels: () => post('/admin/models/test-all'), - 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: (params: string) => post(`/admin/membership/plans${params}`), - updatePlan: (id: string, params: string) => put(`/admin/membership/plans/${id}${params}`), - deletePlan: (id: string) => del(`/admin/membership/plans/${id}`), - getSubscriptions: (page?: number) => get(`/admin/membership/subscriptions${page ? `?page=${page}` : ''}`), - getAutomationRules: () => get('/admin/membership/automation-rules'), - getBenefitsComparison: () => get('/admin/membership/benefits-comparison'), - getMembershipConfigs: () => get('/admin/membership/global-configs'), - updateMembershipConfig: (key: string, params: string) => put(`/admin/membership/global-configs/${key}${params}`), - getUserMemberships: (page?: number, planId?: string, status?: string) => { - const p = new URLSearchParams(); - if (page) p.append('page', String(page)); - if (planId) p.append('plan_id', planId); - if (status) p.append('status', status); - const qs = p.toString(); - return get(`/admin/membership/user-memberships${qs ? `?${qs}` : ''}`); - }, - assignPlan: (userId: string, planId: string, days: number) => put(`/admin/membership/users/${userId}/assign-plan?plan_id=${planId}&duration_days=${days}`), - 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'), -// 智能体管理 (Phase 2.3) - getAdminAgents: (params?: any) => { - const query = params ? '?' + Object.entries(params).filter(([_, v]) => v !== undefined && v !== '').map(([k, v]) => `${k}=${v}`).join('&') : ''; - return get(`/admin/agents/${query}`); - }, - getAdminAgent: (id: string) => get(`/admin/agents/${id}`), - createAdminAgent: (data: any) => post('/admin/agents/', data), - updateAdminAgent: (id: string, data: any) => put(`/admin/agents/${id}`, data), - deleteAdminAgent: (id: string) => del(`/admin/agents/${id}`), - toggleAdminAgent: (id: string, isActive: boolean) => put(`/admin/agents/${id}/toggle`, { is_active: isActive }), - getAdminAgentsStats: () => get('/admin/agents/stats/overview'), - getAdminAgentReviews: (id: string) => get(`/admin/agents/${id}/reviews`), - getAdminAvailableSkills: (params?: any) => { - const query = params ? '?' + Object.entries(params).filter(([_, v]) => v !== undefined && v !== '').map(([k, v]) => `${k}=${v}`).join('&') : ''; - return get(`/admin/agents/available/skills${query}`); - }, - getAdminAvailableModels: (params?: any) => { - const query = params ? '?' + Object.entries(params).filter(([_, v]) => v !== undefined && v !== '').map(([k, v]) => `${k}=${v}`).join('&') : ''; - return get(`/admin/agents/available/models${query}`); - }, - // 知识库管理 (Phase 2.4) - getAdminKnowledgeStats: () => get('/admin/knowledge/stats/overview'), - getAdminKnowledge: (params?: any) => { - const query = params ? '?' + Object.entries(params).filter(([_, v]) => v !== undefined && v !== '').map(([k, v]) => `${k}=${v}`).join('&') : ''; - return get(`/admin/knowledge/${query}`); - }, - getAdminKnowledgeDetail: (id: string) => get(`/admin/knowledge/${id}`), - createAdminKnowledge: (data: any) => post('/admin/knowledge/', data), - updateAdminKnowledge: (id: string, data: any) => put(`/admin/knowledge/${id}`, data), - deleteAdminKnowledge: (id: string) => del(`/admin/knowledge/${id}`), - toggleAdminKnowledge: (id: string, isActive: boolean) => put(`/admin/knowledge/${id}/toggle`, { is_active: isActive }), - getAdminKnowledgeDocs: (kbId: string, params?: any) => { - const query = params ? '?' + Object.entries(params).filter(([_, v]) => v !== undefined && v !== '').map(([k, v]) => `${k}=${v}`).join('&') : ''; - return get(`/admin/knowledge/${kbId}/documents${query}`); - }, - deleteAdminKnowledgeDoc: (kbId: string, docId: string) => del(`/admin/knowledge/${kbId}/documents/${docId}`), - getAdminKnowledgeExtServices: () => get('/admin/knowledge/external/services'), - createAdminKnowledgeExtService: (data: any) => post('/admin/knowledge/external/services', data), - updateAdminKnowledgeExtService: (id: string, data: any) => put(`/admin/knowledge/external/services/${id}`, data), - deleteAdminKnowledgeExtService: (id: string) => del(`/admin/knowledge/external/services/${id}`), - checkAdminKnowledgeExtStatus: (id: string) => get(`/admin/knowledge/external/services/${id}/status`), - syncAdminKnowledge: (kbId: string, data: any) => post(`/admin/knowledge/external/sync/${kbId}`, data), - batchDeleteAdminKnowledge: (ids: string[]) => post('/admin/knowledge/batch/delete', ids), - // OSS存储配置 (Phase 3.1) - getAssetStats: () => get('/admin/assets/stats'), - getAdminAssets: (query: string) => get(`/admin/assets/?${query}`), - deleteAdminAsset: (id: string) => del(`/admin/assets/${id}`), - getAssetActivity: () => get('/admin/assets/activity'), - getAdminStorageStats: () => get('/admin/storage/stats/overview'), - getAdminStorageConfigs: (params?: any) => { - const query = params ? '?' + Object.entries(params).filter(([_, v]) => v !== undefined && v !== '').map(([k, v]) => `${k}=${v}`).join('&') : ''; - return get(`/admin/storage/${query}`); - }, - getAdminStorageConfig: (id: string) => get(`/admin/storage/${id}`), - createAdminStorageConfig: (data: any) => post('/admin/storage/', data), - updateAdminStorageConfig: (id: string, data: any) => put(`/admin/storage/${id}`, data), - deleteAdminStorageConfig: (id: string) => del(`/admin/storage/${id}`), - toggleAdminStorageConfig: (id: string) => put(`/admin/storage/${id}/toggle`), - setDefaultAdminStorageConfig: (id: string) => put(`/admin/storage/${id}/set-default`), - testAdminStorageConfig: (data: any) => post('/admin/storage/test', data), - // Storage Quota APIs (Phase C5) - getStorageQuotaOverview: () => get('/admin/storage/quota/overview'), - getStorageQuotaUsers: (params?: any) => { - if (params) { - const query = Object.entries(params).map(([k, v]) => v !== undefined && v !== '' && v !== null ? `${k}=${encodeURIComponent(String(v))}` : '').filter(Boolean).join('&'); - return get(`/admin/storage/quota/users${query ? '?' + query : ''}`); - } - return get('/admin/storage/quota/users'); - }, - getStorageQuotaUser: (userId: string) => get(`/admin/storage/quota/users/${userId}`), - adjustStorageQuota: (userId: string, data: any) => put(`/admin/storage/quota/users/${userId}`, data), - recalcStorageQuota: (userId: string) => post(`/admin/storage/quota/recalculate/${userId}`, {}), - initAllStorageQuotas: () => post('/admin/storage/quota/init-all', {}), - - // 自动化任务调度 (Phase 3.2) - getAdminAutomationStats: () => get('/admin/automation/stats/overview'), - getAdminAutomationTasks: (params?: any) => { - const query = params ? '?' + Object.entries(params).filter(([_, v]) => v !== undefined && v !== '').map(([k, v]) => `${k}=${v}`).join('&') : ''; - return get(`/admin/automation/${query}`); - }, - getAdminAutomationTask: (id: string) => get(`/admin/automation/${id}`), - createAdminAutomationTask: (data: any) => post('/admin/automation/', data), - updateAdminAutomationTask: (id: string, data: any) => put(`/admin/automation/${id}`, data), - deleteAdminAutomationTask: (id: string) => del(`/admin/automation/${id}`), - toggleAdminAutomationTask: (id: string) => put(`/admin/automation/${id}/toggle`), - executeAdminAutomationTask: (id: string) => post(`/admin/automation/${id}/execute`), - getAdminAutomationTaskHistory: (id: string, params?: any) => { - const query = params ? '?' + Object.entries(params).filter(([_, v]) => v !== undefined && v !== '').map(([k, v]) => `${k}=${v}`).join('&') : ''; - return get(`/admin/automation/${id}/history${query}`); - }, - // MCP Server Management - getAdminMcpStats: () => get('/admin/mcp/stats/overview'), - getAdminMcpServers: (params?: any) => { - const query = new URLSearchParams(params).toString(); - return get(`/admin/mcp/${query ? '?' + query : ''}`); - }, - getAdminMcpServer: (id: string) => get(`/admin/mcp/${id}`), - createAdminMcpServer: (data: any) => post('/admin/mcp/', data), - updateAdminMcpServer: (id: string, data: any) => put(`/admin/mcp/${id}`, data), - deleteAdminMcpServer: (id: string) => del(`/admin/mcp/${id}`), - toggleAdminMcpServer: (id: string) => put(`/admin/mcp/${id}/toggle`), - testAdminMcpServer: (id: string, data?: any) => post(`/admin/mcp/${id}/test`, data || {}), - getAdminMcpServerTools: (id: string) => get(`/admin/mcp/${id}/tools`), - getAdminMcpServerResources: (id: string) => get(`/admin/mcp/${id}/resources`), - // Developer API Management - getAdminDeveloperStats: () => get('/admin/developer/stats/overview'), - getAdminApiKeys: (params?: any) => { - const sp = new URLSearchParams(); - if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); - return get(`/admin/developer/api-keys${sp.toString() ? '?' + sp.toString() : ''}`); - }, - createAdminApiKey: (data: any) => post('/admin/developer/api-keys/', data), - updateAdminApiKey: (id: string, data: any) => put(`/admin/developer/api-keys/${id}`, data), - revokeAdminApiKey: (id: string) => post(`/admin/developer/api-keys/${id}/revoke`), - deleteAdminApiKey: (id: string) => del(`/admin/developer/api-keys/${id}`), - getAdminOpenApiClients: (params?: any) => { - const sp = new URLSearchParams(); - if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); - return get(`/admin/developer/openapi-clients${sp.toString() ? '?' + sp.toString() : ''}`); - }, - createAdminOpenApiClient: (data: any) => post('/admin/developer/openapi-clients/', data), - updateAdminOpenApiClient: (id: string, data: any) => put(`/admin/developer/openapi-clients/${id}`, data), - resetAdminOpenApiClientSecret: (id: string) => post(`/admin/developer/openapi-clients/${id}/reset-secret`), - deleteAdminOpenApiClient: (id: string) => del(`/admin/developer/openapi-clients/${id}`), - getAdminCallStats: (params?: any) => { - const sp = new URLSearchParams(); - if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); - return get(`/admin/developer/call-stats${sp.toString() ? '?' + sp.toString() : ''}`); - }, - getAdminCallStatsOverview: () => get('/admin/developer/call-stats/overview'), - getAdminSlowLogs: (params?: any) => { - const sp = new URLSearchParams(); - if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); - return get(`/admin/developer/slow-logs${sp.toString() ? '?' + sp.toString() : ''}`); - }, - clearAdminSlowLogs: () => del('/admin/developer/slow-logs'), - getAdminSlowLogsStats: () => get('/admin/developer/slow-logs/stats'), - // AI App Management - getAIAppStats: () => get('/admin/apps/stats/overview'), - getAIApps: (params?: string) => get('/admin/apps/apps' + (params ? '?' + params : '')), - getAIApp: (id: string) => get('/admin/apps/apps/' + id), - createAIApp: (data: any) => post('/admin/apps/apps', data), - updateAIApp: (id: string, data: any) => put('/admin/apps/apps/' + id, data), - deleteAIApp: (id: string) => del('/admin/apps/apps/' + id), - aiAppLifecycle: (id: string, action: string) => post('/admin/apps/apps/' + id + '/' + action, {}), - getAIAppVersions: (id: string) => get('/admin/apps/apps/' + id + '/versions'), - getAIAppAnalytics: (id: string) => get('/admin/apps/apps/' + id + '/analytics'), - getAIAppTemplates: () => get('/admin/apps/templates'), - createAIAppTemplate: (data: any) => post('/admin/apps/templates', data), - createAIAppFromTemplate: (templateId: string, data: any) => post('/admin/apps/apps/from-template/' + templateId, data), - getAIAppCategories: () => get('/admin/apps/categories'), - // Logo 上传 - uploadLogo: (formData: FormData) => { - const token = localStorage.getItem('access_token'); - return fetch(`/app/api/admin/settings/logo/upload`, { - method: 'POST', - headers: { 'Authorization': `Bearer ${token}` }, - body: formData, - }).then(res => res.json()); - }, - - // MCP Market (Phase G1) - getMcpMarketStats: () => - get('/admin/mcp-market/stats'), - getMcpTools: (params?: Record) => - get('/admin/mcp-market/?' + new URLSearchParams(params || {}).toString()), - getMcpToolDetail: (id: string) => - get('/admin/mcp-market/' + id), - createMcpTool: (data: any) => - post('/admin/mcp-market/', data), - updateMcpTool: (id: string, data: any) => - put('/admin/mcp-market/' + id, data), - deleteMcpTool: (id: string) => - del('/admin/mcp-market/' + id), - scanMcpTool: (id: string) => - post('/admin/mcp-market/' + id + '/security-scan'), - updateMcpToolSecurity: (id: string, data: any) => - put('/admin/mcp-market/' + id + '/security-status', data), - regenMcpDeepLink: (id: string) => - post('/admin/mcp-market/' + id + '/deep-link'), - getMcpManagedServers: () => - get('/admin/mcp-market/managed/servers'), - seedMcpTools: () => - post('/admin/mcp-market/seed'), - - // Code Execution Sandbox (Phase G2) - getCodeExecStats: () => get('/admin/code-exec/stats'), - getCodeExecRuntimes: () => get('/admin/code-exec/runtimes'), - executeCode: (data: { language: string; code: string; stdin?: string; timeout?: number }) => - post('/admin/code-exec/execute', data), - getCodeExecTasks: (params?: any) => { - const sp = new URLSearchParams(); - if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); - return get(`/admin/code-exec/tasks${sp.toString() ? '?' + sp.toString() : ''}`); - }, - getCodeExecTask: (id: string) => get(`/admin/code-exec/tasks/${id}`), - deleteCodeExecTask: (id: string) => del(`/admin/code-exec/tasks/${id}`), - - - // Distribution Builder (Phase G4) - getDistributionStats: () => get('/admin/distributions/stats'), - getDistributions: (params?: any) => { - const sp = new URLSearchParams(); - if (params) Object.entries(params).forEach(([k, v]: [string, any]) => { if (v) sp.set(k, String(v)); }); - return get('/admin/distributions' + (sp.toString() ? '?' + sp.toString() : '')); - }, - getDistributionTemplates: () => get('/admin/distributions/templates'), - createDistribution: (data: any) => post('/admin/distributions', data), - getDistribution: (id: string) => get('/admin/distributions/' + id), - updateDistribution: (id: string, data: any) => put('/admin/distributions/' + id, data), - deleteDistribution: (id: string) => del('/admin/distributions/' + id), - getDistributionLicenses: (params?: any) => { - const sp = new URLSearchParams(); - if (params) Object.entries(params).forEach(([k, v]: [string, any]) => { if (v) sp.set(k, String(v)); }); - return get('/admin/distributions/licenses' + (sp.toString() ? '?' + sp.toString() : '')); - }, - createDistributionLicense: (distroId: string, data: any) => post('/admin/distributions/' + distroId + '/licenses', data), - getDistributionLicense: (id: string) => get('/admin/distributions/licenses/' + id), - revokeDistributionLicense: (id: string) => post('/admin/distributions/licenses/' + id + '/revoke'), - // ===== Ads Management ===== - 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}`), - - // ===== Market Admin (extended) ===== - getMarketOverview: () => get('/admin/market/overview'), - getTopApps: (metric?: string, limit?: number) => get(`/admin/market/top-apps${metric ? '?metric=' + metric : ''}${limit ? (metric ? '&' : '?') + 'limit=' + limit : ''}`), - getAdminMarketApps: (params?: string) => get(`/admin/market/apps${params ? '?' + params : ''}`), - getPendingApps: () => get('/admin/market/pending'), - getCoupons: () => get('/admin/market/coupons'), - getRecentReviews: () => get('/admin/market/reviews'), - approveApp: (id: string) => post(`/admin/market/apps/${id}/approve`), - rejectApp: (id: string) => post(`/admin/market/apps/${id}/reject`), - setFeaturedApp: (id: string, featured: boolean, sortOrder?: number) => put(`/admin/market/apps/${id}/featured`, { featured, sort_order: sortOrder }), - deleteMarketApp: (id: string) => del(`/admin/market/apps/${id}`), - deleteCoupon: (id: string) => del(`/admin/market/coupons/${id}`), - updateMarketApp: (id: string, data: any) => put(`/admin/market/apps/${id}`, data), - updateCoupon: (id: string, data: any) => put(`/admin/market/coupons/${id}`, data), - addCoupon: (data: any) => post('/admin/market/coupons', data), - - // ===== Membership (extended) ===== - getMembershipStats: () => get('/admin/membership/stats'), - revokeMembership: (userId: string) => post(`/admin/membership/users/${userId}/revoke`), - - // ===== Logs (extended) ===== - 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/operation-logs${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() : ''}`); - }, - - // ===== Analytics ===== - getAnalyticsOverview: (days?: number) => get(`/admin/analytics/overview${days ? '?days=' + days : ''}`), - getAnalyticsApiTrends: (days?: number) => get(`/admin/analytics/trends/api-calls${days ? '?days=' + days : ''}`), - getAnalyticsTokenTrends: (days?: number) => get(`/admin/analytics/trends/tokens${days ? '?days=' + days : ''}`), - getAnalyticsUserTrends: (days?: number) => get(`/admin/analytics/trends/users${days ? '?days=' + days : ''}`), - getAnalyticsRevenueTrends: (days?: number) => get(`/admin/analytics/trends/revenue${days ? '?days=' + days : ''}`), - getAnalyticsModelUsage: (days?: number) => get(`/admin/analytics/models/usage${days ? '?days=' + days : ''}`), - - // ===== Billing ===== - getBillingRules: () => get('/admin/billing/rules'), - getBillingPackages: () => get('/admin/billing/packages'), - getBillingTransactions: (page?: number) => get(`/admin/billing/transactions${page ? '?page=' + page : ''}`), - getBillingStats: () => get('/admin/billing/stats/overview'), - - // ===== Tenant ===== - getTenants: (page?: number) => get(`/admin/tenant/list${page ? '?page=' + page : ''}`), - getTenantStats: () => get('/admin/tenant/stats/overview'), - getTenantMembers: (tenantId: string) => get(`/admin/tenant/${tenantId}/members`), - createTenant: (params: string) => post(`/admin/tenant/create${params}`), - -}; -// ===== 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}`), - - - // Workflow (Phase 3.6) - getWorkflows: (params?: Record) => - get('/admin/workflows/?' + new URLSearchParams(params || {}).toString()), - getWorkflow: (id: string) => - get(`/admin/workflows/${id}`), - getWorkflowStats: () => - get('/admin/workflows/stats'), - getWorkflowTemplates: () => - get('/admin/workflows/templates'), - createWorkflow: (data: any) => - post('/admin/workflows/', data), - updateWorkflow: (id: string, data: any) => - put(`/admin/workflows/${id}`, data), - deleteWorkflow: (id: string) => - del(`/admin/workflows/${id}`), - toggleWorkflow: (id: string, isActive: boolean) => - post(`/admin/workflows/${id}/toggle?is_active=${isActive}`), - executeWorkflow: (id: string, inputText: string) => - post(`/admin/workflows/${id}/execute`, { input_text: inputText }), - - - // Privacy & Local Agent (Phase G5) - getPrivacyStats: () => get('/admin/privacy/stats'), - getPrivacyConfig: () => get('/admin/privacy/config'), - updatePrivacyConfig: (params: any) => { - const sp = new URLSearchParams(); - Object.entries(params).forEach(([k, v]) => { if (v != null) sp.append(k, String(v)); }); - return put('/admin/privacy/config?' + sp.toString()); - }, - switchPrivacyMode: (mode: string) => post('/admin/privacy/switch-mode?mode=' + encodeURIComponent(mode)), - getPrivacyLicenses: (params?: any) => { - const sp = new URLSearchParams(); - if (params?.status) sp.append('status', params.status); - return get('/admin/privacy/licenses' + (sp.toString() ? '?' + sp.toString() : '')); - }, - generateOfflineLicense: (data: any) => { - const sp = new URLSearchParams(); - if (data.agent_id) sp.append('agent_id', data.agent_id); - sp.append('max_offline_days', String(data.max_offline_days || 7)); - return post('/admin/privacy/licenses/generate?' + sp.toString()); - }, - revokePrivacyLicense: (id: number) => post('/admin/privacy/licenses/' + id + '/revoke'), - validatePrivacyLicense: (licenseKey: string) => { - const sp = new URLSearchParams(); - sp.append('license_key', licenseKey); - return post('/admin/privacy/licenses/validate?' + sp.toString()); - }, - getKbSyncStates: () => get('/admin/privacy/kb-sync'), - triggerKbSync: (kbId: number, direction: string) => { - const sp = new URLSearchParams(); - sp.append('knowledge_base_id', String(kbId)); - sp.append('sync_direction', direction); - return post('/admin/privacy/kb-sync/trigger?' + sp.toString()); - }, - getLocalAgents: () => get('/admin/privacy/local-agents'), - getPrivacyUsageStats: () => get('/admin/privacy/usage/stats'), - getBridgeConfig: () => get('/admin/privacy/bridge-config'), - - // IM Bot Management (Phase 4) - getImBotStats: () => get('/admin/im-bot/stats'), - getImBotDashboard: () => get('/admin/im-bot/dashboard'), - getImBotChannels: (params?: string) => get(`/admin/im-bot/channels${params || ''}`), - getImBotChannelDetail: (id: string) => get(`/admin/im-bot/channels/${id}`), - testImBotChannel: (id: string) => post(`/admin/im-bot/channels/${id}/test`), - activateImBotChannel: (id: string) => post(`/admin/im-bot/channels/${id}/activate`), - deactivateImBotChannel: (id: string) => post(`/admin/im-bot/channels/${id}/deactivate`), - getImBotRoutes: (params?: string) => get(`/admin/im-bot/routes${params || ''}`), - createImBotRoute: (params: string) => post(`/admin/im-bot/routes${params}`), - deleteImBotRoute: (id: string) => del(`/admin/im-bot/routes/${id}`), - getLangbotConfig: () => get('/admin/im-bot/langbot'), - updateLangbotConfig: (params: string) => post(`/admin/im-bot/langbot${params}`), - testLangbot: (params: string) => post(`/admin/im-bot/langbot/test${params}`), - getImBotPlatforms: () => get('/admin/im-bot/platforms'), - getImBotWebhookUrl: (platform: string) => get(`/admin/im-bot/webhook-url/${platform}`), - getImBotMessages: (params?: string) => get(`/admin/im-bot/messages${params || ''}`), - imBotQuickSetup: (params: string) => post(`/admin/im-bot/quick-setup${params}`), - - // 推广系统 - getPromotionStats: () => get('/admin/promotion/stats'), - getPromotions: (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/promotion/promotions${sp.toString() ? `?${sp.toString()}` : ''}`); - }, - createPromotion: (params: any) => { - const sp = new URLSearchParams(); - Object.entries(params).forEach(([k, v]) => { if (v !== undefined && v !== null && v !== '') sp.set(k, String(v)); }); - return post(`/admin/promotion/promotions${sp.toString() ? `?${sp.toString()}` : ''}`); - }, - updatePromotion: (id: string, params: any) => { - const sp = new URLSearchParams(); - Object.entries(params).forEach(([k, v]) => { if (v !== undefined && v !== null && v !== '') sp.set(k, String(v)); }); - return put(`/admin/promotion/promotions/${id}${sp.toString() ? `?${sp.toString()}` : ''}`); - }, - deletePromotion: (id: string) => del(`/admin/promotion/promotions/${id}`), - togglePromotion: (id: string) => post(`/admin/promotion/promotions/${id}/toggle`), - getReferralLinks: (params?: any) => { - const sp = new URLSearchParams(); - if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); - return get(`/admin/promotion/referral-links${sp.toString() ? `?${sp.toString()}` : ''}`); - }, - generateReferralLink: (params: any) => { - const sp = new URLSearchParams(); - Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); - return post(`/admin/promotion/referral-links/generate${sp.toString() ? `?${sp.toString()}` : ''}`); - }, - getReferralRewards: (params?: any) => { - const sp = new URLSearchParams(); - if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); - return get(`/admin/promotion/rewards${sp.toString() ? `?${sp.toString()}` : ''}`); - }, - approveReward: (id: string) => post(`/admin/promotion/rewards/${id}/approve`), - markRewardPaid: (id: string, params: any) => { - const sp = new URLSearchParams(); - Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); - return post(`/admin/promotion/rewards/${id}/pay${sp.toString() ? `?${sp.toString()}` : ''}`); - }, - getPromotionLeaderboard: (params?: any) => { - const sp = new URLSearchParams(); - if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); - return get(`/admin/promotion/leaderboard${sp.toString() ? `?${sp.toString()}` : ''}`); - }, - regenerateReferralCode: (id: string) => post(`/admin/promotion/referral-links/${id}/regenerate`), - getMyReferrals: () => get('/admin/promotion/my-referrals'), - getMyRewards: () => get('/admin/promotion/my-rewards'), - - // 成就系统 - getAchievements: (params?: string) => get(`/admin/achievement/achievements${params || ''}`), - getAchievementDetail: (id: string) => get(`/admin/achievement/achievements/${id}`), - createAchievement: (params: string) => post(`/admin/achievement/achievements${params}`), - updateAchievement: (id: string, params: string) => put(`/admin/achievement/achievements/${id}${params}`), - deleteAchievement: (id: string) => del(`/admin/achievement/achievements/${id}`), - getUserAchievements: () => get('/admin/achievement/user/achievements'), - claimAchievementReward: (id: string) => post(`/admin/achievement/user/achievements/${id}/claim`), - checkAchievementProgress: () => post('/admin/achievement/check-progress'), - getAchievementLeaderboard: (params?: string) => get(`/admin/achievement/leaderboard${params || ''}`), - - // 多租户管理 - getTenants: (page?: number, keyword?: string, status?: string) => { - const sp = new URLSearchParams(); - if (page) sp.set('page', String(page)); - if (keyword) sp.set('keyword', keyword); - if (status) sp.set('status', status); - sp.set('page_size', '20'); - return get(`/admin/tenant/list${sp.toString() ? `?${sp.toString()}` : ''}`); - }, - createTenant: (params: string) => post(`/admin/tenant/create${params}`), - getTenantDetail: (id: string) => get(`/admin/tenant/${id}`), - updateTenant: (id: string, params: string) => put(`/admin/tenant/${id}${params}`), - deleteTenant: (id: string) => del(`/admin/tenant/${id}`), - getTenantMembers: (tenantId: string, page?: number) => { - const sp = new URLSearchParams(); - sp.set('page', String(page || 1)); - sp.set('page_size', '100'); - return get(`/admin/tenant/${tenantId}/members?${sp.toString()}`); - }, - addTenantMember: (tenantId: string, params: string) => post(`/admin/tenant/${tenantId}/members/add${params}`), - updateTenantMember: (tenantId: string, memberId: string, params: string) => put(`/admin/tenant/${tenantId}/members/${memberId}${params}`), - removeTenantMember: (tenantId: string, memberId: string) => del(`/admin/tenant/${tenantId}/members/${memberId}`), - getTenantQuotas: (tenantId: string) => get(`/admin/tenant/${tenantId}/quotas`), - updateTenantQuota: (tenantId: string, params: string) => put(`/admin/tenant/${tenantId}/quotas${params}`), - getTenantInvitations: (tenantId: string) => get(`/admin/tenant/${tenantId}/invitations`), - createTenantInvitation: (tenantId: string, params: string) => post(`/admin/tenant/${tenantId}/invitations${params}`), - getTenantStats: () => get('/admin/tenant/stats/overview'), - - // 计费与支付 - getBillingRules: (page?: number) => get(`/admin/billing/rules?page=${page || 1}&page_size=100`), - createBillingRule: (params: string) => post(`/admin/billing/rules/create${params}`), - updateBillingRule: (id: string, params: string) => put(`/admin/billing/rules/${id}${params}`), - deleteBillingRule: (id: string) => del(`/admin/billing/rules/${id}`), - getBillingPackages: () => get('/admin/billing/packages'), - createBillingPackage: (params: string) => post(`/admin/billing/packages/create${params}`), - updateBillingPackage: (id: string, params: string) => put(`/admin/billing/packages/${id}${params}`), - deleteBillingPackage: (id: string) => del(`/admin/billing/packages/${id}`), - getBillingTransactions: (page?: number) => get(`/admin/billing/transactions?page=${page || 1}&page_size=20`), - getInvoices: (page?: number) => get(`/admin/billing/invoices?page=${page || 1}&page_size=20`), - generateInvoice: (params: string) => post(`/admin/billing/invoices/generate${params}`), - getPaymentConfigs: () => get('/admin/billing/payment-configs'), - updatePaymentConfig: (id: string, params: string) => put(`/admin/billing/payment-configs/${id}${params}`), - getBillingStats: () => get('/admin/billing/stats/overview'), - - // 数据分析 - getAnalyticsOverview: (days?: number) => get(`/admin/analytics/overview?days=${days || 30}`), - getAnalyticsApiTrends: (days?: number) => get(`/admin/analytics/trends/api-calls?days=${days || 7}`), - getAnalyticsTokenTrends: (days?: number) => get(`/admin/analytics/trends/tokens?days=${days || 7}`), - getAnalyticsUserTrends: (days?: number) => get(`/admin/analytics/trends/users?days=${days || 30}`), - getAnalyticsRevenueTrends: (days?: number) => get(`/admin/analytics/trends/revenue?days=${days || 30}`), - getAnalyticsModelUsage: (days?: number) => get(`/admin/analytics/models/usage?days=${days || 30}`), - getAnalyticsDashboards: () => get('/admin/analytics/dashboards'), - createAnalyticsDashboard: (params: string) => post(`/admin/analytics/dashboards/create${params}`), - deleteAnalyticsDashboard: (id: string) => del(`/admin/analytics/dashboards/${id}`), - getAnalyticsEvents: (params?: any) => { - const sp = new URLSearchParams(); - if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); - return get(`/admin/analytics/events${sp.toString() ? `?${sp.toString()}` : ''}`); - }, - -}; diff --git a/app/frontend/src/pages/admin/Achievement.tsx b/app/frontend/src/pages/admin/Achievement.tsx deleted file mode 100644 index bf1e878..0000000 --- a/app/frontend/src/pages/admin/Achievement.tsx +++ /dev/null @@ -1,1288 +0,0 @@ -// @ts-nocheck - -/** - - * Phase 4 剩余 - 成就系统管理页面 - - * 成就管理、用户进度、排行榜、奖励领取 - - */ - -import React, { useState, useEffect, useCallback } from 'react'; - -import { adminAPI } from '@/api'; - - - -// ==================== 类型定义 ==================== - -interface Achievement { - - id: string; - - name: string; - - description: string; - - icon: string; - - category: string; - - criteria_type: string; - - criteria_key: string | null; - - criteria_value: number; - - reward_type: string; - - reward_value: number; - - badge_icon: string | null; - - is_active: boolean; - - display_order: number; - - user_progress?: number; - - is_completed?: boolean; - - reward_claimed?: boolean; - - completed_at?: string | null; - -} - - - -interface AchievementStats { - - total_achievements: number; - - active_achievements: number; - - completed_achievements: number; - - rewards_claimed: number; - - total_points: number; - - by_category: Record; - -} - - - -const CATEGORY_LABELS: Record = { - - usage: '使用量', - - social: '社交', - - content: '内容创作', - - payment: '付费', - - special: '特殊', - -}; - - - -const CATEGORY_COLORS: Record = { - - usage: '#6C63FF', - - social: '#00D4AA', - - content: '#FFB800', - - payment: '#FF6B6B', - - special: '#C084FC', - -}; - - - -const REWARD_LABELS: Record = { - - points: '积分', - - badge: '徽章', - - discount: '折扣', - - feature: '功能解锁', - - credits: '额度', - -}; - - - -const CRITERIA_LABELS: Record = { - - message_count: '消息数量', - - conversation_count: '对话数量', - - token_usage: 'Token用量', - - referral_count: '推荐人数', - - kb_doc_count: '知识库文档数', - - agent_count: 'Agent数量', - - skill_count: '技能数量', - - payment_amount: '付费金额', - - days_active: '活跃天数', - - workflow_count: '工作流数量', - -}; - - - -// ==================== 主组件 ==================== - -const Achievement: React.FC = () => { - - const [activeTab, setActiveTab] = useState('overview'); - - const [loading, setLoading] = useState(false); - - const [error, setError] = useState(''); - - - - const [stats, setStats] = useState(null); - - const [achievements, setAchievements] = useState([]); - - const [userProgress, setUserProgress] = useState([]); - - const [leaderboard, setLeaderboard] = useState([]); - - const [categoryFilter, setCategoryFilter] = useState(''); - - - - // Modal states - - const [showCreateModal, setShowCreateModal] = useState(false); - - const [editingAch, setEditingAch] = useState(null); - - const [formData, setFormData] = useState({ - - name: '', - - description: '', - - category: 'usage', - - criteria_type: 'message_count', - - criteria_value: 100, - - reward_type: 'points', - - reward_value: 100, - - icon: '', - - }); - - - - const loadStats = useCallback(async () => { - - try { - - const allRes = await adminAPI.getAchievements(''); - - const items = allRes.data?.items || []; - - const active = items.filter((a: Achievement) => a.is_active); - - const completed = items.filter((a: Achievement) => a.is_completed); - - const claimed = items.filter((a: Achievement) => a.reward_claimed); - - const totalPoints = items - - .filter((a: Achievement) => a.is_completed) - - .reduce((sum: number, a: Achievement) => sum + (a.reward_value || 0), 0); - - const byCategory: Record = {}; - - items.forEach((a: Achievement) => { - - byCategory[a.category] = (byCategory[a.category] || 0) + 1; - - }); - - setStats({ - - total_achievements: items.length, - - active_achievements: active.length, - - completed_achievements: completed.length, - - rewards_claimed: claimed.length, - - total_points: totalPoints, - - by_category: byCategory, - - }); - - } catch (e: any) { - - console.error('Failed to load stats:', e); - - } - - }, []); - - - - const loadAchievements = useCallback(async () => { - - setLoading(true); - - setError(''); - - try { - - const params = categoryFilter ? `?category=${categoryFilter}` : ''; - - const res = await adminAPI.getAchievements(params); - - setAchievements(res.data?.items || []); - - } catch (e: any) { - - setError(e.message || '加载失败'); - - } finally { - - setLoading(false); - - } - - }, [categoryFilter]); - - - - const loadUserProgress = useCallback(async () => { - - setLoading(true); - - try { - - const res = await adminAPI.getUserAchievements(); - - setUserProgress(res.data?.items || []); - - } catch (e: any) { - - setError(e.message || '加载失败'); - - } finally { - - setLoading(false); - - } - - }, []); - - - - const loadLeaderboard = useCallback(async () => { - - setLoading(true); - - try { - - const res = await adminAPI.getAchievementLeaderboard('?period=all'); - - setLeaderboard(res.data?.items || []); - - } catch (e: any) { - - setError(e.message || '加载失败'); - - } finally { - - setLoading(false); - - } - - }, []); - - - - useEffect(() => { - - loadStats(); - - }, [loadStats]); - - - - useEffect(() => { - - if (activeTab === 'list') loadAchievements(); - - else if (activeTab === 'progress') loadUserProgress(); - - else if (activeTab === 'leaderboard') loadLeaderboard(); - - }, [activeTab, loadAchievements, loadUserProgress, loadLeaderboard]); - - - - const handleSave = async () => { - - if (!formData.name.trim()) { - - setError('成就名称不能为空'); - - return; - - } - - setLoading(true); - - try { - - const params = `?name=${encodeURIComponent(formData.name)}&category=${formData.category}&criteria_type=${formData.criteria_type}&criteria_value=${formData.criteria_value}&description=${encodeURIComponent(formData.description)}&reward_type=${formData.reward_type}&reward_value=${formData.reward_value}&icon=${encodeURIComponent(formData.icon)}`; - - if (editingAch) { - - const updateParams = `?name=${encodeURIComponent(formData.name)}&description=${encodeURIComponent(formData.description)}&reward_value=${formData.reward_value}`; - - await adminAPI.updateAchievement(editingAch.id, updateParams); - - } else { - - await adminAPI.createAchievement(params); - - } - - setShowCreateModal(false); - - setEditingAch(null); - - setFormData({ - - name: '', description: '', category: 'usage', criteria_type: 'message_count', - - criteria_value: 100, reward_type: 'points', reward_value: 100, icon: '', - - }); - - loadAchievements(); - - loadStats(); - - } catch (e: any) { - - setError(e.message || '保存失败'); - - } finally { - - setLoading(false); - - } - - }; - - - - const handleDelete = async (id: string) => { - - if (!confirm('确定要删除这个成就吗?')) return; - - try { - - await adminAPI.deleteAchievement(id); - - loadAchievements(); - - loadStats(); - - } catch (e: any) { - - setError(e.message || '删除失败'); - - } - - }; - - - - const handleToggle = async (ach: Achievement) => { - - try { - - const params = `?is_active=${!ach.is_active}`; - - await adminAPI.updateAchievement(ach.id, params); - - loadAchievements(); - - loadStats(); - - } catch (e: any) { - - setError(e.message || '操作失败'); - - } - - }; - - - - const handleClaim = async (id: string) => { - - try { - - await adminAPI.claimAchievementReward(id); - - loadUserProgress(); - - loadStats(); - - } catch (e: any) { - - setError(e.message || '领取失败'); - - } - - }; - - - - const handleCheckProgress = async () => { - - try { - - await adminAPI.checkAchievementProgress(); - - loadUserProgress(); - - loadStats(); - - } catch (e: any) { - - setError(e.message || '检查失败'); - - } - - }; - - - - const openEdit = (ach: Achievement) => { - - setEditingAch(ach); - - setFormData({ - - name: ach.name, - - description: ach.description || '', - - category: ach.category, - - criteria_type: ach.criteria_type, - - criteria_value: ach.criteria_value, - - reward_type: ach.reward_type, - - reward_value: ach.reward_value, - - icon: ach.icon || '', - - }); - - setShowCreateModal(true); - - }; - - - - const getProgressPercent = (current: number, target: number) => { - - if (target <= 0) return 0; - - return Math.min(100, Math.round((current / target) * 100)); - - }; - - - - // ==================== 渲染 ==================== - - return ( - -
- - - - - -
- -

成就系统

- -
- - {activeTab === 'list' && ( - - - - )} - - {activeTab === 'progress' && ( - - - - )} - -
- -
- - - - {error &&
{error}
} - - - - {/* Tabs */} - -
- - - - - - - - - -
- - - - {/* Overview Tab */} - - {activeTab === 'overview' && stats && ( - -
- -
- -
- -
总成就数
- -
{stats.total_achievements}
- -
- -
- -
已激活
- -
{stats.active_achievements}
- -
- -
- -
已完成
- -
{stats.completed_achievements}
- -
- -
- -
奖励已领取
- -
{stats.rewards_claimed}
- -
- -
- -
累计积分
- -
{stats.total_points}
- -
- -
- - - -
- -

分类分布

- - {Object.entries(stats.by_category).map(([cat, count]) => { - - const pct = stats.total_achievements > 0 ? (count / stats.total_achievements) * 100 : 0; - - return ( - -
- - {CATEGORY_LABELS[cat] || cat} - -
- -
- -
- - {count} - -
- - ); - - })} - - {Object.keys(stats.by_category).length === 0 && ( - -
暂无数据
- - )} - -
- -
- - )} - - - - {/* List Tab */} - - {activeTab === 'list' && ( - -
- -
- - - -
- - - - {loading ? ( - -
加载中...
- - ) : achievements.length === 0 ? ( - -
暂无成就数据
- - ) : ( - -
- - {achievements.map((ach) => { - - const progressPct = getProgressPercent(ach.user_progress || 0, ach.criteria_value); - - const color = CATEGORY_COLORS[ach.category] || '#6C63FF'; - - return ( - -
- -
- - {ach.icon || CATEGORY_LABELS[ach.category]?.[0] || '★'} - -
- -
{ach.name}
- -
{ach.description || '暂无描述'}
- -
- - {CATEGORY_LABELS[ach.category] || ach.category} - - - - {CRITERIA_LABELS[ach.criteria_type] || ach.criteria_type}: {ach.criteria_value} - - - - - - {REWARD_LABELS[ach.reward_type] || ach.reward_type}: {ach.reward_value} - - - - {!ach.is_active && ( - - 已停用 - - )} - -
- - {ach.user_progress !== undefined && ( - - <> - -
- -
- -
- -
- - {ach.user_progress || 0} / {ach.criteria_value} - - {progressPct}% - -
- - - - )} - -
- - - - - - - -
- -
- - ); - - })} - -
- - )} - -
- - )} - - - - {/* Progress Tab */} - - {activeTab === 'progress' && ( - -
- - {loading ? ( - -
加载中...
- - ) : userProgress.length === 0 ? ( - -
暂无进度数据
- - ) : ( - -
- - {userProgress.map((ach) => { - - const progressPct = getProgressPercent(ach.progress || 0, ach.criteria_value); - - const color = CATEGORY_COLORS[ach.category] || '#6C63FF'; - - return ( - -
- -
- - {ach.icon || CATEGORY_LABELS[ach.category]?.[0] || '★'} - -
- -
{ach.name}
- -
{ach.description || '暂无描述'}
- -
- - {CATEGORY_LABELS[ach.category] || ach.category} - - {ach.is_completed && ( - - 已完成 - - )} - - {ach.reward_claimed && ( - - 已领取 - - )} - -
- -
- -
- -
- -
- - {ach.progress || 0} / {ach.criteria_value} - - {progressPct}% - -
- - {ach.completed_at && ( - -
- - 完成时间: {new Date(ach.completed_at).toLocaleString('zh-CN')} - -
- - )} - -
- - {ach.is_completed && !ach.reward_claimed && ( - - - - )} - - {ach.reward_claimed && ( - - 奖励已领取 - - )} - -
- -
- - ); - - })} - -
- - )} - -
- - )} - - - - {/* Leaderboard Tab */} - - {activeTab === 'leaderboard' && ( - -
- - {loading ? ( - -
加载中...
- - ) : leaderboard.length === 0 ? ( - -
- -
排行榜暂无数据
- -
用户完成成就后将出现在排行榜中
- -
- - ) : ( - - - - - - - - - - - - - - - - - - - - - - - - {leaderboard.map((entry, idx) => ( - - - - - - - - - - - - - - - - ))} - - - -
排名用户完成成就累计积分最近完成
- - - - {idx + 1} - - - - {entry.username || entry.user_id || '匿名用户'}{entry.achievement_count || 0}{entry.total_points || 0} - - {entry.last_completed ? new Date(entry.last_completed).toLocaleDateString('zh-CN') : '-'} - -
- - )} - -
- - )} - - - - {/* Create/Edit Modal */} - - {showCreateModal && ( - -
setShowCreateModal(false)}> - -
e.stopPropagation()}> - -

{editingAch ? '编辑成就' : '创建成就'}

- -
- - - - setFormData({ ...formData, name: e.target.value })} placeholder="例如:对话达人" /> - -
- -
- - - -