From d5ff22258e95807df2dc9943c2837d4e3b1bcab7 Mon Sep 17 00:00:00 2001 From: ddCat <2695092045@qq.com> Date: Mon, 21 Sep 2026 11:10:58 +0800 Subject: [PATCH] =?UTF-8?q?fix(skills):=20=E6=8A=80=E8=83=BD=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E5=8C=85=20-=20=E4=BF=AE=E5=A4=8D=E5=85=83=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E6=88=AA=E6=96=AD=E8=B6=85=E9=99=90=E5=B9=B6=E8=A1=A5?= =?UTF-8?q?=E5=85=85=E5=88=9D=E5=A7=8B=E5=8C=96=E5=9B=9E=E5=BD=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/skills/skill_packages.go | 26 ++++++++-- .../internal/skills/skill_packages_test.go | 47 +++++++++++++++++++ backend/internal/skills/skills_seed_test.go | 36 +++++++++++++- 3 files changed, 104 insertions(+), 5 deletions(-) diff --git a/backend/internal/skills/skill_packages.go b/backend/internal/skills/skill_packages.go index 6803ac328..699444118 100644 --- a/backend/internal/skills/skill_packages.go +++ b/backend/internal/skills/skill_packages.go @@ -827,14 +827,32 @@ func parseSkillPackageMetadata(data []byte) skillPackageMetadata { } paragraph = append(paragraph, trimmed) } - metadata.Description = kernel.TruncateRunes(strings.Join(paragraph, " "), 500) + metadata.Description = strings.Join(paragraph, " ") } - metadata.Name = kernel.TruncateRunes(strings.TrimSpace(metadata.Name), 80) - metadata.Description = kernel.TruncateRunes(strings.TrimSpace(metadata.Description), 500) - metadata.Version = kernel.TruncateRunes(strings.TrimSpace(metadata.Version), 64) + metadata.Name = truncateSkillMetadata(metadata.Name, 80) + metadata.Description = truncateSkillMetadata(metadata.Description, 500) + metadata.Version = truncateSkillMetadata(metadata.Version, 64) return metadata } +// truncateSkillMetadata keeps the result within the validation limit. The +// general-purpose TruncateRunes helper appends "...", which is useful for +// display snippets but would make a value exactly at the limit invalid. +func truncateSkillMetadata(value string, limit int) string { + value = strings.TrimSpace(value) + if limit <= 0 { + return "" + } + runes := []rune(value) + if len(runes) <= limit { + return value + } + if limit <= 3 { + return string(runes[:limit]) + } + return string(runes[:limit-3]) + "..." +} + func yamlScalar(value string) string { value = strings.TrimSpace(value) if len(value) >= 2 && value[0] == '"' { diff --git a/backend/internal/skills/skill_packages_test.go b/backend/internal/skills/skill_packages_test.go index f979d48cf..159934c39 100644 --- a/backend/internal/skills/skill_packages_test.go +++ b/backend/internal/skills/skill_packages_test.go @@ -3,6 +3,7 @@ package skills import ( "archive/zip" "bytes" + "strings" "testing" "infinite-canvas/backend/internal/kernel" @@ -26,6 +27,52 @@ func TestArchiveFromMarkdownInfersMetadata(t *testing.T) { } } +func TestArchiveFromMarkdownTruncatesInferredDescriptionWithinLimit(t *testing.T) { + longDescription := bytes.Repeat([]byte("描述内容。"), 140) + data := append([]byte("# 风格库四级匹配序\n\n"), longDescription...) + + archive, err := archiveFromMarkdown(data, "", "") + if err != nil { + t.Fatal(err) + } + if got := len([]rune(archive.Metadata.Description)); got > 500 { + t.Fatalf("inferred description length = %d, want <= 500", got) + } + if archive.Metadata.Description == "" { + t.Fatal("inferred description is empty") + } + if !bytes.Equal(archive.Files["SKILL.md"], data) { + t.Fatal("metadata truncation changed the original instruction") + } +} + +func TestSkillMetadataLengthBoundaries(t *testing.T) { + for _, length := range []int{499, 500, 501, 553} { + value := strings.Repeat("文", length) + markdown := []byte("# 技能\n\n" + value) + archive, err := archiveFromMarkdown(markdown, "", "") + if err != nil { + t.Fatalf("description length %d: %v", length, err) + } + want := value + if length > 500 { + want = strings.Repeat("文", 497) + "..." + } + if archive.Metadata.Description != want { + t.Fatalf("description length %d: unexpected truncation", length) + } + } + metadata := parseSkillPackageMetadata([]byte("---\nname: " + strings.Repeat("名", 81) + "\ndescription: " + strings.Repeat("文", 501) + "\nmetadata:\n version: " + strings.Repeat("版", 65) + "\n---\n")) + if len([]rune(metadata.Name)) != 80 || len([]rune(metadata.Description)) != 500 || len([]rune(metadata.Version)) != 64 { + t.Fatalf("metadata exceeds bounds: %#v", metadata) + } + // Supplied metadata still goes through strict validation; no widening of + // the archive contract is needed to fix inferred display metadata. + if _, err := finalizeSkillArchive(map[string][]byte{"SKILL.md": []byte("# 技能")}, skillPackageMetadata{Name: "技能", Description: strings.Repeat("文", 501)}); err == nil { + t.Fatal("expected overlong archive metadata to be rejected") + } +} + func TestArchiveFromZipNormalizesWrapperAndNestedFiles(t *testing.T) { data := skillZip(t, map[string]string{ "director-main/SKILL.md": "---\nname: AI 导演\ndescription: 导演工作流\nmetadata:\n version: 2.1\n---\n", diff --git a/backend/internal/skills/skills_seed_test.go b/backend/internal/skills/skills_seed_test.go index 9618496d3..d73f8f1f3 100644 --- a/backend/internal/skills/skills_seed_test.go +++ b/backend/internal/skills/skills_seed_test.go @@ -27,7 +27,7 @@ func TestBuiltinSeedSyncPreservesUserState(t *testing.T) { t.Error(err) } }) - if err := db.AutoMigrate(&model.Skill{}, &model.UserSkillState{}); err != nil { + if err := db.AutoMigrate(&model.Skill{}, &model.UserSkillState{}, &model.SkillVersion{}, &model.SkillFile{}, &model.User{}, &model.UserIdentity{}); err != nil { t.Fatal(err) } svc := New(repository.New(db), t.TempDir(), nil) @@ -55,6 +55,9 @@ func TestBuiltinSeedSyncPreservesUserState(t *testing.T) { if err := svc.EnsureBuiltinSkills(); err != nil { t.Fatal(err) } + if err := svc.EnsureSkillPackages(); err != nil { + t.Fatal(err) + } state := model.UserSkillState{ID: "test-state", UserID: "test-user", SkillID: "16000000000001", Added: true, Liked: true} if err := db.Create(&state).Error; err != nil { t.Fatal(err) @@ -62,6 +65,9 @@ func TestBuiltinSeedSyncPreservesUserState(t *testing.T) { if err := svc.EnsureBuiltinSkills(); err != nil { t.Fatal(err) } + if err := svc.EnsureSkillPackages(); err != nil { + t.Fatal(err) + } var count int64 if err := db.Model(&model.Skill{}).Count(&count).Error; err != nil { t.Fatal(err) @@ -83,4 +89,32 @@ func TestBuiltinSeedSyncPreservesUserState(t *testing.T) { if skill.OwnerID != "community-itswyatt-k" || skill.CreatedAt.Year() != 2026 { t.Fatalf("invalid persisted seed: %#v", skill) } + var persisted []model.Skill + if err := db.Find(&persisted).Error; err != nil { + t.Fatal(err) + } + for _, item := range persisted { + if item.CurrentVersionID == "" || item.FileCount != 1 { + t.Fatalf("skill %s has no initialized package", item.ID) + } + assertSkillVersionCount(t, db, item.ID, 1) + version, err := svc.repo.SkillVersion(item.CurrentVersionID) + if err != nil { + t.Fatal(err) + } + body, err := svc.readSkillArchiveEntry(version, "SKILL.md") + if err != nil { + t.Fatal(err) + } + if string(body) != item.Instruction { + t.Fatalf("skill %s package instruction was changed", item.ID) + } + } + list, err := svc.Skills("test-user", SkillListRequest{Scope: "public", Search: "freestyle-template-match"}) + if err != nil { + t.Fatal(err) + } + if list.TotalCount != 1 || len(list.Skills) != 1 || list.Skills[0].SkillID != "16000000000003" { + t.Fatalf("community skill not visible in public search: %#v", list) + } }