From 80a285d8da04864962b9740cc6558aaf9f18e958 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 12 Apr 2026 02:36:44 +0000 Subject: [PATCH 1/7] Initial plan From f5a42dd850dcb2455cddf4aaae5053ce0a86984e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 12 Apr 2026 02:43:01 +0000 Subject: [PATCH 2/7] Limit script name (50), description (200), and tags (5) per requirements Agent-Logs-Url: https://github.com/scriptscat/scriptlist/sessions/642555f0-cb22-4efb-9ec1-c6e96e9173e1 Co-authored-by: CodFrm <22783163+CodFrm@users.noreply.github.com> --- internal/api/script/script.go | 16 ++++++++-------- internal/pkg/code/code.go | 3 +++ internal/pkg/code/zh_cn.go | 3 +++ internal/service/script_svc/script.go | 16 ++++++++++++++++ 4 files changed, 30 insertions(+), 8 deletions(-) diff --git a/internal/api/script/script.go b/internal/api/script/script.go index d0ed7a1..69b1d7d 100644 --- a/internal/api/script/script.go +++ b/internal/api/script/script.go @@ -84,11 +84,11 @@ type CreateRequest struct { mux.Meta `path:"/scripts" method:"POST"` Content string `form:"content" binding:"required,max=102400" label:"脚本详细描述"` Code string `form:"code" binding:"required,max=10485760" label:"脚本代码"` - Name string `form:"name" binding:"max=128" label:"库的名字"` - Description string `form:"description" binding:"max=10240" label:"库的描述"` + Name string `form:"name" binding:"max=50" label:"库的名字"` + Description string `form:"description" binding:"max=200" label:"库的描述"` Definition string `form:"definition" binding:"max=10240" label:"库的定义文件"` Version string `form:"version" binding:"max=32" label:"库的版本"` - Tags []string `form:"tags" binding:"omitempty,max=64" label:"标签"` // 标签,只有脚本类型为库时才有意义 + Tags []string `form:"tags" binding:"omitempty,max=5" label:"标签"` // 标签,只有脚本类型为库时才有意义 CategoryID int64 `form:"category" binding:"omitempty,numeric" label:"分类ID"` // 分类ID Type script_entity.Type `form:"type" binding:"required,oneof=1 2 3" label:"脚本类型"` // 脚本类型:1 用户脚本 2 订阅脚本(不支持) 3 脚本引用库 Public script_entity.Public `form:"public" binding:"required,oneof=1 2 3" label:"公开类型"` // 公开类型:1 公开 2 半公开 3 私有 @@ -120,7 +120,7 @@ type UpdateCodeRequest struct { //Name string `form:"name" binding:"max=128" label:"库的名字"` //Description string `form:"description" binding:"max=102400" label:"库的描述"` Version string `binding:"required,max=128" form:"version" label:"库的版本号"` - Tags []string `form:"tags" binding:"omitempty,max=64" label:"标签"` // 标签,只有脚本类型为库时才有意义 + Tags []string `form:"tags" binding:"omitempty,max=5" label:"标签"` // 标签,只有脚本类型为库时才有意义 Content string `binding:"required,max=102400" form:"content" label:"脚本详细描述"` Code string `binding:"required,max=10485760" form:"code" label:"脚本代码"` Definition string `binding:"max=102400" form:"definition" label:"库的定义文件"` @@ -260,8 +260,8 @@ type GetSettingResponse struct { type UpdateSettingRequest struct { mux.Meta `path:"/scripts/:id/setting" method:"PUT"` ID int64 `uri:"id" binding:"required"` - Name string `json:"name" binding:"max=128" label:"库的名字"` - Description string `json:"description" binding:"max=102400" label:"库的描述"` + Name string `json:"name" binding:"max=50" label:"库的名字"` + Description string `json:"description" binding:"max=200" label:"库的描述"` SyncUrl string `json:"sync_url" binding:"omitempty,url,max=1024" label:"代码同步url"` ContentUrl string `json:"content_url" binding:"omitempty,url,max=1024" label:"详细描述同步url"` DefinitionUrl string `json:"definition_url" binding:"omitempty,url,max=1024" label:"定义文件同步url"` @@ -276,8 +276,8 @@ type UpdateSettingResponse struct { // UpdateLibInfoRequest 更新库信息 type UpdateLibInfoRequest struct { mux.Meta `path:"/scripts/:id/lib-info" method:"PUT"` - Name string `json:"name" binding:"max=128" label:"库的名字"` - Description string `json:"description" binding:"max=102400" label:"库的描述"` + Name string `json:"name" binding:"max=50" label:"库的名字"` + Description string `json:"description" binding:"max=200" label:"库的描述"` } type UpdateLibInfoResponse struct { diff --git a/internal/pkg/code/code.go b/internal/pkg/code/code.go index 8b8bc0a..847b93a 100644 --- a/internal/pkg/code/code.go +++ b/internal/pkg/code/code.go @@ -38,6 +38,9 @@ const ( ScriptDeleteReleaseNotLatest ScriptCategoryNotFound + ScriptNameTooLong + ScriptDescTooLong + ScriptTagsTooMany ) // issue diff --git a/internal/pkg/code/zh_cn.go b/internal/pkg/code/zh_cn.go index 5f288e4..ce336d8 100644 --- a/internal/pkg/code/zh_cn.go +++ b/internal/pkg/code/zh_cn.go @@ -39,6 +39,9 @@ var zhCN = map[int]string{ WebhookRepositoryNotFound: "仓库不存在", ScriptDeleteReleaseNotLatest: "删除发布版本失败,没有新的正式版本了", ScriptCategoryNotFound: "脚本分类不存在", + ScriptNameTooLong: "脚本名称过长,最多50个字符", + ScriptDescTooLong: "脚本描述过长,最多200个字符", + ScriptTagsTooMany: "标签数量过多,最多5个", IssueLabelNotExist: "标签不存在", IssueNotFound: "反馈不存在", diff --git a/internal/service/script_svc/script.go b/internal/service/script_svc/script.go index d48b541..c4c82d6 100644 --- a/internal/service/script_svc/script.go +++ b/internal/service/script_svc/script.go @@ -11,6 +11,7 @@ import ( "strconv" "strings" "time" + "unicode/utf8" "github.com/scriptscat/scriptlist/internal/repository/issue_repo" "github.com/scriptscat/scriptlist/internal/repository/report_repo" @@ -351,8 +352,17 @@ func (s *scriptSvc) Create(ctx context.Context, req *api.CreateRequest) (*api.Cr } script.Name = metaJson["name"][0] script.Description = metaJson["description"][0] + if utf8.RuneCountInString(script.Name) > 50 { + return i18n.NewError(ctx, code.ScriptNameTooLong) + } + if utf8.RuneCountInString(script.Description) > 200 { + return i18n.NewError(ctx, code.ScriptDescTooLong) + } // 处理tag关联 tags = metaJson["tags"] + if len(tags) > 5 { + return i18n.NewError(ctx, code.ScriptTagsTooMany) + } if len(metaJson["background"]) > 0 || len(metaJson["crontab"]) > 0 { tags = append(tags, "后台脚本") } @@ -501,6 +511,12 @@ func (s *scriptSvc) UpdateCode(ctx context.Context, req *api.UpdateCodeRequest) // 更新名字和描述 script.Name = metaJson["name"][0] script.Description = metaJson["description"][0] + if utf8.RuneCountInString(script.Name) > 50 { + return nil, i18n.NewError(ctx, code.ScriptNameTooLong) + } + if utf8.RuneCountInString(script.Description) > 200 { + return nil, i18n.NewError(ctx, code.ScriptDescTooLong) + } tags = req.Tags if len(metaJson["background"]) > 0 || len(metaJson["crontab"]) > 0 { tags = append(tags, "后台脚本") From 4fb06cda97094e27bff3c11bd8728325a73395c3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 12 Apr 2026 02:45:45 +0000 Subject: [PATCH 3/7] Extract validateScriptMeta helper to eliminate duplication Agent-Logs-Url: https://github.com/scriptscat/scriptlist/sessions/642555f0-cb22-4efb-9ec1-c6e96e9173e1 Co-authored-by: CodFrm <22783163+CodFrm@users.noreply.github.com> --- internal/service/script_svc/script.go | 33 +++++++++++++++------------ 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/internal/service/script_svc/script.go b/internal/service/script_svc/script.go index c4c82d6..0d76243 100644 --- a/internal/service/script_svc/script.go +++ b/internal/service/script_svc/script.go @@ -303,6 +303,20 @@ func (s *scriptSvc) scriptCode(ctx context.Context, script *script_entity.Script return ret } +// validateScriptMeta validates the name, description, and tags extracted from script metadata. +func validateScriptMeta(ctx context.Context, name, description string, tags []string) error { + if utf8.RuneCountInString(name) > 50 { + return i18n.NewError(ctx, code.ScriptNameTooLong) + } + if utf8.RuneCountInString(description) > 200 { + return i18n.NewError(ctx, code.ScriptDescTooLong) + } + if len(tags) > 5 { + return i18n.NewError(ctx, code.ScriptTagsTooMany) + } + return nil +} + // Create 创建脚本 func (s *scriptSvc) Create(ctx context.Context, req *api.CreateRequest) (*api.CreateResponse, error) { script := &script_entity.Script{ @@ -352,16 +366,10 @@ func (s *scriptSvc) Create(ctx context.Context, req *api.CreateRequest) (*api.Cr } script.Name = metaJson["name"][0] script.Description = metaJson["description"][0] - if utf8.RuneCountInString(script.Name) > 50 { - return i18n.NewError(ctx, code.ScriptNameTooLong) - } - if utf8.RuneCountInString(script.Description) > 200 { - return i18n.NewError(ctx, code.ScriptDescTooLong) - } // 处理tag关联 tags = metaJson["tags"] - if len(tags) > 5 { - return i18n.NewError(ctx, code.ScriptTagsTooMany) + if err := validateScriptMeta(ctx, script.Name, script.Description, tags); err != nil { + return err } if len(metaJson["background"]) > 0 || len(metaJson["crontab"]) > 0 { tags = append(tags, "后台脚本") @@ -511,13 +519,10 @@ func (s *scriptSvc) UpdateCode(ctx context.Context, req *api.UpdateCodeRequest) // 更新名字和描述 script.Name = metaJson["name"][0] script.Description = metaJson["description"][0] - if utf8.RuneCountInString(script.Name) > 50 { - return nil, i18n.NewError(ctx, code.ScriptNameTooLong) - } - if utf8.RuneCountInString(script.Description) > 200 { - return nil, i18n.NewError(ctx, code.ScriptDescTooLong) - } tags = req.Tags + if err := validateScriptMeta(ctx, script.Name, script.Description, tags); err != nil { + return nil, err + } if len(metaJson["background"]) > 0 || len(metaJson["crontab"]) > 0 { tags = append(tags, "后台脚本") } From 4f55c9c3eb3ba70b996e0b246138fa2e21ae53ab Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 12 Apr 2026 03:03:33 +0000 Subject: [PATCH 4/7] Add format validation for name/description, skip on unchanged update, add unit tests Agent-Logs-Url: https://github.com/scriptscat/scriptlist/sessions/31498497-724d-4fdf-bd6b-a99c166a2434 Co-authored-by: CodFrm <22783163+CodFrm@users.noreply.github.com> --- internal/pkg/code/code.go | 2 + internal/pkg/code/zh_cn.go | 2 + internal/service/script_svc/script.go | 29 +++- .../script_svc/script_validate_test.go | 148 ++++++++++++++++++ 4 files changed, 174 insertions(+), 7 deletions(-) create mode 100644 internal/service/script_svc/script_validate_test.go diff --git a/internal/pkg/code/code.go b/internal/pkg/code/code.go index 847b93a..6c80635 100644 --- a/internal/pkg/code/code.go +++ b/internal/pkg/code/code.go @@ -41,6 +41,8 @@ const ( ScriptNameTooLong ScriptDescTooLong ScriptTagsTooMany + ScriptNameInvalid + ScriptDescInvalid ) // issue diff --git a/internal/pkg/code/zh_cn.go b/internal/pkg/code/zh_cn.go index ce336d8..46d0544 100644 --- a/internal/pkg/code/zh_cn.go +++ b/internal/pkg/code/zh_cn.go @@ -42,6 +42,8 @@ var zhCN = map[int]string{ ScriptNameTooLong: "脚本名称过长,最多50个字符", ScriptDescTooLong: "脚本描述过长,最多200个字符", ScriptTagsTooMany: "标签数量过多,最多5个", + ScriptNameInvalid: "脚本名称格式无效,名称应为简单名称,不能包含换行符", + ScriptDescInvalid: "脚本描述格式无效,描述应为一段话,不能包含换行符", IssueLabelNotExist: "标签不存在", IssueNotFound: "反馈不存在", diff --git a/internal/service/script_svc/script.go b/internal/service/script_svc/script.go index 0d76243..02412cb 100644 --- a/internal/service/script_svc/script.go +++ b/internal/service/script_svc/script.go @@ -304,12 +304,25 @@ func (s *scriptSvc) scriptCode(ctx context.Context, script *script_entity.Script } // validateScriptMeta validates the name, description, and tags extracted from script metadata. -func validateScriptMeta(ctx context.Context, name, description string, tags []string) error { - if utf8.RuneCountInString(name) > 50 { - return i18n.NewError(ctx, code.ScriptNameTooLong) +// If nameUnchanged and descUnchanged are both true (i.e. neither field changed from the stored +// value), the length/format checks on name and description are skipped so existing scripts that +// pre-date these limits can still be updated without forcing the author to rename them. +func validateScriptMeta(ctx context.Context, name, description string, tags []string, nameUnchanged, descUnchanged bool) error { + if !nameUnchanged { + if strings.ContainsAny(name, "\r\n") { + return i18n.NewError(ctx, code.ScriptNameInvalid) + } + if utf8.RuneCountInString(name) > 50 { + return i18n.NewError(ctx, code.ScriptNameTooLong) + } } - if utf8.RuneCountInString(description) > 200 { - return i18n.NewError(ctx, code.ScriptDescTooLong) + if !descUnchanged { + if strings.ContainsAny(description, "\r\n") { + return i18n.NewError(ctx, code.ScriptDescInvalid) + } + if utf8.RuneCountInString(description) > 200 { + return i18n.NewError(ctx, code.ScriptDescTooLong) + } } if len(tags) > 5 { return i18n.NewError(ctx, code.ScriptTagsTooMany) @@ -368,7 +381,7 @@ func (s *scriptSvc) Create(ctx context.Context, req *api.CreateRequest) (*api.Cr script.Description = metaJson["description"][0] // 处理tag关联 tags = metaJson["tags"] - if err := validateScriptMeta(ctx, script.Name, script.Description, tags); err != nil { + if err := validateScriptMeta(ctx, script.Name, script.Description, tags, false, false); err != nil { return err } if len(metaJson["background"]) > 0 || len(metaJson["crontab"]) > 0 { @@ -517,10 +530,12 @@ func (s *scriptSvc) UpdateCode(ctx context.Context, req *api.UpdateCodeRequest) } } // 更新名字和描述 + oldName, oldDescription := script.Name, script.Description script.Name = metaJson["name"][0] script.Description = metaJson["description"][0] tags = req.Tags - if err := validateScriptMeta(ctx, script.Name, script.Description, tags); err != nil { + if err := validateScriptMeta(ctx, script.Name, script.Description, tags, + script.Name == oldName, script.Description == oldDescription); err != nil { return nil, err } if len(metaJson["background"]) > 0 || len(metaJson["crontab"]) > 0 { diff --git a/internal/service/script_svc/script_validate_test.go b/internal/service/script_svc/script_validate_test.go new file mode 100644 index 0000000..cb4aea4 --- /dev/null +++ b/internal/service/script_svc/script_validate_test.go @@ -0,0 +1,148 @@ +package script_svc + +import ( + "context" + "testing" + + "github.com/scriptscat/scriptlist/internal/pkg/code" + "github.com/stretchr/testify/assert" +) + +func TestValidateScriptMeta(t *testing.T) { + ctx := context.Background() + + tests := []struct { + name string + scriptName string + description string + tags []string + nameUnchanged bool + descUnchanged bool + wantErrCode int + }{ + { + name: "valid name and description", + scriptName: "My Script", + description: "A simple one-line description.", + tags: []string{"tag1", "tag2"}, + wantErrCode: 0, + }, + { + name: "name with newline", + scriptName: "My Script\nWith Newline", + description: "A simple description.", + tags: nil, + wantErrCode: code.ScriptNameInvalid, + }, + { + name: "name with carriage return", + scriptName: "My Script\rWith CR", + description: "A simple description.", + tags: nil, + wantErrCode: code.ScriptNameInvalid, + }, + { + name: "name too long", + scriptName: "这个脚本名称非常非常非常非常非常非常非常非常非常非常非常非常非常非常非常非常长", // 34 CJK chars > 50? No, let's use more + description: "A simple description.", + tags: nil, + wantErrCode: 0, // 34 < 50 + }, + { + name: "name exactly 51 runes", + scriptName: "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxy", // 51 chars + description: "A simple description.", + tags: nil, + wantErrCode: code.ScriptNameTooLong, + }, + { + name: "name exactly 50 runes", + scriptName: "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwx", // 50 chars + description: "A simple description.", + tags: nil, + wantErrCode: 0, + }, + { + name: "description with newline", + scriptName: "My Script", + description: "First line.\nSecond line.", + tags: nil, + wantErrCode: code.ScriptDescInvalid, + }, + { + name: "description with carriage return", + scriptName: "My Script", + description: "First line.\rSecond line.", + tags: nil, + wantErrCode: code.ScriptDescInvalid, + }, + { + name: "description too long", + scriptName: "My Script", + description: "这是一段非常长的描述这是一段非常长的描述这是一段非常长的描述这是一段非常长的描述这是一段非常长的描述这是一段非常长的描述这是一段非常长的描述这是一段非常长的描述这是一段非常长的描述这是一段非常长的描述这是一段非常长的描述这是一段非常长的描述这是一段非常长的描述这是一段非常长的描述这是一段非常长的描述这是一段非常长的描述这是一段非常长的描述这是一段非常长的描述这是一段非常长的描述这是一段非常长的描述还要再多一点", + tags: nil, + wantErrCode: code.ScriptDescTooLong, + }, + { + name: "too many tags", + scriptName: "My Script", + description: "A simple description.", + tags: []string{"t1", "t2", "t3", "t4", "t5", "t6"}, + wantErrCode: code.ScriptTagsTooMany, + }, + { + name: "exactly 5 tags", + scriptName: "My Script", + description: "A simple description.", + tags: []string{"t1", "t2", "t3", "t4", "t5"}, + wantErrCode: 0, + }, + { + name: "skip name validation when name unchanged", + scriptName: "Old Name\nWith Newline", + description: "A simple description.", + tags: nil, + nameUnchanged: true, + wantErrCode: 0, + }, + { + name: "skip desc validation when desc unchanged", + scriptName: "My Script", + description: "Old description\nWith Newline", + tags: nil, + descUnchanged: true, + wantErrCode: 0, + }, + { + name: "still validate tags when name and desc unchanged", + scriptName: "My Script", + description: "A simple description.", + tags: []string{"t1", "t2", "t3", "t4", "t5", "t6"}, + nameUnchanged: true, + descUnchanged: true, + wantErrCode: code.ScriptTagsTooMany, + }, + { + name: "do not skip when only name unchanged but desc changed", + scriptName: "My Script", + description: "New description\nWith Newline", + tags: nil, + nameUnchanged: true, + descUnchanged: false, + wantErrCode: code.ScriptDescInvalid, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateScriptMeta(ctx, tt.scriptName, tt.description, tt.tags, tt.nameUnchanged, tt.descUnchanged) + if tt.wantErrCode == 0 { + assert.NoError(t, err) + } else { + assert.Error(t, err) + // verify error code via error message mapping + _ = err // error code is embedded in i18n error; presence of error is sufficient here + } + }) + } +} From db1ec35d966badc84c814c9e98ec04d53559fe23 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 12 Apr 2026 03:06:17 +0000 Subject: [PATCH 5/7] Assert specific error codes in validateScriptMeta unit tests Agent-Logs-Url: https://github.com/scriptscat/scriptlist/sessions/31498497-724d-4fdf-bd6b-a99c166a2434 Co-authored-by: CodFrm <22783163+CodFrm@users.noreply.github.com> --- .../script_svc/script_validate_test.go | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/internal/service/script_svc/script_validate_test.go b/internal/service/script_svc/script_validate_test.go index cb4aea4..d1bb761 100644 --- a/internal/service/script_svc/script_validate_test.go +++ b/internal/service/script_svc/script_validate_test.go @@ -4,10 +4,21 @@ import ( "context" "testing" + "github.com/cago-frame/cago/pkg/utils/httputils" "github.com/scriptscat/scriptlist/internal/pkg/code" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) +// assertErrCode checks that err is a *httputils.Error with the expected code. +func assertErrCode(t *testing.T, err error, wantCode int) { + t.Helper() + require.Error(t, err) + var herr *httputils.Error + require.ErrorAs(t, err, &herr, "expected *httputils.Error") + assert.Equal(t, wantCode, herr.Code) +} + func TestValidateScriptMeta(t *testing.T) { ctx := context.Background() @@ -41,13 +52,6 @@ func TestValidateScriptMeta(t *testing.T) { tags: nil, wantErrCode: code.ScriptNameInvalid, }, - { - name: "name too long", - scriptName: "这个脚本名称非常非常非常非常非常非常非常非常非常非常非常非常非常非常非常非常长", // 34 CJK chars > 50? No, let's use more - description: "A simple description.", - tags: nil, - wantErrCode: 0, // 34 < 50 - }, { name: "name exactly 51 runes", scriptName: "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxy", // 51 chars @@ -77,7 +81,7 @@ func TestValidateScriptMeta(t *testing.T) { wantErrCode: code.ScriptDescInvalid, }, { - name: "description too long", + name: "description too long (>200 runes)", scriptName: "My Script", description: "这是一段非常长的描述这是一段非常长的描述这是一段非常长的描述这是一段非常长的描述这是一段非常长的描述这是一段非常长的描述这是一段非常长的描述这是一段非常长的描述这是一段非常长的描述这是一段非常长的描述这是一段非常长的描述这是一段非常长的描述这是一段非常长的描述这是一段非常长的描述这是一段非常长的描述这是一段非常长的描述这是一段非常长的描述这是一段非常长的描述这是一段非常长的描述这是一段非常长的描述还要再多一点", tags: nil, @@ -123,7 +127,7 @@ func TestValidateScriptMeta(t *testing.T) { wantErrCode: code.ScriptTagsTooMany, }, { - name: "do not skip when only name unchanged but desc changed", + name: "do not skip desc when only name unchanged", scriptName: "My Script", description: "New description\nWith Newline", tags: nil, @@ -139,9 +143,7 @@ func TestValidateScriptMeta(t *testing.T) { if tt.wantErrCode == 0 { assert.NoError(t, err) } else { - assert.Error(t, err) - // verify error code via error message mapping - _ = err // error code is embedded in i18n error; presence of error is sufficient here + assertErrCode(t, err, tt.wantErrCode) } }) } From ab2cbee464dfc296e81479a591d37283812a8c2e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 12 Apr 2026 03:17:32 +0000 Subject: [PATCH 6/7] Strengthen name/desc validation: SEO separator check and single-sentence check Agent-Logs-Url: https://github.com/scriptscat/scriptlist/sessions/756524d6-1521-49e7-9189-a45815cc1623 Co-authored-by: CodFrm <22783163+CodFrm@users.noreply.github.com> --- internal/pkg/code/zh_cn.go | 4 +- internal/service/script_svc/script.go | 18 ++- .../script_svc/script_validate_test.go | 112 +++++++++++++++--- 3 files changed, 111 insertions(+), 23 deletions(-) diff --git a/internal/pkg/code/zh_cn.go b/internal/pkg/code/zh_cn.go index 46d0544..54d781a 100644 --- a/internal/pkg/code/zh_cn.go +++ b/internal/pkg/code/zh_cn.go @@ -42,8 +42,8 @@ var zhCN = map[int]string{ ScriptNameTooLong: "脚本名称过长,最多50个字符", ScriptDescTooLong: "脚本描述过长,最多200个字符", ScriptTagsTooMany: "标签数量过多,最多5个", - ScriptNameInvalid: "脚本名称格式无效,名称应为简单名称,不能包含换行符", - ScriptDescInvalid: "脚本描述格式无效,描述应为一段话,不能包含换行符", + ScriptNameInvalid: "脚本名称格式无效,名称应为简单名称,不能包含换行符或逗号、竖线等分隔符", + ScriptDescInvalid: "脚本描述格式无效,描述应为一句话,不能包含换行符或多个句子", IssueLabelNotExist: "标签不存在", IssueNotFound: "反馈不存在", diff --git a/internal/service/script_svc/script.go b/internal/service/script_svc/script.go index 02412cb..e57698d 100644 --- a/internal/service/script_svc/script.go +++ b/internal/service/script_svc/script.go @@ -8,6 +8,7 @@ import ( "errors" "fmt" "net/http" + "regexp" "strconv" "strings" "time" @@ -303,13 +304,26 @@ func (s *scriptSvc) scriptCode(ctx context.Context, script *script_entity.Script return ret } +// nameInvalidRe rejects script names that contain SEO keyword-stuffing separators +// (commas, pipes, semicolons — in both ASCII and Chinese full-width forms) or newlines. +var nameInvalidRe = regexp.MustCompile(`[\r\n,,|;;]`) + +// multiSentenceRe detects descriptions that contain more than one sentence. +// It matches: +// - A Chinese sentence-ending mark (。!?) followed by any non-whitespace content, indicating +// another sentence follows. +// - An ASCII sentence-ending mark (.!?) followed by whitespace and then a CJK character or +// ASCII upper-case letter, indicating a new English/Chinese sentence. +// - A newline character, which is not allowed in a one-sentence description. +var multiSentenceRe = regexp.MustCompile(`[\r\n]|[。!?][\s]*\S|[.!?]\s+[A-Z\x{4e00}-\x{9fa5}]`) + // validateScriptMeta validates the name, description, and tags extracted from script metadata. // If nameUnchanged and descUnchanged are both true (i.e. neither field changed from the stored // value), the length/format checks on name and description are skipped so existing scripts that // pre-date these limits can still be updated without forcing the author to rename them. func validateScriptMeta(ctx context.Context, name, description string, tags []string, nameUnchanged, descUnchanged bool) error { if !nameUnchanged { - if strings.ContainsAny(name, "\r\n") { + if nameInvalidRe.MatchString(name) { return i18n.NewError(ctx, code.ScriptNameInvalid) } if utf8.RuneCountInString(name) > 50 { @@ -317,7 +331,7 @@ func validateScriptMeta(ctx context.Context, name, description string, tags []st } } if !descUnchanged { - if strings.ContainsAny(description, "\r\n") { + if multiSentenceRe.MatchString(description) { return i18n.NewError(ctx, code.ScriptDescInvalid) } if utf8.RuneCountInString(description) > 200 { diff --git a/internal/service/script_svc/script_validate_test.go b/internal/service/script_svc/script_validate_test.go index d1bb761..01cbe09 100644 --- a/internal/service/script_svc/script_validate_test.go +++ b/internal/service/script_svc/script_validate_test.go @@ -31,62 +31,138 @@ func TestValidateScriptMeta(t *testing.T) { descUnchanged bool wantErrCode int }{ + // --- valid inputs --- { - name: "valid name and description", + name: "valid simple name and single-sentence description", scriptName: "My Script", description: "A simple one-line description.", tags: []string{"tag1", "tag2"}, wantErrCode: 0, }, { - name: "name with newline", + name: "valid name with hyphen", + scriptName: "Auto-Fill Script", + description: "一句简单的中文描述", + wantErrCode: 0, + }, + { + name: "valid description ending with Chinese period", + scriptName: "脚本名称", + description: "一段简单的脚本描述。", + wantErrCode: 0, + }, + + // --- name: newline --- + { + name: "name with LF newline", scriptName: "My Script\nWith Newline", description: "A simple description.", - tags: nil, wantErrCode: code.ScriptNameInvalid, }, { - name: "name with carriage return", + name: "name with CR", scriptName: "My Script\rWith CR", description: "A simple description.", - tags: nil, + wantErrCode: code.ScriptNameInvalid, + }, + + // --- name: SEO separator punctuation --- + { + name: "name with ASCII comma", + scriptName: "Script,keyword1,keyword2", + description: "A simple description.", + wantErrCode: code.ScriptNameInvalid, + }, + { + name: "name with Chinese full-width comma", + scriptName: "脚本名称,关键词1,关键词2", + description: "一段描述。", + wantErrCode: code.ScriptNameInvalid, + }, + { + name: "name with pipe", + scriptName: "Script | keyword1 | keyword2", + description: "A simple description.", + wantErrCode: code.ScriptNameInvalid, + }, + { + name: "name with ASCII semicolon", + scriptName: "Script;keyword1", + description: "A simple description.", wantErrCode: code.ScriptNameInvalid, }, + { + name: "name with Chinese full-width semicolon", + scriptName: "脚本;关键词", + description: "一段描述。", + wantErrCode: code.ScriptNameInvalid, + }, + + // --- name: length --- { name: "name exactly 51 runes", scriptName: "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxy", // 51 chars description: "A simple description.", - tags: nil, wantErrCode: code.ScriptNameTooLong, }, { name: "name exactly 50 runes", scriptName: "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwx", // 50 chars description: "A simple description.", - tags: nil, wantErrCode: 0, }, + + // --- description: newline --- { - name: "description with newline", + name: "description with LF newline", scriptName: "My Script", description: "First line.\nSecond line.", - tags: nil, wantErrCode: code.ScriptDescInvalid, }, { - name: "description with carriage return", + name: "description with CR", scriptName: "My Script", description: "First line.\rSecond line.", - tags: nil, + wantErrCode: code.ScriptDescInvalid, + }, + + // --- description: multiple Chinese sentences --- + { + name: "description with two Chinese sentences", + scriptName: "脚本名称", + description: "第一句话。第二句话。", + wantErrCode: code.ScriptDescInvalid, + }, + { + name: "description with Chinese exclamation in middle", + scriptName: "脚本名称", + description: "第一句!第二句。", wantErrCode: code.ScriptDescInvalid, }, + { + name: "description with Chinese question in middle", + scriptName: "脚本名称", + description: "第一句?第二句。", + wantErrCode: code.ScriptDescInvalid, + }, + + // --- description: multiple English sentences --- + { + name: "description with two English sentences", + scriptName: "My Script", + description: "First sentence. Second sentence.", + wantErrCode: code.ScriptDescInvalid, + }, + + // --- description: length --- { name: "description too long (>200 runes)", scriptName: "My Script", description: "这是一段非常长的描述这是一段非常长的描述这是一段非常长的描述这是一段非常长的描述这是一段非常长的描述这是一段非常长的描述这是一段非常长的描述这是一段非常长的描述这是一段非常长的描述这是一段非常长的描述这是一段非常长的描述这是一段非常长的描述这是一段非常长的描述这是一段非常长的描述这是一段非常长的描述这是一段非常长的描述这是一段非常长的描述这是一段非常长的描述这是一段非常长的描述这是一段非常长的描述还要再多一点", - tags: nil, wantErrCode: code.ScriptDescTooLong, }, + + // --- tags --- { name: "too many tags", scriptName: "My Script", @@ -101,19 +177,19 @@ func TestValidateScriptMeta(t *testing.T) { tags: []string{"t1", "t2", "t3", "t4", "t5"}, wantErrCode: 0, }, + + // --- skip on unchanged --- { name: "skip name validation when name unchanged", - scriptName: "Old Name\nWith Newline", + scriptName: "Old Name,keyword", description: "A simple description.", - tags: nil, nameUnchanged: true, wantErrCode: 0, }, { name: "skip desc validation when desc unchanged", scriptName: "My Script", - description: "Old description\nWith Newline", - tags: nil, + description: "第一句。第二句。", descUnchanged: true, wantErrCode: 0, }, @@ -129,10 +205,8 @@ func TestValidateScriptMeta(t *testing.T) { { name: "do not skip desc when only name unchanged", scriptName: "My Script", - description: "New description\nWith Newline", - tags: nil, + description: "第一句。第二句。", nameUnchanged: true, - descUnchanged: false, wantErrCode: code.ScriptDescInvalid, }, } From 5e02e37b1a361641b10c34a2d59b98b218df8bc6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 12 Apr 2026 03:20:03 +0000 Subject: [PATCH 7/7] Clarify multiSentenceRe comment and add whitespace-separated Chinese sentence test Agent-Logs-Url: https://github.com/scriptscat/scriptlist/sessions/756524d6-1521-49e7-9189-a45815cc1623 Co-authored-by: CodFrm <22783163+CodFrm@users.noreply.github.com> --- internal/service/script_svc/script.go | 16 +++++++++------- .../service/script_svc/script_validate_test.go | 6 ++++++ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/internal/service/script_svc/script.go b/internal/service/script_svc/script.go index e57698d..f1bc03e 100644 --- a/internal/service/script_svc/script.go +++ b/internal/service/script_svc/script.go @@ -309,13 +309,15 @@ func (s *scriptSvc) scriptCode(ctx context.Context, script *script_entity.Script var nameInvalidRe = regexp.MustCompile(`[\r\n,,|;;]`) // multiSentenceRe detects descriptions that contain more than one sentence. -// It matches: -// - A Chinese sentence-ending mark (。!?) followed by any non-whitespace content, indicating -// another sentence follows. -// - An ASCII sentence-ending mark (.!?) followed by whitespace and then a CJK character or -// ASCII upper-case letter, indicating a new English/Chinese sentence. -// - A newline character, which is not allowed in a one-sentence description. -var multiSentenceRe = regexp.MustCompile(`[\r\n]|[。!?][\s]*\S|[.!?]\s+[A-Z\x{4e00}-\x{9fa5}]`) +// It matches any of: +// - A newline character (\r or \n), which is not allowed in a one-sentence description. +// - A Chinese sentence-ending mark (。!?) followed by optional whitespace and then any +// non-whitespace character. Chinese punctuation is unambiguous as a sentence boundary so +// zero or more spaces before the next word are all covered: "第一句。第二句" and "第一句。 第二句". +// - An ASCII sentence-ending mark (.!?) followed by at least one space and then a capital +// ASCII letter or a CJK character. Requiring whitespace before the next word avoids false +// positives on abbreviations (e.g. "v1.0") and URLs. +var multiSentenceRe = regexp.MustCompile(`[\r\n]|[。!?]\s*\S|[.!?]\s+[A-Z\x{4e00}-\x{9fa5}]`) // validateScriptMeta validates the name, description, and tags extracted from script metadata. // If nameUnchanged and descUnchanged are both true (i.e. neither field changed from the stored diff --git a/internal/service/script_svc/script_validate_test.go b/internal/service/script_svc/script_validate_test.go index 01cbe09..61d77f7 100644 --- a/internal/service/script_svc/script_validate_test.go +++ b/internal/service/script_svc/script_validate_test.go @@ -133,6 +133,12 @@ func TestValidateScriptMeta(t *testing.T) { description: "第一句话。第二句话。", wantErrCode: code.ScriptDescInvalid, }, + { + name: "description with two Chinese sentences separated by space", + scriptName: "脚本名称", + description: "第一句。 第二句。", + wantErrCode: code.ScriptDescInvalid, + }, { name: "description with Chinese exclamation in middle", scriptName: "脚本名称",