2026-02-03 01:12:39 +08:00

1487 lines
49 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Skill 管理 API 路由
提供:
1. Skill 的 CRUD 操作
2. Agent-Skill 生命周期端点
3. AI 辅助 Skill 创建(完整流程)
4. Skill 选择和路由LLM 与 Skills 结合)
5. Agent 工作流配置
6. 文档驱动的 Skill 生成(新增)
"""
import re
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 作为唯一标识
- 基于名称生成可读 IDkebab-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 IDkebab-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)}"
)