Files
ttstd-infinite-canvas/backend/internal/handler/skills.go

402 lines
10 KiB
Go

package handler
import (
"errors"
"mime"
"net/http"
"path/filepath"
"strconv"
"strings"
"infinite-canvas/backend/internal/model"
"infinite-canvas/backend/internal/service"
"github.com/gin-gonic/gin"
)
func RegisterSkillRoutes(r *gin.RouterGroup, svc *service.Service) {
registerSkillCurationRoutes(r, svc)
r.POST("/skills/install", func(c *gin.Context) {
user, err := currentUser(c, svc)
if err != nil {
failService(c, err)
return
}
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, service.SkillPackageUploadMaxBytes)
file, err := c.FormFile("file")
if err != nil {
failService(c, service.BadAuthRequest("请选择 Markdown 或 ZIP 技能文件"))
return
}
isPrivate, err := parseOptionalBool(c.PostForm("isPrivate"))
if err != nil {
failService(c, service.BadAuthRequest("isPrivate 必须是布尔值"))
return
}
sourceType := strings.ToLower(strings.TrimSpace(c.PostForm("sourceType")))
if sourceType == "" {
switch strings.ToLower(filepath.Ext(file.Filename)) {
case ".md", ".markdown":
sourceType = "markdown"
case ".zip":
sourceType = "zip"
}
}
skill, err := svc.InstallSkillUpload(user.ID, sourceType, file, service.SkillInstallRequest{
Name: c.PostForm("name"), Description: c.PostForm("description"), Tag: c.PostForm("tag"), IsPrivate: isPrivate,
})
if err != nil {
failService(c, err)
return
}
ok(c, gin.H{"skill": skill})
})
r.POST("/skills/install/github", func(c *gin.Context) {
user, err := currentUser(c, svc)
if err != nil {
failService(c, err)
return
}
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, 32<<10)
var req service.SkillGitHubInstallRequest
if err := c.ShouldBindJSON(&req); err != nil {
failService(c, service.BadAuthRequest("GitHub 技能数据格式无效"))
return
}
skill, err := svc.InstallGitHubSkill(user.ID, req)
if err != nil {
failService(c, err)
return
}
ok(c, gin.H{"skill": skill})
})
r.GET("/skills", func(c *gin.Context) {
user, err := currentUser(c, svc)
if err != nil {
failService(c, err)
return
}
page, pageSize, err := parsePaginationQuery(c, 20)
if err != nil {
fail(c, http.StatusBadRequest, err)
return
}
libraryUncategorized, err := parseOptionalBool(c.Query("libraryUncategorized"))
if err != nil {
failService(c, service.BadAuthRequest("libraryUncategorized 必须是布尔值"))
return
}
result, err := svc.Skills(user.ID, service.SkillListRequest{
PlatformRootID: c.Query("platformRootId"),
PlatformCategoryID: c.Query("platformCategoryId"), PlatformUncategorized: c.Query("platformUncategorized") == "true",
Page: page, PageSize: pageSize, Scope: c.DefaultQuery("scope", "public"),
Search: c.Query("search"), Tag: c.Query("tag"), Sort: c.DefaultQuery("sort", "popular"),
LibraryCategoryID: c.Query("libraryCategoryId"), LibraryUncategorized: libraryUncategorized,
})
if err != nil {
failService(c, err)
return
}
ok(c, result)
})
r.GET("/skills/library-categories", func(c *gin.Context) {
user, err := currentUser(c, svc)
if err != nil {
failService(c, err)
return
}
result, err := svc.SkillLibraryCategories(user.ID, c.DefaultQuery("scope", "mine"))
if err != nil {
failService(c, err)
return
}
ok(c, result)
})
r.POST("/skills/library-categories", func(c *gin.Context) {
user, err := currentUser(c, svc)
if err != nil {
failService(c, err)
return
}
var req service.SkillLibraryCategoryMutationRequest
if err := c.ShouldBindJSON(&req); err != nil {
failService(c, service.BadAuthRequest("技能库分类数据格式无效"))
return
}
category, err := svc.CreateSkillLibraryCategory(user, req)
if err != nil {
failService(c, err)
return
}
ok(c, gin.H{"category": category})
})
r.DELETE("/skills/library-categories/:id", func(c *gin.Context) {
user, err := currentUser(c, svc)
if err != nil {
failService(c, err)
return
}
if err := svc.DeleteSkillLibraryCategory(user, c.Param("id")); err != nil {
failService(c, err)
return
}
ok(c, gin.H{"deleted": true})
})
r.GET("/skills/presets", func(c *gin.Context) {
// 场景预设目录:公开只读的市场元数据(与 /openapi.yaml 同模式,无用户数据)。
presets, err := svc.SkillPresets()
if err != nil {
failService(c, err)
return
}
ok(c, gin.H{"presets": presets})
})
r.GET("/skills/added", func(c *gin.Context) {
user, err := currentUser(c, svc)
if err != nil {
failService(c, err)
return
}
skills, err := svc.AddedSkills(user.ID)
if err != nil {
failService(c, err)
return
}
ok(c, gin.H{"skills": skills})
})
r.GET("/skills/:id", func(c *gin.Context) {
user, err := currentUser(c, svc)
if err != nil {
failService(c, err)
return
}
skill, err := svc.SkillDetail(user.ID, c.Param("id"))
if err != nil {
failService(c, err)
return
}
ok(c, gin.H{"skill": skill})
})
r.PATCH("/skills/:id/library-category", func(c *gin.Context) {
user, err := currentUser(c, svc)
if err != nil {
failService(c, err)
return
}
var req service.SkillLibraryCategoryAssignmentRequest
if err := c.ShouldBindJSON(&req); err != nil {
failService(c, service.BadAuthRequest("技能库分类数据格式无效"))
return
}
skill, err := svc.SetSkillLibraryCategory(user.ID, c.Param("id"), req.CategoryID)
if err != nil {
failService(c, err)
return
}
ok(c, gin.H{"skill": skill})
})
r.GET("/skills/:id/files", func(c *gin.Context) {
user, err := currentUser(c, svc)
if err != nil {
failService(c, err)
return
}
files, err := svc.SkillPackageFiles(user.ID, c.Param("id"))
if err != nil {
failService(c, err)
return
}
ok(c, gin.H{"files": files})
})
r.GET("/skills/:id/file", func(c *gin.Context) {
user, err := currentUser(c, svc)
if err != nil {
failService(c, err)
return
}
file, err := svc.SkillPackageFile(user.ID, c.Param("id"), c.Query("path"))
if err != nil {
failService(c, err)
return
}
ok(c, gin.H{"file": file})
})
r.GET("/skills/:id/file/raw", func(c *gin.Context) {
user, err := currentUser(c, svc)
if err != nil {
failService(c, err)
return
}
data, mimeType, fileName, err := svc.SkillPackageRawFile(user.ID, c.Param("id"), c.Query("path"))
if err != nil {
failService(c, err)
return
}
c.Header("Cache-Control", "private, no-store")
c.Header("X-Content-Type-Options", "nosniff")
c.Header("Content-Security-Policy", "default-src 'none'; sandbox")
c.Header("Content-Disposition", mime.FormatMediaType("inline", map[string]string{"filename": fileName}))
c.Data(http.StatusOK, mimeType, data)
})
r.GET("/skills/:id/bundle", func(c *gin.Context) {
user, err := currentUser(c, svc)
if err != nil {
failService(c, err)
return
}
bundle, err := svc.SkillPackageBundle(user.ID, c.Param("id"))
if err != nil {
failService(c, err)
return
}
c.Header("Cache-Control", "private, no-store")
ok(c, gin.H{"bundle": bundle})
})
r.GET("/skills/:id/search", func(c *gin.Context) {
user, err := currentUser(c, svc)
if err != nil {
failService(c, err)
return
}
results, err := svc.SearchSkillPackage(user.ID, c.Param("id"), c.Query("q"))
if err != nil {
failService(c, err)
return
}
ok(c, gin.H{"results": results})
})
r.POST("/skills/:id/sync", func(c *gin.Context) {
user, err := currentUser(c, svc)
if err != nil {
failService(c, err)
return
}
skill, err := svc.SyncGitHubSkill(user.ID, c.Param("id"))
if err != nil {
failService(c, err)
return
}
ok(c, gin.H{"skill": skill})
})
r.POST("/skills", func(c *gin.Context) {
user, err := currentUser(c, svc)
if err != nil {
failService(c, err)
return
}
var req service.SkillMutationRequest
if err := c.ShouldBindJSON(&req); err != nil {
failService(c, service.BadAuthRequest("技能数据格式无效"))
return
}
skill, err := svc.CreateSkill(user.ID, req)
if err != nil {
failService(c, err)
return
}
ok(c, gin.H{"skill": skill})
})
r.PUT("/skills/:id", func(c *gin.Context) {
user, err := currentUser(c, svc)
if err != nil {
failService(c, err)
return
}
var req service.SkillMutationRequest
if err := c.ShouldBindJSON(&req); err != nil {
failService(c, service.BadAuthRequest("技能数据格式无效"))
return
}
skill, err := svc.UpdateSkill(user.ID, c.Param("id"), req)
if err != nil {
failService(c, err)
return
}
ok(c, gin.H{"skill": skill})
})
r.DELETE("/skills/:id", func(c *gin.Context) {
user, err := currentUser(c, svc)
if err != nil {
failService(c, err)
return
}
if user.Role == model.UserRoleAdmin {
if err := svc.DeleteBuiltinSkill(user, c.Param("id")); err == nil {
ok(c, gin.H{"deleted": true})
return
} else if !errors.Is(err, service.ErrNotBuiltinSkill) {
failService(c, err)
return
}
}
if err := svc.DeleteSkill(user.ID, c.Param("id")); err != nil {
failService(c, err)
return
}
ok(c, gin.H{"deleted": true})
})
r.POST("/skills/:id/add", setSkillAdded(svc, true))
r.DELETE("/skills/:id/add", setSkillAdded(svc, false))
r.POST("/skills/:id/like", setSkillLiked(svc, true))
r.DELETE("/skills/:id/like", setSkillLiked(svc, false))
}
func parseOptionalBool(value string) (bool, error) {
value = strings.TrimSpace(value)
if value == "" {
return false, nil
}
return strconv.ParseBool(value)
}
func setSkillAdded(svc *service.Service, added bool) gin.HandlerFunc {
return func(c *gin.Context) {
user, err := currentUser(c, svc)
if err != nil {
failService(c, err)
return
}
skill, err := svc.SetSkillAdded(user.ID, c.Param("id"), added)
if err != nil {
failService(c, err)
return
}
ok(c, gin.H{"skill": skill})
}
}
func setSkillLiked(svc *service.Service, liked bool) gin.HandlerFunc {
return func(c *gin.Context) {
user, err := currentUser(c, svc)
if err != nil {
failService(c, err)
return
}
skill, err := svc.SetSkillLiked(user.ID, c.Param("id"), liked)
if err != nil {
failService(c, err)
return
}
ok(c, gin.H{"skill": skill})
}
}