Files
scriptforge/backend/internal/handler/script.go
zhilv e6e4357a28 feat: 脚本创作+发布+市场体系
- 数据模型新增: title(必填), description(可选), status(draft/published)
- 新增 API: POST /scripts/:id/publish, GET /api/market (搜索+分页+runtime过滤)
- 前端首页重构: 选语言 → CodeMirror 编辑器(8种语言语法高亮) → 标题/描述 → 草稿/发布
- 新增 /market 页面: 浏览已发布脚本, 搜索+过滤+分页
- 详情页新增: 发布按钮(草稿→市场), title/description 展示
- Shell 类运行时显示 source 命令(继承环境变量)
- backend GetSourceCommand 支持 bash/zsh/sh/fish 四种 shell 格式

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 14:04:15 +08:00

187 lines
5.0 KiB
Go

package handler
import (
"errors"
"net/http"
"strconv"
"strings"
"github.com/gin-gonic/gin"
"gitea.kmux.cn/zhilv/scriptforge/internal/service"
)
type createRequest struct {
Title string `json:"title" binding:"required,max=128"`
Description string `json:"description" binding:"max=512"`
Content string `json:"content" binding:"required,max=16384"`
Runtime string `json:"runtime" binding:"required"`
ExpiresIn string `json:"expires_in" binding:"required,oneof=1h 24h 7d 30d"`
Publish bool `json:"publish"`
}
func CreateScript(svc *service.ScriptService) gin.HandlerFunc {
return func(c *gin.Context) {
var req createRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
scheme := "https"
if strings.HasPrefix(c.Request.Host, "localhost") || strings.HasPrefix(c.Request.Host, "127.0.0.1") {
scheme = "http"
}
result, err := svc.Create(service.CreateInput{
Title: req.Title,
Description: req.Description,
Content: req.Content,
Runtime: req.Runtime,
ExpiresIn: req.ExpiresIn,
Publish: req.Publish,
}, scheme, c.Request.Host)
if errors.Is(err, service.ErrInvalidRuntime) {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid runtime"})
return
}
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "internal error"})
return
}
c.JSON(http.StatusCreated, gin.H{
"id": result.Script.ID,
"title": result.Script.Title,
"description": result.Script.Description,
"admin_token": result.AdminToken,
"url": result.RawURL,
"command": result.Command,
"source_command": result.SourceCmd,
"runtime": result.Script.Runtime,
"status": result.Script.Status,
"expires_at": result.Script.ExpiresAt,
})
}
}
func GetScript(svc *service.ScriptService) gin.HandlerFunc {
return func(c *gin.Context) {
id := c.Param("id")
script, err := svc.GetByID(id)
if errors.Is(err, service.ErrNotFound) {
c.JSON(http.StatusNotFound, gin.H{"error": "script not found or expired"})
return
}
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "internal error"})
return
}
c.JSON(http.StatusOK, gin.H{
"id": script.ID,
"title": script.Title,
"description": script.Description,
"runtime": script.Runtime,
"content": script.Content,
"content_length": len(script.Content),
"status": script.Status,
"created_at": script.CreatedAt,
"expires_at": script.ExpiresAt,
"published_at": script.PublishedAt,
"expired": false,
})
}
}
func PublishScript(svc *service.ScriptService) gin.HandlerFunc {
return func(c *gin.Context) {
id := c.Param("id")
token := c.Query("token")
if token == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "token query parameter is required"})
return
}
script, err := svc.Publish(id, token)
if errors.Is(err, service.ErrNotFound) {
c.JSON(http.StatusNotFound, gin.H{"error": "script not found or invalid token"})
return
}
if errors.Is(err, service.ErrAlreadyPublished) {
c.JSON(http.StatusBadRequest, gin.H{"error": "script already published"})
return
}
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "internal error"})
return
}
c.JSON(http.StatusOK, gin.H{
"id": script.ID,
"status": script.Status,
"published_at": script.PublishedAt,
})
}
}
func ListMarket(svc *service.ScriptService) gin.HandlerFunc {
return func(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
perPage, _ := strconv.Atoi(c.DefaultQuery("per_page", "20"))
runtime := c.Query("runtime")
search := c.Query("search")
result, err := svc.ListMarket(service.MarketQuery{
Page: page,
PerPage: perPage,
Runtime: runtime,
Search: search,
})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "internal error"})
return
}
items := make([]gin.H, len(result.Items))
for i, s := range result.Items {
items[i] = gin.H{
"id": s.ID,
"title": s.Title,
"description": s.Description,
"runtime": s.Runtime,
"published_at": s.PublishedAt,
}
}
c.JSON(http.StatusOK, gin.H{
"items": items,
"total": result.Total,
"page": result.Page,
"per_page": result.PerPage,
})
}
}
func DeleteScript(svc *service.ScriptService) gin.HandlerFunc {
return func(c *gin.Context) {
id := c.Param("id")
token := c.Query("token")
if token == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "token query parameter is required"})
return
}
if err := svc.Delete(id, token); errors.Is(err, service.ErrNotFound) {
c.JSON(http.StatusNotFound, gin.H{"error": "script not found or invalid token"})
return
} else if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "internal error"})
return
}
c.Status(http.StatusNoContent)
}
}