mirror of
https://github.com/iflytek/skillhub.git
synced 2026-10-06 02:48:28 +00:00
feat(publish): support SKILL.md in subdirectory with warning for ignored files
When SKILL.md is found in a single subdirectory (e.g. my-skill/SKILL.md), promote that directory's contents to root and discard files outside it. Discarded files are reported as warnings through the existing confirm flow.
This commit is contained in:
parent
4d8f9dd3f5
commit
66fb89dc48
2 changed files with 113 additions and 0 deletions
|
|
@ -18,6 +18,8 @@ import java.util.zip.ZipInputStream;
|
|||
@Component
|
||||
public class SkillPackageArchiveExtractor {
|
||||
|
||||
public record ExtractionResult(List<PackageEntry> entries, List<String> warnings) {}
|
||||
|
||||
private final long maxTotalPackageSize;
|
||||
private final long maxSingleFileSize;
|
||||
private final int maxFileCount;
|
||||
|
|
@ -76,6 +78,11 @@ public class SkillPackageArchiveExtractor {
|
|||
return stripSingleRootDirectory(entries);
|
||||
}
|
||||
|
||||
public ExtractionResult extractWithWarnings(MultipartFile file) throws IOException {
|
||||
List<PackageEntry> entries = extract(file);
|
||||
return promoteSingleSkillMdDirectory(entries);
|
||||
}
|
||||
|
||||
/**
|
||||
* If all file paths share a single root directory prefix (e.g., "my-skill/xxx"),
|
||||
* strip that prefix. Otherwise return entries unchanged.
|
||||
|
|
@ -107,6 +114,51 @@ public class SkillPackageArchiveExtractor {
|
|||
.toList();
|
||||
}
|
||||
|
||||
static ExtractionResult promoteSingleSkillMdDirectory(List<PackageEntry> entries) {
|
||||
boolean hasRootSkillMd = entries.stream()
|
||||
.anyMatch(e -> SkillPackagePolicy.SKILL_MD_PATH.equals(e.path()));
|
||||
if (hasRootSkillMd) {
|
||||
return new ExtractionResult(entries, List.of());
|
||||
}
|
||||
|
||||
Set<String> skillMdDirs = new HashSet<>();
|
||||
for (PackageEntry entry : entries) {
|
||||
int slashIndex = entry.path().indexOf('/');
|
||||
if (slashIndex > 0) {
|
||||
String relativePath = entry.path().substring(slashIndex + 1);
|
||||
if (SkillPackagePolicy.SKILL_MD_PATH.equals(relativePath)) {
|
||||
skillMdDirs.add(entry.path().substring(0, slashIndex));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (skillMdDirs.isEmpty()) {
|
||||
return new ExtractionResult(entries, List.of());
|
||||
}
|
||||
if (skillMdDirs.size() > 1) {
|
||||
throw new IllegalArgumentException(
|
||||
"Ambiguous package: SKILL.md found in multiple directories: " + skillMdDirs);
|
||||
}
|
||||
|
||||
String prefix = skillMdDirs.iterator().next() + "/";
|
||||
List<PackageEntry> promoted = new ArrayList<>();
|
||||
List<String> warnings = new ArrayList<>();
|
||||
|
||||
for (PackageEntry entry : entries) {
|
||||
if (entry.path().startsWith(prefix)) {
|
||||
promoted.add(new PackageEntry(
|
||||
entry.path().substring(prefix.length()),
|
||||
entry.content(),
|
||||
entry.size(),
|
||||
entry.contentType()));
|
||||
} else {
|
||||
warnings.add("Ignored file outside skill directory: " + entry.path());
|
||||
}
|
||||
}
|
||||
|
||||
return new ExtractionResult(promoted, warnings);
|
||||
}
|
||||
|
||||
private byte[] readEntry(ZipInputStream zis, String path) throws IOException {
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
byte[] buffer = new byte[8192];
|
||||
|
|
|
|||
|
|
@ -126,6 +126,67 @@ class SkillPackageArchiveExtractorTest {
|
|||
assertTrue(entries.stream().anyMatch(e -> e.path().equals("dir-b/other.md")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void promotesSkillMdFromSubdirectoryAndDiscardsRootFiles() throws Exception {
|
||||
byte[] zipBytes = createZip(Map.of(
|
||||
"my-skill/SKILL.md", "---\nname: test\n---\n".getBytes(),
|
||||
"my-skill/README.md", "# readme".getBytes(),
|
||||
"other.txt", "stray file".getBytes()
|
||||
));
|
||||
MockMultipartFile file = new MockMultipartFile("file", "test.zip", "application/zip", zipBytes);
|
||||
|
||||
SkillPackageArchiveExtractor.ExtractionResult result = extractor.extractWithWarnings(file);
|
||||
|
||||
assertEquals(2, result.entries().size());
|
||||
assertTrue(result.entries().stream().anyMatch(e -> e.path().equals("SKILL.md")));
|
||||
assertTrue(result.entries().stream().anyMatch(e -> e.path().equals("README.md")));
|
||||
assertTrue(result.warnings().stream().anyMatch(w -> w.contains("other.txt")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsAmbiguousMultipleSkillMdInSubdirectories() throws Exception {
|
||||
byte[] zipBytes = createZip(Map.of(
|
||||
"dir1/SKILL.md", "---\nname: a\n---\n".getBytes(),
|
||||
"dir2/SKILL.md", "---\nname: b\n---\n".getBytes()
|
||||
));
|
||||
MockMultipartFile file = new MockMultipartFile("file", "test.zip", "application/zip", zipBytes);
|
||||
|
||||
IllegalArgumentException error = assertThrows(IllegalArgumentException.class,
|
||||
() -> extractor.extractWithWarnings(file));
|
||||
assertTrue(error.getMessage().contains("Ambiguous"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void noPromotionWhenSkillMdAtRoot() throws Exception {
|
||||
byte[] zipBytes = createZip(Map.of(
|
||||
"SKILL.md", "---\nname: test\n---\n".getBytes(),
|
||||
"sub/file.txt", "content".getBytes()
|
||||
));
|
||||
MockMultipartFile file = new MockMultipartFile("file", "test.zip", "application/zip", zipBytes);
|
||||
|
||||
SkillPackageArchiveExtractor.ExtractionResult result = extractor.extractWithWarnings(file);
|
||||
|
||||
assertEquals(2, result.entries().size());
|
||||
assertTrue(result.warnings().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void promotesSubdirectoryPreservingNestedPaths() throws Exception {
|
||||
byte[] zipBytes = createZip(Map.of(
|
||||
"my-skill/SKILL.md", "---\nname: test\n---\n".getBytes(),
|
||||
"my-skill/sub/deep.md", "nested".getBytes(),
|
||||
"stray.txt", "ignored".getBytes()
|
||||
));
|
||||
MockMultipartFile file = new MockMultipartFile("file", "test.zip", "application/zip", zipBytes);
|
||||
|
||||
SkillPackageArchiveExtractor.ExtractionResult result = extractor.extractWithWarnings(file);
|
||||
|
||||
assertEquals(2, result.entries().size());
|
||||
assertTrue(result.entries().stream().anyMatch(e -> e.path().equals("SKILL.md")));
|
||||
assertTrue(result.entries().stream().anyMatch(e -> e.path().equals("sub/deep.md")));
|
||||
assertTrue(result.warnings().stream().anyMatch(w -> w.contains("stray.txt")));
|
||||
}
|
||||
|
||||
private byte[] createZip(String entryName, String content) throws Exception {
|
||||
return createZip(entryName, content.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue