""" Skill 管理 API 路由 提供: 1. Skill 的 CRUD 操作 2. Agent-Skill 生命周期端点 3. AI 辅助 Skill 创建(完整流程) 4. Skill 选择和路由(LLM 与 Skills 结合) 5. Agent 工作流配置 6. 文档驱动的 Skill 生成(新增) """ from datetime import datetime from fastapi import APIRouter, Depends, HTTPException, status from typing import List, Optional, Dict, Any from pydantic import BaseModel from app.models.skill import ( Skill, SkillCreate, SkillUpdate, SkillConfigUpdate, SkillTestRequest, SkillTestResponse, SkillGenerateRequest, SkillGenerateResponse, # 新增模型 DocFetchRequest, GitHubDocFetchRequest, SkillGenerateFromDocsRequest, SkillGenerateFromDocsResponse, SkillPreviewRequest, SkillPreviewResponse, SkillSaveFromPreviewRequest, SkillRefineRequest, SkillRefineResponse, ) from app.models.skill_integration import ( SkillMetadata, SkillTriggerType, SkillStructure, SkillGenerationRequest, SkillGenerationResponse, SkillAnalysis, SkillPlanning, SkillRefinementRequest, SkillRefinementResponse, SkillSelectionCriteria, SkillSelectionResult, SkillExecutionContext, SkillExecutionResponse, AgentWorkflowConfig, AgentWorkflowStep, ) from app.core.skills.skill_manager import get_skill_manager, SkillManager from app.core.llm.glm_client import get_glm_client, GLMClient from app.utils.logger import get_logger logger = get_logger(__name__) router = APIRouter(prefix="/skills", tags=["Skill管理"]) # ============================================================================ # 基础 CRUD 端点 # ============================================================================ @router.get("/", response_model=List[Skill]) async def list_skills( skill_type: Optional[str] = None, category: Optional[str] = None, skill_manager: SkillManager = Depends(get_skill_manager) ): """ 列出所有 Skills - **skill_type**: 筛选类型 (builtin/user) - **category**: 筛选分类 """ skills = await skill_manager.list_skills(skill_type=skill_type, category=category) return skills @router.get("/{skill_id}", response_model=Skill) async def get_skill( skill_id: str, skill_manager: SkillManager = Depends(get_skill_manager) ): """获取 Skill 详情""" skill = await skill_manager.load_skill(skill_id) if not skill: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Skill 不存在: {skill_id}" ) return skill @router.post("/", response_model=Skill, status_code=status.HTTP_201_CREATED) async def create_skill( skill_data: SkillCreate, skill_manager: SkillManager = Depends(get_skill_manager) ): """ 创建新的用户 Skill 如果不提供 ID,系统将自动生成: - 使用 UUID 作为唯一标识 - 基于名称生成可读 ID(kebab-case) """ try: # 如果没有提供 ID,自动生成 if not skill_data.id: import uuid import re # 生成基于名称的 kebab-case ID base_id = skill_data.name.lower() base_id = re.sub(r'[^\w\s-]', '', base_id) # 移除特殊字符 base_id = re.sub(r'\s+', '-', base_id) # 空格替换为连字符 base_id = re.sub(r'-{2,}', '-', base_id) # 多个连字符合并为一个 # 如果生成结果为空,使用名称的拼音或默认前缀 if not base_id or len(base_id) < 3: base_id = f"skill-{uuid.uuid4().hex[:8]}" # 确保 ID 唯一(添加随机后缀) unique_id = f"{base_id}-{uuid.uuid4().hex[:6]}" # 创建新的 SkillCreate 对象,使用自动生成的 ID skill_data_with_id = skill_data.copy() skill_data_with_id.id = unique_id skill = await skill_manager.create_user_skill(skill_data_with_id) else: skill = await skill_manager.create_user_skill(skill_data) return skill except Exception as e: logger.error(f"创建 Skill 失败: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"创建 Skill 失败: {str(e)}" ) @router.put("/{skill_id}", response_model=Skill) async def update_skill( skill_id: str, skill_data: SkillUpdate, skill_manager: SkillManager = Depends(get_skill_manager) ): """更新 Skill 内容""" skill = await skill_manager.update_user_skill(skill_id, skill_data) if not skill: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Skill 不存在或不允许更新: {skill_id}" ) return skill @router.put("/{skill_id}/config", response_model=Skill) async def update_skill_config( skill_id: str, config_update: SkillConfigUpdate, skill_manager: SkillManager = Depends(get_skill_manager) ): """更新 Skill 配置""" skill = await skill_manager.load_skill(skill_id) if not skill: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Skill 不存在: {skill_id}" ) # 更新配置 if config_update.preset is not None: skill.config.preset = config_update.preset if config_update.parameters is not None: skill.config.parameters.update(config_update.parameters) if config_update.weights is not None: skill.config.weights = config_update.weights return skill @router.post("/{skill_id}/test", response_model=SkillTestResponse) async def test_skill( skill_id: str, test_request: SkillTestRequest, skill_manager: SkillManager = Depends(get_skill_manager), glm_client: GLMClient = Depends(get_glm_client) ): """ 测试 Skill 使用 Skill 的行为指导来处理测试输入,返回 LLM 的响应 """ skill = await skill_manager.load_skill(skill_id) if not skill: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Skill 不存在: {skill_id}" ) try: # 使用 GLM 客户端的 chat_with_skill 方法 response = await glm_client.chat_with_skill( skill_behavior=skill.behavior_guide, user_input=test_request.test_input, context=test_request.context or {}, temperature=test_request.temperature ) return SkillTestResponse( skill_id=skill_id, skill_name=skill.name, response=response ) except Exception as e: logger.error(f"测试 Skill 失败: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"测试 Skill 失败: {str(e)}" ) @router.delete("/{skill_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_skill( skill_id: str, skill_manager: SkillManager = Depends(get_skill_manager) ): """删除用户 Skill""" success = await skill_manager.delete_user_skill(skill_id) if not success: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Skill 不存在或不允许删除: {skill_id}" ) return None @router.post("/{skill_id}/reload", response_model=Skill) async def reload_skill( skill_id: str, skill_manager: SkillManager = Depends(get_skill_manager) ): """重新加载 Skill(清除缓存)""" skill = await skill_manager.reload_skill(skill_id) if not skill: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Skill 不存在: {skill_id}" ) return skill @router.get("/categories/list", response_model=List[str]) async def list_categories( skill_manager: SkillManager = Depends(get_skill_manager) ): """列出所有 Skill 分类""" skills = await skill_manager.list_skills() categories = set(skill.category for skill in skills) return sorted(list(categories)) @router.post("/generate", response_model=SkillGenerateResponse) async def generate_skill_with_ai( request: SkillGenerateRequest, skill_manager: SkillManager = Depends(get_skill_manager), glm_client: GLMClient = Depends(get_glm_client) ): """ 使用 AI 生成 Skill(类似 Claude Code 的 skill-creator) 流程: 1. 加载 skill-creator 的行为指导 2. 将用户需求和 skill-creator 标准一起发送给 GLM-4.7 3. AI 生成符合标准的 Skill 内容 Args: request: 包含用户描述、分类、标签的请求 Returns: 生成的 Skill 内容和建议信息 """ try: # 1. 加载 skill-creator 的行为指导 skill_creator = await skill_manager.load_skill("skill-creator") if not skill_creator: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="skill-creator 未找到,请确保内置 Skills 正确安装" ) # 2. 构建提示词 user_requirements = f"""用户想要创建的 Skill 描述: {request.description} """ if request.category: user_requirements += f"\n指定分类:{request.category}" if request.tags: user_requirements += f"\n指定标签:{', '.join(request.tags)}" system_prompt = f"""你是一个专业的 Skill 创建专家。 以下是 skill-creator 的行为指导(关于如何创建有效 Skill 的指南): {'━' * 60} {skill_creator.behavior_guide} {'━' * 60} 你的任务是根据用户的需求,创建一个符合上述标准的 Skill。 **重要要求**: 1. SKILL.md 必须以 YAML frontmatter 开始,包含 name 和 description 字段 2. description 应该清晰说明此 Skill 的用途和使用场景 3. 行为指导部分应该简洁、具体,避免冗余的解释 4. 使用 markdown 格式 5. 返回完整的 SKILL.md 内容 请以 JSON 格式返回结果,包含以下字段: - suggested_id: 建议 Skill ID(kebab-case,如 dialogue-writer-ancient) - suggested_name: 建议 Skill 名称(简短中文) - skill_content: 完整的 SKILL.md 内容 - category: 分类(如"编剧"、"审核"、"通用"等) - suggested_tags: 建议标签数组 - explanation: 对生成的 Skill 的简要说明(中文) """ # 3. 调用 GLM-4.7 生成 response = await glm_client.chat( messages=[ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_requirements} ], temperature=request.temperature ) # 4. 解析响应 import json import re response_text = response["choices"][0]["message"]["content"] # 尝试提取 JSON json_match = re.search(r'\{[\s\S]*\}', response_text) if json_match: result = json.loads(json_match.group()) else: # 如果没有找到 JSON,创建一个默认响应 result = { "suggested_id": "custom-skill", "suggested_name": "自定义 Skill", "skill_content": response_text, "category": request.category or "通用", "suggested_tags": request.tags or ["自定义"], "explanation": "AI 生成的 Skill 内容" } return SkillGenerateResponse(**result) except HTTPException: raise except Exception as e: logger.error(f"AI 生成 Skill 失败: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"AI 生成 Skill 失败: {str(e)}" ) @router.get("/{skill_id}/with-references", response_model=dict) async def get_skill_with_references( skill_id: str, skill_manager: SkillManager = Depends(get_skill_manager), include_references: bool = True ): """ 获取 Skill 及其参考文件 这是实现参考文件融入 LLM 的关键端点: - 返回 Skill 对象 - 返回所有参考文件的内容 Args: skill_id: Skill ID include_references: 是否包含参考文件内容 """ try: skill = await skill_manager.load_skill(skill_id) if not skill: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Skill 不存在: {skill_id}" ) result = {"skill": skill.model_dump()} # 加载参考文件(如果需要) if include_references: references = await skill_manager.load_skill_references(skill.id) result["references"] = references return result except HTTPException: raise except Exception as e: logger.error(f"获取 Skill 失败: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"获取 Skill 失败: {str(e)}" ) @router.post("/analyze", response_model=SkillAnalysis) async def analyze_skill_requirements( request: SkillGenerationRequest, skill_manager: SkillManager = Depends(get_skill_manager), glm_client: GLMClient = Depends(get_glm_client) ): """ 分析 Skill 创建需求(完整流程 Step 1: Understanding) Args: request: 包含用户意图、描述、用例的请求 Returns: SkillAnalysis: 需求分析结果 """ try: # Step 1: 加载 skill-creator skill_creator = await skill_manager.load_skill("skill-creator") if not skill_creator: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="skill-creator 未找到,请确保内置 Skills 正确安装" ) # Step 2: 需求分析 user_intent = request.user_intent or "创建新的 Skill" system_prompt = f"""你是一个专业的 Skill 创建分析师。 以下是 skill-creator 的行为指导(关于如何创建有效 Skill 的指南): {'━' * 60} {skill_creator.behavior_guide} {'━' * 60} 你的任务是分析用户的需求:{user_intent} 请从以下几个方面分析: 1. **核心功能**:这个 Skill 主要用于什么? 2. **目标用户**:谁会使用这个 Skill? 3. **技术要求**:前端、后端、数据分析等 4. **特殊要求**:多语言支持、高精度输出、实时处理 分析用户需求,并提出 Skill 创作建议。 请以 JSON 格式返回分析结果,包含以下字段: - intent_analysis: 用户的意图分析 - suggested_category: 建议的分类(如 "dialogue", "analysis", "writing") - suggested_features: 建议的核心功能列表 - complexity_assessment: 复杂度评估(simple/medium/complex) - recommended_approach: 推荐的实现方式 """ # 构建分析请求 analysis_request = { "user_intent": user_intent, "user_description": request.user_intent, "use_cases": request.use_cases or [] } # 调用 LLM 进行需求分析 analysis_response = await glm_client.chat( messages=[ {"role": "system", "content": system_prompt}, {"role": "user", "content": f"分析以下需求:{json.dumps(analysis_request, ensure_ascii=False)}"} ], temperature=0.5 ) # 解析分析结果 try: import json analysis_result = json.loads(analysis_response["choices"][0]["message"]["content"]) # 提取结构化数据 intent_analysis = analysis_result.get("intent_analysis", {}) suggested_category = analysis_result.get("suggested_category", "general") suggested_features = analysis_result.get("suggested_features", []) complexity_assessment = analysis_result.get("complexity_assessment", "medium") recommended_approach = analysis_result.get("recommended_approach", "standard") except json.JSONDecodeError: # 如果无法解析,使用默认分析 intent_analysis = {"primary": "创建新的 Skill", "details": "用户想要创建自定义 Skill"} suggested_category = "general" suggested_features = ["standard_dialogue_creation", "basic_content_editing"] complexity_assessment = "medium" recommended_approach = "standard" return SkillAnalysis( intent_analysis=intent_analysis, suggested_category=suggested_category, suggested_features=suggested_features, complexity_assessment=complexity_assessment, recommended_approach=recommended_approach ) except HTTPException: raise except Exception as e: logger.error(f"需求分析失败: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"需求分析失败: {str(e)}" ) # ============================================================================ # Skill 选择和路由(LLM 自动选择合适的 Skills) # ============================================================================ @router.post("/select-skills", response_model=SkillSelectionResult) async def select_skills_for_context( criteria: SkillSelectionCriteria, skill_manager: SkillManager = Depends(get_skill_manager), glm_client: GLMClient = Depends(get_glm_client) ): """ LLM 根据描述自动选择最合适的 Skills 流程: 1. 分析用户输入 2. 匹配所有 Skills 的 metadata 3. 评分和排序 4. 返回选择结果 Args: criteria: 选择条件(用户意图、上下文、约束) """ try: # 加载所有可用 Skills all_skills = await skill_manager.list_skills(skill_type="user") # 提取 Skills 的 metadata skills_metadata = [] for skill in all_skills: metadata = { "id": skill.id, "name": skill.name, "description": skill.description, "category": skill.category, "tags": skill.tags, "keywords": skill.description.split(), "capabilities": skill.behavior_guide[:200] if skill.behavior_guide else "" } skills_metadata.append(metadata) # 构建提示词 context_info = "" if criteria.context: context_info = f"\n上下文信息:{json.dumps(criteria.context, ensure_ascii=False)}\n" if criteria.exclude_skills: exclude_info = f"\n排除的 Skills:{', '.join(criteria.exclude_skills)}\n" system_prompt = f"""你是一个智能 Skill 选择助手。 以下是可以使用的 Skills 及其元数据: {'━' * 60} """ for i, skill_meta in enumerate(skills_metadata, 1): system_prompt += f"\n{i+1}. {skill_meta['name']}: {skill_meta['description']}\n" if skill_meta['capabilities']: system_prompt += f"\n 能力:{skill_meta['capabilities']}\n" system_prompt += f"\n" system_prompt += f"""你的任务是根据用户的需求和偏好,从以下 Skills 中选择最合适的。 用户需求: - {criteria.user_intent} {criteria.exclude_info if criteria.exclude_skills else ""} {criteria.context_info if criteria.context else ""} {criteria.required_category if criteria.required_category else ""} {criteria.required_tags if criteria.required_tags else ""} 选择标准: 1. 相关性(description/keywords 匹配度) 2. 功能覆盖度(capabilities 是否满足需求) 3. 难度适配(complexity_assessment 是否匹配) 请返回 JSON 数组,包含选中的 Skill ID 列表和选择理由。 每个元素应包含: - skill_id: Skill ID - score: 匹配分数(0-100) - reason: 选择理由 """ # 调用 LLM 进行选择 selection_response = await glm_client.chat( messages=[ {"role": "system", "content": system_prompt}, {"role": "user", "content": f"请分析以下 Skills,并返回最合适的 3-5 个:{json.dumps(skills_metadata, ensure_ascii=False)}"} ], temperature=0.3 ) # 解析选择结果 try: import json selected_ids = json.loads(selection_response["choices"][0]["message"]["content"]) except: selected_ids = [] # 如果是复杂场景,需要 LLM 进行智能选择 if len(skills_metadata) > 5 or criteria.user_intent == "复杂需求分析": # 使用结构化输出确保可解析性 structured_prompt = """ 请以以下 JSON 格式返回选择,必须是可以解析的 JSON 数组: [ {{"skill_id": "...", "score": 85, "reason": "..."}}, ... ] 确保输出是有效的 JSON 格式。 """ selection_response = await glm_client.chat( messages=[ {"role": "system", "content": structured_prompt}, {"role": "user", "content": f"用户需求:{criteria.user_intent or '标准 Skill 创建'}\n\n\nAvailable Skills: {json.dumps(skills_metadata, ensure_ascii=False)}"} ], temperature=0.2 ) try: selected_ids = json.loads(selection_response["choices"][0]["message"]["content"]) except: selected_ids = [] return SkillSelectionResult( selected_skills=[s for s in all_skills if s.id in selected_ids], selection_reason=f"基于需求分析选择了最相关的 Skills", confidence_scores={} ) except HTTPException: logger.error(f"Skill 选择失败: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Skill 选择失败: {str(e)}" ) # ============================================================================ # Agent 工作流配置 API # ============================================================================ @router.post("/agent-workflow/configure", response_model=AgentWorkflowConfig) async def configure_agent_workflow( config: AgentWorkflowConfig, skill_manager: SkillManager = Depends(get_skill_manager) ): """ 配置 Agent 工作流的 Skill 使用方式 这是实现 Agent 与 Skill 深度结合的关键配置: - 定义哪些工作流步骤使用哪些 Skills - 设置默认温度 - 启用/禁用 Skill 替代 Args: config: 包含工作流步骤配置 """ try: # 加载 skill-creator 获取指导 skill_creator = await skill_manager.load_skill("skill-creator") # 保存配置 await skill_manager.save_workflow_config(config) return { "success": True, "message": "Agent 工作流配置已更新", "config": config.model_dump() } except Exception as e: logger.error(f"配置失败: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"配置失败: {str(e)}" ) # ============================================================================ # 基础 CRUD 端点继续 # ============================================================================ # ============================================================================ # Agent-Skill 生命周期端点 - 使用 skill-creator 脚本 # ============================================================================ class SkillInitRequest(BaseModel): """初始化 Skill 模板请求""" skill_name: str # Skill 名称(hyphen-case) output_path: Optional[str] = None # 输出路径 class SkillValidateScriptRequest(BaseModel): """脚本验证 Skill 请求""" skill_id: str # 要验证的 Skill ID class SkillPackageRequest(BaseModel): """打包 Skill 请求""" skill_id: str # 要打包的 Skill ID output_dir: Optional[str] = None # 输出目录 @router.post("/init", response_model=dict) async def init_skill_template( request: SkillInitRequest, skill_manager: SkillManager = Depends(get_skill_manager) ): """ 初始化 Skill 模板(使用 skill-creator 的 init_skill.py 脚本) 这是 Agent-Skill 生命周期的正确实现: - 调用 skill-creator 的 init_skill.py 脚本 - 创建标准的 Skill 目录结构(SKILL.md、scripts/、references/、assets/) """ try: result = await skill_manager.init_skill_template( skill_name=request.skill_name, output_path=request.output_path ) if result["success"]: logger.info(f"成功初始化 Skill 模板: {request.skill_name}") return { "success": True, "message": f"Skill 模板 '{request.skill_name}' 初始化成功", "output": result["output"] } else: logger.error(f"初始化 Skill 失败: {result.get('error')}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=result.get("error", "初始化失败") ) except Exception as e: logger.error(f"初始化 Skill 异常: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"初始化 Skill 异常: {str(e)}" ) @router.post("/validate-script", response_model=dict) async def validate_skill_script( request: SkillValidateScriptRequest, skill_manager: SkillManager = Depends(get_skill_manager) ): """ 验证 Skill 结构(使用 skill-creator 的 quick_validate.py 脚本) 这是 Agent-Skill 生命周期的正确实现: - 调用 skill-creator 的 quick_validate.py 脚本 - 检查 YAML frontmatter、命名规范等 """ try: result = await skill_manager.validate_skill_structure(request.skill_id) if result["success"]: logger.info(f"Skill 验证通过: {request.skill_id}") return { "success": True, "valid": True, "message": result["output"] } else: logger.warning(f"Skill 验证失败: {request.skill_id}") return { "success": True, "valid": False, "error": result.get("error") } except Exception as e: logger.error(f"验证 Skill 异常: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"验证 Skill 异常: {str(e)}" ) @router.post("/package", response_model=dict) async def package_skill( request: SkillPackageRequest, skill_manager: SkillManager = Depends(get_skill_manager) ): """ 打包 Skill(使用 skill-creator 的 package_skill.py 脚本) 这是 Agent-Skill 生命周期的正确实现: - 调用 skill-creator 的 package_skill.py 脚本 - 将 Skill 打包成 .skill 文件用于分发 """ try: result = await skill_manager.package_skill( skill_id=request.skill_id, output_dir=request.output_dir ) if result["success"]: logger.info(f"成功打包 Skill: {request.skill_id}") return { "success": True, "message": f"Skill '{request.skill_id}' 打包成功", "output": result["output"] } else: logger.error(f"打包 Skill 失败: {request.skill_id}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=result.get("error", "打包失败") ) except Exception as e: logger.error(f"打包 Skill 异常: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"打包 Skill 异常: {str(e)}" ) @router.get("/tools", response_model=List[dict]) async def list_available_tools( skill_manager: SkillManager = Depends(get_skill_manager) ): """ 列出所有已注册的工具(Agent-Skill 生命周期:注册阶段) 返回所有已注册工具的 JSON Schema,用于: - 注入到 LLM System Prompt - 或作为 API 的 tools 参数 """ return skill_manager.get_available_tools_for_llm() @router.post("/execute-tool", response_model=dict) async def execute_tool( tool_name: str, parameters: dict, skill_manager: SkillManager = Depends(get_skill_manager) ): """ 执行工具(Agent-Skill 生命周期:路由 → 执行 → 反馈) 实现: - 路由:根据 tool_name 找到对应的 handler - 执行:调用 handler 执行实际操作 - 反馈:返回结果给调用者(通常是 LLM) 这是 Agent-Skill 生命周期的核心实现 """ result = await skill_manager.route_and_execute_tool(tool_name, parameters) return result # ============================================================================ # 增强的 Skill 生成工作流(文档驱动) # ============================================================================ import uuid from collections import defaultdict from app.core.documentation_fetcher import get_documentation_fetcher, DocumentationFetcher # 预览存储(生产环境应使用 Redis 或数据库) _preview_storage: Dict[str, Dict[str, Any]] = defaultdict(dict) _preview_references: Dict[str, Dict[str, str]] = defaultdict(dict) @router.post("/fetch-doc", response_model=dict) async def fetch_documentation( request: DocFetchRequest, doc_fetcher: DocumentationFetcher = Depends(get_documentation_fetcher) ): """ 获取文档内容(用于预览或生成 Skill) Args: request: 包含文档 URL 和可选的 CSS 选择器 Returns: 获取的文档内容 """ try: result = await doc_fetcher.fetch_from_url(request.url, request.selector) if result.get("success"): return { "success": True, "title": result.get("title"), "content": result.get("content"), "word_count": result.get("word_count"), "char_count": result.get("char_count"), "url": result.get("url") } else: return { "success": False, "error": result.get("error") } except Exception as e: logger.error(f"获取文档失败: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"获取文档失败: {str(e)}" ) @router.post("/fetch-github-doc", response_model=dict) async def fetch_github_documentation( request: GitHubDocFetchRequest, doc_fetcher: DocumentationFetcher = Depends(get_documentation_fetcher) ): """ 获取 GitHub 文档内容 Args: request: GitHub 仓库和文档路径 Returns: 获取的文档内容 """ try: result = await doc_fetcher.fetch_from_github( repo_url=request.repo_url, docs_path=request.docs_path ) if result.get("success"): return { "success": True, "repo_url": result.get("repo_url"), "docs_path": result.get("docs_path"), "content": result.get("content"), "word_count": result.get("word_count"), "char_count": result.get("char_count") } else: return { "success": False, "error": result.get("error") } except Exception as e: logger.error(f"获取 GitHub 文档失败: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"获取 GitHub 文档失败: {str(e)}" ) @router.post("/generate-from-docs", response_model=SkillGenerateFromDocsResponse) async def generate_skill_from_documentation( request: SkillGenerateFromDocsRequest, skill_manager: SkillManager = Depends(get_skill_manager), glm_client: GLMClient = Depends(get_glm_client), doc_fetcher: DocumentationFetcher = Depends(get_documentation_fetcher) ): """ 基于文档生成 Skill(新工作流) 完整流程: 1. 获取文档内容(URL/GitHub/上传) 2. 提取关键信息 3. 使用 LLM 生成 SKILL.md 4. 生成预览 ID 5. 返回可预览的内容 用户可以: - 预览生成的内容 - 手动编辑 - 用 AI 调整 - 确认后保存到系统 """ try: logger.info(f"开始基于文档生成 Skill: {request.skill_name}") # 1. 收集所有文档内容 all_docs = [] doc_summary_parts = [] # 从 URL 获取 if request.doc_urls: for url in request.doc_urls: result = await doc_fetcher.fetch_from_url(url) if result.get("success"): all_docs.append({ "source": f"URL: {url}", "title": result.get("title"), "content": result.get("content") }) doc_summary_parts.append(f"- {result.get('title')} ({url})") # 从 GitHub 获取 if request.github_repos: for github_req in request.github_repos: result = await doc_fetcher.fetch_from_github( repo_url=github_req.repo_url, docs_path=github_req.docs_path ) if result.get("success"): all_docs.append({ "source": f"GitHub: {github_req.repo_url}/{github_req.docs_path}", "title": github_req.docs_path, "content": result.get("content") }) doc_summary_parts.append(f"- {github_req.repo_url}/{github_req.docs_path}") # 用户上传的 references if request.uploaded_references: for filename, content in request.uploaded_references.items(): all_docs.append({ "source": f"上传: {filename}", "title": filename, "content": content }) doc_summary_parts.append(f"- {filename} (用户上传)") # 2. 构建 LLM 提示词 doc_context = "" if all_docs: # 取文档摘要(前 500 字) doc_summary = "\n".join(doc_summary_parts) # 构建文档上下文(避免超过上下文窗口) doc_context_parts = [] for doc in all_docs[:3]: # 最多 3 个文档 content_preview = doc["content"][:500].replace("\n", " ") doc_context_parts.append(f"#### {doc['title']}\n{content_preview}...") doc_context = f""" **文档来源**: {doc_summary} **文档内容摘要**: {''.join(doc_context_parts)} """ # 3. 加载 skill-creator 标准 skill_creator = await skill_manager.load_skill("skill-creator") creator_guide = skill_creator.behavior_guide if skill_creator else "" # 4. 构建 LLM 提示词 system_prompt = f"""你是一个专业的 Skill 创建专家。 {creator_guide if creator_guide else ''} 你的任务是基于提供的文档内容,创建一个高质量的 Skill。 **重要要求**: 1. SKILL.md 必须以 YAML frontmatter 开始 2. YAML 必须包含:name, description, category, tags 3. description 应该清晰说明此 Skill 的用途和使用场景 4. 行为指导部分应该简洁、具体,从文档中提取关键信息 5. 如果文档包含代码示例、API、配置等,应该在行为指导中体现 6. 使用 markdown 格式 请以 JSON 格式返回结果,包含以下字段: - skill_content: 完整的 SKILL.md 内容(以 --- 开头的 YAML frontmatter + markdown 内容) - explanation: 对生成的 Skill 的简要说明(中文) - suggested_category: 建议的分类 - suggested_tags: 建议的标签数组""" user_prompt = f"""请基于以下文档创建一个 Skill: **Skill 名称**:{request.skill_name} **描述**:{request.description or f'基于文档生成的 Skill:{request.skill_name}'} **指定分类**:{request.category or '自动推断'} **指定标签**:{', '.join(request.tags) if request.tags else '自动推断'} {doc_context if request.include_doc_summary else ''} 请生成完整的 SKILL.md:""" # 5. 调用 LLM 生成 response = await glm_client.chat( messages=[ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt} ], temperature=request.temperature ) response_text = response["choices"][0]["message"]["content"] # 6. 解析 JSON 响应 import json import re json_match = re.search(r'\{[\s\S]*\}', response_text) if json_match: try: result = json.loads(json_match.group()) skill_content = result.get("skill_content", response_text) explanation = result.get("explanation", "AI 生成的 Skill") suggested_category = result.get("suggested_category", request.category or "通用") suggested_tags = result.get("suggested_tags", request.tags or ["自定义"]) except json.JSONDecodeError: skill_content = response_text explanation = "AI 生成的 Skill" suggested_category = request.category or "通用" suggested_tags = request.tags or ["自定义"] else: skill_content = response_text explanation = "AI 生成的 Skill" suggested_category = request.category or "通用" suggested_tags = request.tags or ["自定义"] # 7. 生成预览 ID preview_id = str(uuid.uuid4()) # 8. 存储预览数据 _preview_storage[preview_id] = { "skill_name": request.skill_name, "skill_content": skill_content, "category": suggested_category, "tags": suggested_tags, "doc_urls": request.doc_urls, "github_repos": [r.model_dump() for r in request.github_repos], "uploaded_references": request.uploaded_references, "explanation": explanation, "created_at": str(datetime.now()) } # 9. 存储 references(后续保存时使用) references = {} for i, doc in enumerate(all_docs): filename = f"{doc['title'].replace('/', '-').replace(' ', '_')}.md" # 清理文件名 filename = ''.join(c for c in filename if c.isalnum() or c in '._-') references[filename] = doc["content"] _preview_references[preview_id] = references # 10. 生成文档摘要 doc_summary = "\n".join(doc_summary_parts) if doc_summary_parts else "无文档来源" return SkillGenerateFromDocsResponse( success=True, preview_id=preview_id, skill_content=skill_content, skill_name=request.skill_name, suggested_id=request.skill_name.lower().replace(" ", "-").replace("_", "-"), category=suggested_category, tags=suggested_tags, doc_summary=doc_summary, references_count=len(references), explanation=explanation ) except HTTPException: raise except Exception as e: logger.error(f"基于文档生成 Skill 失败: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"生成失败: {str(e)}" ) @router.post("/preview", response_model=SkillPreviewResponse) async def preview_skill( request: SkillPreviewRequest ): """ 预览 Skill(验证和解析) 返回: - 解析后的元数据 - 验证警告 - 字数统计 - 预估 token 数 """ try: # 解析 YAML frontmatter metadata = {} content = request.skill_content import yaml yaml_match = re.match(r'^---\n(.*?)\n---', content, re.DOTALL) if yaml_match: try: metadata = yaml.safe_load(yaml_match.group(1)) or {} except: pass # 验证警告 warnings = [] if not metadata.get("name"): warnings.append("缺少 name 字段") if not metadata.get("description"): warnings.append("缺少 description 字段") if len(content) < 100: warnings.append("内容过短,可能不够详细") if len(content) > 50000: warnings.append("内容过长,可能占用过多上下文") # 统计 word_count = len(content.split()) estimated_tokens = int(len(content) * 1.3) # 粗略估计 # 生成预览 ID preview_id = str(uuid.uuid4()) _preview_storage[preview_id] = { "skill_name": request.skill_name, "skill_content": content, "category": request.category, "tags": request.tags, "metadata": metadata, "warnings": warnings, "created_at": str(datetime.now()) } return SkillPreviewResponse( preview_id=preview_id, skill_content=content, parsed_metadata=metadata, validation_warnings=warnings, word_count=word_count, estimated_tokens=estimated_tokens ) except Exception as e: logger.error(f"预览 Skill 失败: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"预览失败: {str(e)}" ) @router.post("/refine", response_model=SkillRefineResponse) async def refine_skill_with_ai( request: SkillRefineRequest, glm_client: GLMClient = Depends(get_glm_client) ): """ 使用 AI 调整/优化 Skill 用户可以输入自然语言提示词,让 AI 修改 Skill 内容。 示例提示词: - "把行为指导改得更简洁" - "增加一个快速开始部分" - "添加更多代码示例" - "优化描述,让它更清晰" """ try: logger.info(f"AI 调整 Skill,提示词: {request.refinement_prompt[:50]}...") prompt = f"""请根据以下要求调整 SKILL.md 内容: **调整要求**: {request.refinement_prompt} **原始 SKILL.md**: {request.skill_content} 请返回完整的调整后的 SKILL.md 内容(不要包含其他解释):""" response = await glm_client.chat( messages=[{"role": "user", "content": prompt}], temperature=request.temperature ) refined_content = response["choices"][0]["message"]["content"].strip() # 清理可能的 markdown 代码块标记 if refined_content.startswith("```"): lines = refined_content.split("\n") refined_content = "\n".join(lines[1:]) if refined_content.endswith("```"): refined_content = "\n".join(refined_content.split("\n")[:-1]) # 生成变更摘要 changes_summary = f"根据提示词「{request.refinement_prompt}」进行了调整" return SkillRefineResponse( success=True, refined_content=refined_content.strip(), changes_summary=changes_summary, original_length=len(request.skill_content), new_length=len(refined_content.strip()) ) except Exception as e: logger.error(f"AI 调整失败: {str(e)}") return SkillRefineResponse( success=False, refined_content=request.skill_content, changes_summary=f"调整失败: {str(e)}", original_length=len(request.skill_content), new_length=len(request.skill_content) ) @router.post("/save-from-preview", response_model=Skill) async def save_skill_from_preview( request: SkillSaveFromPreviewRequest, skill_manager: SkillManager = Depends(get_skill_manager) ): """ 从预览保存 Skill 到系统 保存流程: 1. 创建 Skill 目录结构 2. 保存 SKILL.md 3. 保存 references(如果有) 4. 注册到 SkillManager """ try: logger.info(f"从预览保存 Skill: {request.skill_id}") # 1. 创建 Skill skill_data = SkillCreate( id=request.skill_id, name=request.skill_id.replace("-", " ").title(), content=request.skill_content, category="通用", # 从 YAML frontmatter 解析 tags=[] # 从 YAML frontmatter 解析 ) skill = await skill_manager.create_user_skill(skill_data) # 2. 保存 references(如果有) if request.references: skill_path = skill_manager._find_skill_path(request.skill_id) if skill_path: refs_dir = skill_path.parent / "references" refs_dir.mkdir(exist_ok=True) for filename, content in request.references.items(): # 确保文件名安全 safe_filename = filename.replace("..", "").replace("/", "").replace("\\", "") if not safe_filename.endswith(".md"): safe_filename += ".md" ref_file = refs_dir / safe_filename ref_file.write_text(content, encoding="utf-8") logger.info(f"保存 reference: {request.skill_id}/references/{safe_filename}") # 3. 清理预览数据 if request.preview_id in _preview_storage: del _preview_storage[request.preview_id] if request.preview_id in _preview_references: del _preview_references[request.preview_id] return skill except Exception as e: logger.error(f"保存 Skill 失败: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"保存失败: {str(e)}" ) @router.get("/preview/{preview_id}", response_model=dict) async def get_preview(preview_id: str): """ 获取预览数据 """ if preview_id not in _preview_storage: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"预览不存在: {preview_id}" ) preview_data = _preview_storage[preview_id] references = _preview_references.get(preview_id, {}) return { "preview": preview_data, "references": references, "references_count": len(references) } @router.delete("/preview/{preview_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_preview(preview_id: str): """ 删除预览数据 """ if preview_id in _preview_storage: del _preview_storage[preview_id] if preview_id in _preview_references: del _preview_references[preview_id] return None @router.put("/{skill_id}/with-references", response_model=Skill) async def update_skill_with_references( skill_id: str, request: SkillSaveFromPreviewRequest, skill_manager: SkillManager = Depends(get_skill_manager) ): """ 更新 Skill 及其 References 用于编辑模式,同时更新 SKILL.md 和 references/ 目录中的文件 """ try: logger.info(f"更新 Skill 及 references: {skill_id}") # 1. 更新 SKILL.md skill_update = SkillUpdate(content=request.skill_content) skill = await skill_manager.update_user_skill(skill_id, skill_update) if not skill: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Skill 不存在: {skill_id}" ) # 2. 更新 references(如果有) if request.references is not None: # 允许传入空字典清空references skill_path = skill_manager._find_skill_path(skill_id) if skill_path: refs_dir = skill_path.parent / "references" # 如果传入空字典,删除整个references目录 if len(request.references) == 0: if refs_dir.exists(): import shutil shutil.rmtree(refs_dir) logger.info(f"清空 references: {skill_id}/references/") else: # 确保目录存在 refs_dir.mkdir(exist_ok=True) # 清空现有文件 for existing_file in refs_dir.iterdir(): if existing_file.is_file(): existing_file.unlink() # 写入新文件 for filename, content in request.references.items(): safe_filename = filename.replace("..", "").replace("/", "").replace("\\", "") if not safe_filename.endswith(".md"): safe_filename += ".md" ref_file = refs_dir / safe_filename ref_file.write_text(content, encoding="utf-8") logger.info(f"更新 reference: {skill_id}/references/{safe_filename}") # 清除缓存 if skill_id in skill_manager._cache: del skill_manager._cache[skill_id] # 重新加载并返回 return await skill_manager.load_skill(skill_id) except HTTPException: raise except Exception as e: logger.error(f"更新 Skill 失败: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"更新失败: {str(e)}" )