mirror of
https://github.com/countbot-ai/CountBot.git
synced 2026-09-14 20:46:47 +08:00
08e66ccc02
1. 解决多个 issue 反馈问题,修复已知 Bug 2. 优化前端界面与交互体验 3. 优化 tool 调用链路,较此前版本节省约 70% token 用量 4. 增加模型思考控制开关,提升整体响应体感 5. 新增 find-skills,全面接入腾讯云 SkillsHub,支持通过对话进行 skills 管理 6. 新增 ima-knowledge-base、ima-notes,全面接入 IMA 知识库和笔记,支持知识库与笔记内容管理和写入 7. 优化 README,并补充 0.7.0 发布说明 8. 发布说明:https://654321.ai/docs/releases/v0.7.0
946 lines
30 KiB
Python
946 lines
30 KiB
Python
"""Skills API 端点"""
|
||
|
||
import json
|
||
from pathlib import Path
|
||
from typing import Any, List, Optional
|
||
|
||
from fastapi import APIRouter, HTTPException, Request, status
|
||
from loguru import logger
|
||
from pydantic import BaseModel, Field
|
||
|
||
from backend.modules.agent.skills import SkillsLoader
|
||
from backend.modules.agent.skills_config import SkillConfigManager
|
||
from backend.modules.agent.skills_schema import SkillConfigSchema
|
||
from backend.modules.config.loader import config_loader
|
||
from backend.utils.paths import WORKSPACE_DIR
|
||
|
||
router = APIRouter(prefix="/api/skills", tags=["skills"])
|
||
|
||
|
||
# ============================================================================
|
||
# Request/Response Models
|
||
# ============================================================================
|
||
|
||
|
||
class SkillInfo(BaseModel):
|
||
"""技能信息"""
|
||
|
||
name: str = Field(..., description="技能名称")
|
||
description: str = Field(..., description="技能描述")
|
||
enabled: bool = Field(..., description="是否启用")
|
||
auto_load: bool = Field(..., description="是否自动加载", alias="autoLoad")
|
||
has_config: bool = Field(False, description="是否支持可视化配置", alias="hasConfig")
|
||
requirements: List[str] = Field(default_factory=list, description="依赖要求")
|
||
source: str = Field(..., description="技能来源: workspace、builtin 或 openclaw")
|
||
|
||
class Config:
|
||
populate_by_name = True
|
||
|
||
|
||
class SkillDetail(BaseModel):
|
||
"""技能详情"""
|
||
|
||
name: str = Field(..., description="技能名称")
|
||
description: str = Field(..., description="技能描述")
|
||
content: str = Field(..., description="技能内容")
|
||
enabled: bool = Field(..., description="是否启用")
|
||
auto_load: bool = Field(..., description="是否自动加载", alias="autoLoad")
|
||
has_config: bool = Field(False, description="是否支持可视化配置", alias="hasConfig")
|
||
requirements: List[str] = Field(default_factory=list, description="依赖要求")
|
||
source: str = Field(..., description="技能来源: workspace、builtin 或 openclaw")
|
||
|
||
class Config:
|
||
populate_by_name = True
|
||
|
||
|
||
class ListSkillsResponse(BaseModel):
|
||
"""技能列表响应"""
|
||
|
||
skills: List[SkillInfo] = Field(..., description="技能列表")
|
||
|
||
|
||
class ToggleSkillRequest(BaseModel):
|
||
"""切换技能请求"""
|
||
|
||
enabled: bool = Field(..., description="是否启用")
|
||
|
||
|
||
class ToggleSkillResponse(BaseModel):
|
||
"""切换技能响应"""
|
||
|
||
success: bool = Field(..., description="是否成功")
|
||
message: Optional[str] = Field(None, description="消息")
|
||
|
||
|
||
class CreateSkillRequest(BaseModel):
|
||
"""创建技能请求"""
|
||
|
||
name: str = Field(..., description="技能名称", min_length=1, max_length=50)
|
||
description: str = Field("", description="技能描述")
|
||
content: str = Field(..., description="技能内容", min_length=1)
|
||
auto_load: bool = Field(False, description="是否自动加载", alias="autoLoad")
|
||
requirements: List[str] = Field(default_factory=list, description="依赖要求")
|
||
|
||
class Config:
|
||
populate_by_name = True
|
||
|
||
|
||
class UpdateSkillRequest(BaseModel):
|
||
"""更新技能请求"""
|
||
|
||
description: str = Field("", description="技能描述")
|
||
content: str = Field(..., description="技能内容", min_length=1)
|
||
auto_load: bool = Field(False, description="是否自动加载", alias="autoLoad")
|
||
requirements: List[str] = Field(default_factory=list, description="依赖要求")
|
||
|
||
class Config:
|
||
populate_by_name = True
|
||
|
||
|
||
class DeleteSkillResponse(BaseModel):
|
||
"""删除技能响应"""
|
||
|
||
success: bool = Field(..., description="是否成功")
|
||
message: str = Field(..., description="消息")
|
||
|
||
|
||
class ConfigFieldSchema(BaseModel):
|
||
"""配置字段Schema"""
|
||
|
||
key: str = Field(..., description="字段键名")
|
||
type: str = Field(..., description="字段类型")
|
||
label: str = Field(..., description="显示标签")
|
||
description: Optional[str] = Field(None, description="字段说明")
|
||
required: bool = Field(False, description="是否必填")
|
||
sensitive: bool = Field(False, description="是否敏感信息")
|
||
readonly: bool = Field(False, description="是否只读")
|
||
default: Any = Field(None, description="默认值")
|
||
placeholder: Optional[str] = Field(None, description="占位符")
|
||
validation: Optional[str] = Field(None, description="正则验证")
|
||
min: Optional[int] = Field(None, description="最小值")
|
||
max: Optional[int] = Field(None, description="最大值")
|
||
options: Optional[List[dict]] = Field(None, description="选项列表")
|
||
help_url: Optional[str] = Field(None, description="帮助链接")
|
||
fields: Optional[List['ConfigFieldSchema']] = Field(None, description="子字段")
|
||
collapsible: bool = Field(False, description="是否可折叠")
|
||
|
||
|
||
class SkillConfigSchemaResponse(BaseModel):
|
||
"""技能配置Schema响应"""
|
||
|
||
has_schema: bool = Field(..., description="是否有Schema")
|
||
schema_definition: Optional[dict] = Field(None, description="Schema定义", alias="schema")
|
||
|
||
class Config:
|
||
populate_by_name = True
|
||
|
||
|
||
class SkillConfigResponse(BaseModel):
|
||
"""技能配置响应"""
|
||
|
||
has_config: bool = Field(..., description="是否有配置文件")
|
||
config: Optional[dict] = Field(None, description="配置内容")
|
||
status: str = Field(..., description="配置状态")
|
||
errors: List[str] = Field(default_factory=list, description="错误列表")
|
||
|
||
|
||
class UpdateSkillConfigRequest(BaseModel):
|
||
"""更新技能配置请求"""
|
||
|
||
config: dict = Field(..., description="配置内容")
|
||
|
||
|
||
class SkillConfigStatusResponse(BaseModel):
|
||
"""技能配置状态响应"""
|
||
|
||
status: str = Field(..., description="配置状态")
|
||
message: str = Field(..., description="状态消息")
|
||
errors: List[str] = Field(default_factory=list, description="错误列表")
|
||
|
||
|
||
class SkillConfigHelpResponse(BaseModel):
|
||
"""技能配置帮助响应"""
|
||
|
||
has_help: bool = Field(..., description="是否有帮助文档")
|
||
content: Optional[str] = Field(None, description="帮助内容")
|
||
|
||
|
||
class FixSkillConfigResponse(BaseModel):
|
||
"""修复技能配置响应"""
|
||
|
||
success: bool = Field(..., description="是否成功")
|
||
message: str = Field(..., description="消息")
|
||
|
||
|
||
class ReloadSkillsResponse(BaseModel):
|
||
"""重载技能响应"""
|
||
|
||
success: bool = Field(..., description="是否成功")
|
||
message: str = Field(..., description="消息")
|
||
total: int = Field(..., description="重载的技能总数")
|
||
|
||
|
||
# ============================================================================
|
||
# Helper Functions
|
||
# ============================================================================
|
||
|
||
|
||
def get_workspace_skills_dir() -> Path:
|
||
"""解析当前工作空间下的 skills 目录。"""
|
||
config = config_loader.config
|
||
workspace = Path(config.workspace.path) if config.workspace.path else WORKSPACE_DIR
|
||
workspace = workspace.resolve()
|
||
skills_dir = workspace / "skills"
|
||
skills_dir.mkdir(parents=True, exist_ok=True)
|
||
return skills_dir
|
||
|
||
|
||
def get_skill_config_manager() -> SkillConfigManager:
|
||
"""获取绑定当前工作空间的技能配置管理器。"""
|
||
return SkillConfigManager(get_workspace_skills_dir())
|
||
|
||
|
||
def get_skill_schema_loader() -> SkillConfigSchema:
|
||
"""获取绑定当前工作空间的技能 Schema 加载器。"""
|
||
return SkillConfigSchema(get_workspace_skills_dir())
|
||
|
||
|
||
def get_skills_loader(request: Request = None) -> SkillsLoader:
|
||
"""
|
||
获取全局技能加载器实例
|
||
|
||
优先从 app.state 获取全局单例,提升性能
|
||
"""
|
||
# 尝试从 app state 获取全局实例
|
||
if request and hasattr(request.app.state, 'shared'):
|
||
return request.app.state.shared["skills"]
|
||
|
||
# 回退:创建临时实例(向后兼容)
|
||
skills_dir = get_workspace_skills_dir()
|
||
logger.warning("Creating temporary SkillsLoader instance - performance may be impacted")
|
||
return SkillsLoader(skills_dir)
|
||
|
||
|
||
def ensure_workspace_skill(skill, action: str) -> None:
|
||
"""仅允许对工作空间技能执行写操作"""
|
||
if skill.source != "workspace":
|
||
raise HTTPException(
|
||
status_code=status.HTTP_403_FORBIDDEN,
|
||
detail=f"Cannot {action} non-workspace skills"
|
||
)
|
||
|
||
|
||
# ============================================================================
|
||
# Skills Endpoints
|
||
# ============================================================================
|
||
|
||
|
||
@router.get("", response_model=ListSkillsResponse)
|
||
async def list_skills(request: Request) -> ListSkillsResponse:
|
||
"""
|
||
获取所有技能列表(自动重载技能)
|
||
|
||
Returns:
|
||
ListSkillsResponse: 技能列表
|
||
"""
|
||
try:
|
||
skills_loader = get_skills_loader(request)
|
||
schema_loader = get_skill_schema_loader()
|
||
|
||
# 自动重载技能(确保显示最新的技能列表)
|
||
try:
|
||
skills_loader.reload()
|
||
logger.debug("Auto-reloaded skills when accessing skills page")
|
||
except Exception as e:
|
||
logger.warning(f"Failed to auto-reload skills: {e}")
|
||
|
||
skills_list = skills_loader.list_skills()
|
||
|
||
# 转换为响应格式
|
||
skills_info = []
|
||
for skill in skills_list:
|
||
# 获取技能摘要
|
||
summary = skills_loader.get_skill_summary(skill["name"])
|
||
|
||
skills_info.append(
|
||
SkillInfo(
|
||
name=skill["name"],
|
||
description=summary.get("description", ""),
|
||
enabled=skill.get("enabled", True),
|
||
auto_load=summary.get("auto_load", False),
|
||
has_config=schema_loader.has_schema(skill["name"]),
|
||
requirements=summary.get("requirements", []),
|
||
source=skill.get("source", "workspace"),
|
||
)
|
||
)
|
||
|
||
return ListSkillsResponse(skills=skills_info)
|
||
|
||
except Exception as e:
|
||
logger.exception(f"Failed to list skills: {e}")
|
||
raise HTTPException(
|
||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||
detail=f"Failed to list skills: {str(e)}"
|
||
)
|
||
|
||
|
||
@router.get("/{name}", response_model=SkillDetail)
|
||
async def get_skill(name: str, request: Request) -> SkillDetail:
|
||
"""
|
||
获取技能详情
|
||
|
||
Args:
|
||
name: 技能名称
|
||
|
||
Returns:
|
||
SkillDetail: 技能详情
|
||
"""
|
||
try:
|
||
skills_loader = get_skills_loader(request)
|
||
schema_loader = get_skill_schema_loader()
|
||
|
||
# 检查技能是否存在
|
||
skills_list = skills_loader.list_skills()
|
||
skill_exists = any(s["name"] == name for s in skills_list)
|
||
|
||
if not skill_exists:
|
||
raise HTTPException(
|
||
status_code=status.HTTP_404_NOT_FOUND,
|
||
detail=f"Skill '{name}' not found"
|
||
)
|
||
|
||
# 获取技能内容和摘要
|
||
content = skills_loader.load_skill(name)
|
||
summary = skills_loader.get_skill_summary(name)
|
||
|
||
# 获取启用状态
|
||
enabled = True
|
||
for skill in skills_list:
|
||
if skill["name"] == name:
|
||
enabled = skill.get("enabled", True)
|
||
break
|
||
|
||
# 获取技能来源
|
||
source = "workspace"
|
||
for skill in skills_list:
|
||
if skill["name"] == name:
|
||
source = skill.get("source", "workspace")
|
||
break
|
||
|
||
return SkillDetail(
|
||
name=name,
|
||
description=summary.get("description", ""),
|
||
content=content,
|
||
enabled=enabled,
|
||
auto_load=summary.get("auto_load", False),
|
||
has_config=schema_loader.has_schema(name),
|
||
requirements=summary.get("requirements", []),
|
||
source=source,
|
||
)
|
||
|
||
except HTTPException:
|
||
raise
|
||
except Exception as e:
|
||
logger.exception(f"Failed to get skill: {e}")
|
||
raise HTTPException(
|
||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||
detail=f"Failed to get skill: {str(e)}"
|
||
)
|
||
|
||
|
||
@router.post("/{name}/toggle", response_model=ToggleSkillResponse)
|
||
async def toggle_skill(name: str, request: ToggleSkillRequest, req: Request) -> ToggleSkillResponse:
|
||
"""
|
||
切换技能启用状态(持久化到配置)
|
||
|
||
Args:
|
||
name: 技能名称
|
||
request: 切换技能请求
|
||
|
||
Returns:
|
||
ToggleSkillResponse: 切换结果
|
||
"""
|
||
try:
|
||
skills_loader = get_skills_loader(req)
|
||
|
||
# 检查技能是否存在
|
||
skills_list = skills_loader.list_skills()
|
||
skill_exists = any(s["name"] == name for s in skills_list)
|
||
|
||
if not skill_exists:
|
||
raise HTTPException(
|
||
status_code=status.HTTP_404_NOT_FOUND,
|
||
detail=f"Skill '{name}' not found"
|
||
)
|
||
|
||
# 切换技能状态
|
||
success = skills_loader.toggle_skill(name, request.enabled)
|
||
|
||
if not success:
|
||
raise HTTPException(
|
||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||
detail=f"Failed to toggle skill '{name}'"
|
||
)
|
||
|
||
# 持久化到配置文件
|
||
config = config_loader.config
|
||
workspace = Path(config.workspace.path) if config.workspace.path else WORKSPACE_DIR
|
||
workspace = workspace.resolve() # 确保是绝对路径
|
||
config_file = workspace / ".skills_config.json"
|
||
|
||
logger.info(f"Saving skills config to: {config_file}")
|
||
|
||
# 读取现有配置
|
||
skills_config = {}
|
||
if config_file.exists():
|
||
try:
|
||
skills_config = json.loads(config_file.read_text(encoding="utf-8"))
|
||
logger.debug(f"Loaded existing config: {skills_config}")
|
||
except Exception as e:
|
||
logger.warning(f"Failed to load existing config: {e}")
|
||
|
||
# 更新配置
|
||
if "disabled_skills" not in skills_config:
|
||
skills_config["disabled_skills"] = []
|
||
|
||
if request.enabled:
|
||
# 启用:从禁用列表中移除
|
||
if name in skills_config["disabled_skills"]:
|
||
skills_config["disabled_skills"].remove(name)
|
||
logger.info(f"Removed '{name}' from disabled list")
|
||
else:
|
||
# 禁用:添加到禁用列表
|
||
if name not in skills_config["disabled_skills"]:
|
||
skills_config["disabled_skills"].append(name)
|
||
logger.info(f"Added '{name}' to disabled list")
|
||
|
||
# 保存配置
|
||
config_file.write_text(json.dumps(skills_config, indent=2, ensure_ascii=False), encoding="utf-8")
|
||
|
||
status_text = "enabled" if request.enabled else "disabled"
|
||
return ToggleSkillResponse(
|
||
success=True,
|
||
message=f"Skill '{name}' {status_text} successfully",
|
||
)
|
||
|
||
except HTTPException:
|
||
raise
|
||
except Exception as e:
|
||
logger.exception(f"Failed to toggle skill: {e}")
|
||
raise HTTPException(
|
||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||
detail=f"Failed to toggle skill: {str(e)}"
|
||
)
|
||
|
||
|
||
@router.post("", response_model=SkillDetail, status_code=status.HTTP_201_CREATED)
|
||
async def create_skill(request: CreateSkillRequest, req: Request) -> SkillDetail:
|
||
"""
|
||
创建新技能
|
||
|
||
Args:
|
||
request: 创建技能请求
|
||
|
||
Returns:
|
||
SkillDetail: 创建的技能详情
|
||
"""
|
||
try:
|
||
skills_loader = get_skills_loader(req)
|
||
|
||
# 检查技能是否已存在
|
||
if skills_loader.get_skill(request.name):
|
||
raise HTTPException(
|
||
status_code=status.HTTP_409_CONFLICT,
|
||
detail=f"Skill '{request.name}' already exists"
|
||
)
|
||
|
||
# 构建技能内容(包含 frontmatter)
|
||
metadata = {
|
||
"CountBot": {
|
||
"always": request.auto_load,
|
||
"requires": {
|
||
"bins": request.requirements
|
||
}
|
||
}
|
||
}
|
||
|
||
frontmatter = f"""---
|
||
name: {request.name}
|
||
description: {request.description}
|
||
metadata: {json.dumps(metadata)}
|
||
---
|
||
|
||
"""
|
||
full_content = frontmatter + request.content
|
||
|
||
# 创建技能
|
||
success = skills_loader.add_skill(request.name, full_content)
|
||
|
||
if not success:
|
||
raise HTTPException(
|
||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||
detail=f"Failed to create skill '{request.name}'"
|
||
)
|
||
|
||
# 返回创建的技能
|
||
return await get_skill(request.name, req)
|
||
|
||
except HTTPException:
|
||
raise
|
||
except Exception as e:
|
||
logger.exception(f"Failed to create skill: {e}")
|
||
raise HTTPException(
|
||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||
detail=f"Failed to create skill: {str(e)}"
|
||
)
|
||
|
||
|
||
@router.put("/{name}", response_model=SkillDetail)
|
||
async def update_skill(name: str, request: UpdateSkillRequest, req: Request) -> SkillDetail:
|
||
"""
|
||
更新技能
|
||
|
||
Args:
|
||
name: 技能名称
|
||
request: 更新技能请求
|
||
|
||
Returns:
|
||
SkillDetail: 更新后的技能详情
|
||
"""
|
||
try:
|
||
skills_loader = get_skills_loader(req)
|
||
|
||
# 检查技能是否存在
|
||
skill = skills_loader.get_skill(name)
|
||
if not skill:
|
||
raise HTTPException(
|
||
status_code=status.HTTP_404_NOT_FOUND,
|
||
detail=f"Skill '{name}' not found"
|
||
)
|
||
|
||
ensure_workspace_skill(skill, "update")
|
||
|
||
# 构建技能内容(包含 frontmatter)
|
||
metadata = {
|
||
"CountBot": {
|
||
"always": request.auto_load,
|
||
"requires": {
|
||
"bins": request.requirements
|
||
}
|
||
}
|
||
}
|
||
|
||
frontmatter = f"""---
|
||
name: {name}
|
||
description: {request.description}
|
||
metadata: {json.dumps(metadata)}
|
||
---
|
||
|
||
"""
|
||
full_content = frontmatter + request.content
|
||
|
||
# 更新技能
|
||
success = skills_loader.update_skill(name, full_content)
|
||
|
||
if not success:
|
||
raise HTTPException(
|
||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||
detail=f"Failed to update skill '{name}'"
|
||
)
|
||
|
||
# 返回更新后的技能
|
||
return await get_skill(name, req)
|
||
|
||
except HTTPException:
|
||
raise
|
||
except Exception as e:
|
||
logger.exception(f"Failed to update skill: {e}")
|
||
raise HTTPException(
|
||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||
detail=f"Failed to update skill: {str(e)}"
|
||
)
|
||
|
||
|
||
@router.delete("/{name}", response_model=DeleteSkillResponse)
|
||
async def delete_skill(name: str, req: Request) -> DeleteSkillResponse:
|
||
"""
|
||
删除技能
|
||
|
||
Args:
|
||
name: 技能名称
|
||
|
||
Returns:
|
||
DeleteSkillResponse: 删除结果
|
||
"""
|
||
try:
|
||
skills_loader = get_skills_loader(req)
|
||
|
||
# 检查技能是否存在
|
||
skill = skills_loader.get_skill(name)
|
||
if not skill:
|
||
raise HTTPException(
|
||
status_code=status.HTTP_404_NOT_FOUND,
|
||
detail=f"Skill '{name}' not found"
|
||
)
|
||
|
||
ensure_workspace_skill(skill, "delete")
|
||
|
||
# 删除技能
|
||
success = skills_loader.delete_skill(name)
|
||
|
||
if not success:
|
||
raise HTTPException(
|
||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||
detail=f"Failed to delete skill '{name}'"
|
||
)
|
||
|
||
return DeleteSkillResponse(
|
||
success=True,
|
||
message=f"Skill '{name}' deleted successfully"
|
||
)
|
||
|
||
except HTTPException:
|
||
raise
|
||
except Exception as e:
|
||
logger.exception(f"Failed to delete skill: {e}")
|
||
raise HTTPException(
|
||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||
detail=f"Failed to delete skill: {str(e)}"
|
||
)
|
||
|
||
|
||
# ============================================================================
|
||
# Skills Config Endpoints
|
||
# ============================================================================
|
||
|
||
|
||
@router.get("/{name}/config/schema", response_model=SkillConfigSchemaResponse)
|
||
async def get_skill_config_schema(name: str) -> SkillConfigSchemaResponse:
|
||
"""
|
||
获取技能配置Schema
|
||
|
||
Args:
|
||
name: 技能名称
|
||
|
||
Returns:
|
||
SkillConfigSchemaResponse: Schema信息
|
||
"""
|
||
try:
|
||
schema_loader = get_skill_schema_loader()
|
||
schema = schema_loader.load_schema(name)
|
||
|
||
if not schema:
|
||
return SkillConfigSchemaResponse(
|
||
has_schema=False,
|
||
schema=None
|
||
)
|
||
|
||
return SkillConfigSchemaResponse(
|
||
has_schema=True,
|
||
schema=schema
|
||
)
|
||
|
||
except Exception as e:
|
||
logger.exception(f"Failed to get config schema: {e}")
|
||
raise HTTPException(
|
||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||
detail=f"Failed to get config schema: {str(e)}"
|
||
)
|
||
|
||
|
||
@router.get("/{name}/config", response_model=SkillConfigResponse)
|
||
async def get_skill_config(name: str) -> SkillConfigResponse:
|
||
"""
|
||
获取技能配置
|
||
|
||
Args:
|
||
name: 技能名称
|
||
|
||
Returns:
|
||
SkillConfigResponse: 配置信息
|
||
"""
|
||
try:
|
||
config_manager = get_skill_config_manager()
|
||
|
||
# 检查是否有配置文件
|
||
if not config_manager.has_config(name):
|
||
return SkillConfigResponse(
|
||
has_config=False,
|
||
config=None,
|
||
status="not_configured",
|
||
errors=[]
|
||
)
|
||
|
||
# 加载配置
|
||
config = config_manager.load_config(name)
|
||
if config is None:
|
||
return SkillConfigResponse(
|
||
has_config=True,
|
||
config=None,
|
||
status="invalid_format",
|
||
errors=["配置文件格式错误"]
|
||
)
|
||
|
||
# 检查配置状态
|
||
status_code, errors = config_manager.get_config_status(name)
|
||
|
||
return SkillConfigResponse(
|
||
has_config=True,
|
||
config=config,
|
||
status=status_code,
|
||
errors=errors
|
||
)
|
||
|
||
except Exception as e:
|
||
logger.exception(f"Failed to get skill config: {e}")
|
||
raise HTTPException(
|
||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||
detail=f"Failed to get skill config: {str(e)}"
|
||
)
|
||
|
||
|
||
@router.put("/{name}/config", response_model=SkillConfigResponse)
|
||
async def update_skill_config(
|
||
name: str,
|
||
request: UpdateSkillConfigRequest
|
||
) -> SkillConfigResponse:
|
||
"""
|
||
更新技能配置
|
||
|
||
Args:
|
||
name: 技能名称
|
||
request: 更新配置请求
|
||
|
||
Returns:
|
||
SkillConfigResponse: 更新后的配置信息
|
||
"""
|
||
try:
|
||
config_manager = get_skill_config_manager()
|
||
schema_loader = get_skill_schema_loader()
|
||
|
||
# 检查配置文件是否存在
|
||
if not config_manager.has_config(name):
|
||
raise HTTPException(
|
||
status_code=status.HTTP_404_NOT_FOUND,
|
||
detail=f"Config file not found for skill '{name}'"
|
||
)
|
||
|
||
# 验证配置
|
||
is_valid, errors = schema_loader.validate_config(name, request.config)
|
||
if not is_valid:
|
||
raise HTTPException(
|
||
status_code=status.HTTP_400_BAD_REQUEST,
|
||
detail=f"Invalid config: {', '.join(errors)}"
|
||
)
|
||
|
||
# 保存配置
|
||
success = config_manager.save_config(name, request.config)
|
||
|
||
if not success:
|
||
raise HTTPException(
|
||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||
detail=f"Failed to save config for skill '{name}'"
|
||
)
|
||
|
||
logger.info(f"Updated config for skill: {name}")
|
||
|
||
# 返回更新后的配置
|
||
return await get_skill_config(name)
|
||
|
||
except HTTPException:
|
||
raise
|
||
except Exception as e:
|
||
logger.exception(f"Failed to update skill config: {e}")
|
||
raise HTTPException(
|
||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||
detail=f"Failed to update skill config: {str(e)}"
|
||
)
|
||
|
||
|
||
@router.get("/{name}/config/status", response_model=SkillConfigStatusResponse)
|
||
async def get_skill_config_status(name: str) -> SkillConfigStatusResponse:
|
||
"""
|
||
获取技能配置状态
|
||
|
||
Args:
|
||
name: 技能名称
|
||
|
||
Returns:
|
||
SkillConfigStatusResponse: 配置状态
|
||
"""
|
||
try:
|
||
config_manager = get_skill_config_manager()
|
||
schema_loader = get_skill_schema_loader()
|
||
|
||
# 检查Schema
|
||
if not schema_loader.has_schema(name):
|
||
return SkillConfigStatusResponse(
|
||
status="no_schema",
|
||
message="该技能不支持可视化配置编辑",
|
||
errors=[]
|
||
)
|
||
|
||
# 检查配置文件
|
||
if not config_manager.has_config(name):
|
||
return SkillConfigStatusResponse(
|
||
status="not_configured",
|
||
message="配置文件不存在",
|
||
errors=[]
|
||
)
|
||
|
||
# 获取配置状态
|
||
status_code, errors = config_manager.get_config_status(name)
|
||
|
||
status_messages = {
|
||
"valid": "配置正常",
|
||
"invalid_format": "配置文件格式错误",
|
||
"missing_fields": "配置不完整",
|
||
}
|
||
|
||
return SkillConfigStatusResponse(
|
||
status=status_code,
|
||
message=status_messages.get(status_code, "未知状态"),
|
||
errors=errors
|
||
)
|
||
|
||
except Exception as e:
|
||
logger.exception(f"Failed to check config status: {e}")
|
||
raise HTTPException(
|
||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||
detail=f"Failed to check config status: {str(e)}"
|
||
)
|
||
|
||
|
||
@router.post("/{name}/config/fix", response_model=FixSkillConfigResponse)
|
||
async def fix_skill_config(name: str) -> FixSkillConfigResponse:
|
||
"""
|
||
自动修复技能配置
|
||
|
||
Args:
|
||
name: 技能名称
|
||
|
||
Returns:
|
||
FixSkillConfigResponse: 修复结果
|
||
"""
|
||
try:
|
||
config_manager = get_skill_config_manager()
|
||
schema_loader = get_skill_schema_loader()
|
||
|
||
# 检查Schema
|
||
if not schema_loader.has_schema(name):
|
||
raise HTTPException(
|
||
status_code=status.HTTP_404_NOT_FOUND,
|
||
detail="Schema not found"
|
||
)
|
||
|
||
# 检查配置文件
|
||
if not config_manager.has_config(name):
|
||
raise HTTPException(
|
||
status_code=status.HTTP_404_NOT_FOUND,
|
||
detail="Config file not found"
|
||
)
|
||
|
||
# 执行修复
|
||
success = config_manager.auto_fix_config(name)
|
||
|
||
if not success:
|
||
raise HTTPException(
|
||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||
detail="Failed to fix config"
|
||
)
|
||
|
||
logger.info(f"Fixed config for skill: {name}")
|
||
|
||
return FixSkillConfigResponse(
|
||
success=True,
|
||
message="配置已自动修复"
|
||
)
|
||
|
||
except HTTPException:
|
||
raise
|
||
except Exception as e:
|
||
logger.exception(f"Failed to fix config: {e}")
|
||
raise HTTPException(
|
||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||
detail=f"Failed to fix config: {str(e)}"
|
||
)
|
||
|
||
|
||
@router.get("/{name}/config/help", response_model=SkillConfigHelpResponse)
|
||
async def get_skill_config_help(name: str) -> SkillConfigHelpResponse:
|
||
"""
|
||
获取技能配置帮助文档
|
||
|
||
Args:
|
||
name: 技能名称
|
||
|
||
Returns:
|
||
SkillConfigHelpResponse: 帮助文档
|
||
"""
|
||
try:
|
||
config_manager = get_skill_config_manager()
|
||
|
||
content = config_manager.get_help_content(name)
|
||
|
||
if not content:
|
||
return SkillConfigHelpResponse(
|
||
has_help=False,
|
||
content=None
|
||
)
|
||
|
||
return SkillConfigHelpResponse(
|
||
has_help=True,
|
||
content=content
|
||
)
|
||
|
||
except Exception as e:
|
||
logger.exception(f"Failed to get config help: {e}")
|
||
raise HTTPException(
|
||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||
detail=f"Failed to get config help: {str(e)}"
|
||
)
|
||
|
||
|
||
# ============================================================================
|
||
# Skills Reload Endpoint
|
||
# ============================================================================
|
||
|
||
|
||
@router.post("/reload", response_model=ReloadSkillsResponse)
|
||
async def reload_skills(req: Request) -> ReloadSkillsResponse:
|
||
"""
|
||
重新加载所有技能(热重载,无需重启应用)
|
||
|
||
适用场景:
|
||
- 手动添加新技能到 skills 目录后
|
||
- 修改现有技能文件后
|
||
- 需要刷新技能列表时
|
||
|
||
Returns:
|
||
ReloadSkillsResponse: 重载结果
|
||
"""
|
||
try:
|
||
# 获取全局 skills 实例
|
||
skills_loader = get_skills_loader(req)
|
||
|
||
# 执行重载
|
||
skills_loader.reload()
|
||
|
||
# 获取统计信息
|
||
stats = skills_loader.get_stats()
|
||
|
||
logger.info(f"Skills reloaded successfully: {stats['total']} total, {stats['enabled']} enabled")
|
||
|
||
return ReloadSkillsResponse(
|
||
success=True,
|
||
message=f"Successfully reloaded {stats['total']} skills",
|
||
total=stats['total']
|
||
)
|
||
|
||
except Exception as e:
|
||
logger.exception(f"Failed to reload skills: {e}")
|
||
raise HTTPException(
|
||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||
detail=f"Failed to reload skills: {str(e)}"
|
||
)
|