"""Skill 插件管理路由""" import json from datetime import datetime, timezone from fastapi import APIRouter, HTTPException from sqlmodel import Session, select, or_ from ..database import engine from ..models import Skill from ..schemas import SkillCreate, SkillUpdate, SkillRead, SkillList, ToolInfo from ..services.tools import TOOL_DEFINITIONS router = APIRouter(prefix="/api/skills", tags=["skills"]) def _skill_to_read(skill: Skill) -> SkillRead: """将 Skill ORM 对象转为 SkillRead,反序列化 allowed_tools""" try: tools = json.loads(skill.allowed_tools) if skill.allowed_tools else [] except (json.JSONDecodeError, TypeError): tools = [] return SkillRead( id=skill.id, # type: ignore name=skill.name, description=skill.description, system_prompt=skill.system_prompt, allowed_tools=tools, is_builtin=skill.is_builtin, novel_id=skill.novel_id, created_at=skill.created_at, updated_at=skill.updated_at, ) @router.get("", response_model=SkillList) def list_skills(novel_id: int | None = None): """列出 Skill,返回全局 + 指定小说的""" with Session(engine) as session: if novel_id is not None: stmt = ( select(Skill) .where(or_(Skill.novel_id.is_(None), Skill.novel_id == novel_id)) # type: ignore .order_by(Skill.is_builtin.desc(), Skill.created_at) # type: ignore ) else: stmt = select(Skill).order_by(Skill.is_builtin.desc(), Skill.created_at) # type: ignore skills = session.exec(stmt).all() items = [_skill_to_read(s) for s in skills] return SkillList(items=items, total=len(items)) @router.post("", response_model=SkillRead, status_code=201) def create_skill(data: SkillCreate): """创建 Skill""" with Session(engine) as session: skill = Skill( name=data.name, description=data.description, system_prompt=data.system_prompt, allowed_tools=json.dumps(data.allowed_tools, ensure_ascii=False), is_builtin=False, novel_id=data.novel_id, ) session.add(skill) session.commit() session.refresh(skill) return _skill_to_read(skill) @router.get("/tools", response_model=list[ToolInfo]) def get_available_tools(): """返回所有可用工具名和描述(供前端工具 checkbox 使用)""" return [ ToolInfo(name=t["name"], description=t["description"]) for t in TOOL_DEFINITIONS ] @router.patch("/{skill_id}", response_model=SkillRead) def update_skill(skill_id: int, data: SkillUpdate): """更新 Skill""" with Session(engine) as session: skill = session.get(Skill, skill_id) if not skill: raise HTTPException(status_code=404, detail="Skill 不存在") if data.name is not None: skill.name = data.name if data.description is not None: skill.description = data.description if data.system_prompt is not None: skill.system_prompt = data.system_prompt if data.allowed_tools is not None: skill.allowed_tools = json.dumps(data.allowed_tools, ensure_ascii=False) skill.updated_at = datetime.now(timezone.utc) session.add(skill) session.commit() session.refresh(skill) return _skill_to_read(skill) @router.delete("/{skill_id}", status_code=204) def delete_skill(skill_id: int): """删除 Skill""" with Session(engine) as session: skill = session.get(Skill, skill_id) if not skill: raise HTTPException(status_code=404, detail="Skill 不存在") session.delete(skill) session.commit()