fix(skills): 技能文件包 - 修复元数据截断超限并补充初始化回归

This commit is contained in:
ddCat
2026-09-21 11:10:58 +08:00
parent 4ab43ca6f9
commit d5ff22258e
3 changed files with 104 additions and 5 deletions

View File

@@ -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] == '"' {

View File

@@ -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",

View File

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