"""Skill 能力包服务(CRUD + 关键词触发匹配)。 CRUD 记审计;``match_active_skills`` 加载 enabled skills 调 matcher,返回 Skill ORM 列表(供 send_message 取 prompt_fragment 注入 contract)。 """ from app.core.errors import AppError from app.db.session import AsyncSessionLocal from app.models.skills import Skill from app.modules.skills.matcher import match_skills from app.repositories import skills as skills_repo from app.repositories.json_utils import dumps, loads_list from app.schemas.skills import SkillResponse, ToolCapability from app.services.audit import record_audit_event async def create_skill( *, name: str, description: str = "", trigger: str = "", prompt_fragment: str = "", tool_bindings: list[str] | None = None, risk_level: str = "L1", status: str = "enabled", ) -> SkillResponse: async with AsyncSessionLocal() as session: existing = await skills_repo.get_skill_by_name(session, name) if existing is not None: raise AppError("conflict", f"Skill 名「{name}」已存在。") skill = Skill( name=name, description=description, trigger=trigger, prompt_fragment=prompt_fragment, tool_bindings_json=dumps(tool_bindings or []), risk_level=risk_level, status=status, ) await skills_repo.add_skill(session, skill) await record_audit_event( session, action="skill.created", actor="user", input_summary=name[:200], resource_type="skill", resource_id=skill.id, ) await session.commit() await session.refresh(skill) return _to_response(skill) async def get_skill(skill_id: str) -> SkillResponse | None: async with AsyncSessionLocal() as session: skill = await skills_repo.get_skill(session, skill_id) return _to_response(skill) if skill else None async def list_skills(*, status: str | None = None) -> list[SkillResponse]: async with AsyncSessionLocal() as session: skills = await skills_repo.list_skills(session, status=status) return [_to_response(s) for s in skills] async def list_all_capabilities(runtime) -> list[ToolCapability]: """统一列举 builtin + mcp(runtime.list_specs)+ skill(DB),含来源标记。""" capabilities: list[ToolCapability] = [] if runtime is not None: for spec in runtime.list_specs(): source = "mcp" if spec.name.startswith("mcp:") else "builtin" capabilities.append( ToolCapability( name=spec.name, description=spec.description, source=source, risk_level=spec.risk_level, status="enabled", ) ) for s in await list_skills(status="enabled"): capabilities.append( ToolCapability( name=s.name, description=s.description, source="skill", risk_level=s.risk_level, status=s.status, ) ) return capabilities async def update_skill( skill_id: str, *, description: str | None = None, trigger: str | None = None, prompt_fragment: str | None = None, tool_bindings: list[str] | None = None, risk_level: str | None = None, status: str | None = None, ) -> SkillResponse: async with AsyncSessionLocal() as session: skill = await skills_repo.get_skill(session, skill_id) if skill is None: raise AppError("not_found", "Skill 不存在。") if description is not None: skill.description = description if trigger is not None: skill.trigger = trigger if prompt_fragment is not None: skill.prompt_fragment = prompt_fragment if tool_bindings is not None: skill.tool_bindings_json = dumps(tool_bindings) if risk_level is not None: skill.risk_level = risk_level if status is not None: skill.status = status await record_audit_event( session, action="skill.updated", actor="user", input_summary=skill.name[:200], resource_type="skill", resource_id=skill.id, ) await session.commit() await session.refresh(skill) return _to_response(skill) async def delete_skill(skill_id: str) -> None: async with AsyncSessionLocal() as session: skill = await skills_repo.get_skill(session, skill_id) if skill is None: raise AppError("not_found", "Skill 不存在。") name = skill.name await session.delete(skill) await record_audit_event( session, action="skill.deleted", actor="user", input_summary=name[:200], resource_type="skill", resource_id=skill_id, ) await session.commit() async def match_active_skills(context_text: str) -> list[Skill]: """加载 enabled skills,关键词匹配;返回命中的 Skill ORM(供注入 prompt_fragment)。""" async with AsyncSessionLocal() as session: skills = await skills_repo.list_skills(session, status="enabled") return match_skills(context_text, skills) async def get_enabled_skills_by_names(names: tuple[str, ...]) -> list[Skill]: """按名称返回 enabled Skills,供 Agent 显式引用。""" wanted = set(names) if not wanted: return [] async with AsyncSessionLocal() as session: skills = await skills_repo.list_skills(session, status="enabled") return [skill for skill in skills if skill.name in wanted] def _to_response(skill: Skill) -> SkillResponse: return SkillResponse( id=skill.id, name=skill.name, description=skill.description, trigger=skill.trigger, prompt_fragment=skill.prompt_fragment, tool_bindings=loads_list(skill.tool_bindings_json), risk_level=skill.risk_level, status=skill.status, created_at=skill.created_at, updated_at=skill.updated_at, )