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) { 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{ 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}) } }