"""技能商店用户端路由. 注: prefix 不带 /api/v1, create_app include 时叠 settings.API_V1_PREFIX. """ from __future__ import annotations from typing import Annotated from fastapi import APIRouter, Depends, HTTPException, Query, Request from sqlalchemy.ext.asyncio import AsyncSession from luffy_agent.core.db import get_db, get_redis from luffy_agent.sandbox.deps import get_sandbox_registry from luffy_agent.skill_hub import service from luffy_agent.skill_hub.models import HubSkillStatus from luffy_agent.skill_hub.rate_limit import check_mutation_rate from luffy_agent.skill_hub.repositories import hub_skill_repository from luffy_agent.skill_hub.schemas import ( AppSkillItem, HubSkillDetail, HubSkillListItem, ) from luffy_agent.user.auth.deps import get_current_user from luffy_agent.user.models import User skill_hub_router = APIRouter(prefix="/skill-hub", tags=["skill-hub"]) @skill_hub_router.get("/skills", response_model=list[HubSkillListItem]) async def list_skills( db: Annotated[AsyncSession, Depends(get_db)], _: Annotated[User, Depends(get_current_user)], query: Annotated[str | None, Query(max_length=80)] = None, tag: Annotated[str | None, Query(max_length=40)] = None, skip: Annotated[int, Query(ge=0)] = 0, limit: Annotated[int, Query(ge=1, le=100)] = 50, ) -> list[HubSkillListItem]: rows = await hub_skill_repository.list_published( db, q=query, tag=tag, skip=skip, limit=limit ) return [HubSkillListItem.model_validate(row) for row in rows] @skill_hub_router.get("/skills/{slug}", response_model=HubSkillDetail) async def get_skill( slug: str, db: Annotated[AsyncSession, Depends(get_db)], _: Annotated[User, Depends(get_current_user)], ) -> HubSkillDetail: skill = await hub_skill_repository.get_by_slug(db, slug) if skill is None or skill.status != HubSkillStatus.PUBLISHED: raise HTTPException(status_code=404, detail=f"技能不存在: {slug}") return HubSkillDetail.model_validate(skill) @skill_hub_router.get("/apps/{app_id}/skills", response_model=list[AppSkillItem]) async def list_installed( app_id: str, db: Annotated[AsyncSession, Depends(get_db)], current_user: Annotated[User, Depends(get_current_user)], ) -> list[AppSkillItem]: rows = await service.list_app_skills(db, user_id=current_user.id, app_id=app_id) return [ AppSkillItem( slug=skill.slug, name=skill.name, description=skill.description, version=record.version, installed_at=record.installed_at, ) for record, skill in rows ] @skill_hub_router.post("/apps/{app_id}/skills/{slug}", status_code=204) async def install( app_id: str, slug: str, request: Request, db: Annotated[AsyncSession, Depends(get_db)], current_user: Annotated[User, Depends(get_current_user)], ) -> None: await check_mutation_rate(get_redis(), user_id=current_user.id) registry = get_sandbox_registry(request) await service.install_skill( db, user_id=current_user.id, app_id=app_id, slug=slug, sandbox_registry=registry, ) @skill_hub_router.delete("/apps/{app_id}/skills/{slug}", status_code=204) async def uninstall( app_id: str, slug: str, request: Request, db: Annotated[AsyncSession, Depends(get_db)], current_user: Annotated[User, Depends(get_current_user)], ) -> None: await check_mutation_rate(get_redis(), user_id=current_user.id) registry = get_sandbox_registry(request) await service.uninstall_skill( db, user_id=current_user.id, app_id=app_id, slug=slug, sandbox_registry=registry, )