mirror of
https://github.com/iflytek/skillhub.git
synced 2026-08-28 11:25:00 +00:00
fix(bootstrap): support skill directory archives
Signed-off-by: dongmucat <1127093059@qq.com>
This commit is contained in:
parent
973c37613e
commit
dd3e511a91
3 changed files with 19 additions and 32 deletions
|
|
@ -83,7 +83,7 @@ manifest 文件格式如下:
|
|||
|
||||
manifest 中的 `url` 必须指向 zip 包。zip 包需要满足 SkillHub Skill 包协议:
|
||||
|
||||
- zip 根目录必须包含 `SKILL.md`。
|
||||
- zip 可以在根目录直接包含 `SKILL.md`,也可以包含一个单独的顶层 Skill 目录,并在该目录下包含 `SKILL.md`。
|
||||
- `SKILL.md` frontmatter 中必须包含合法的 `name`、`description`、`version` 等元数据。
|
||||
- `SKILL.md` 中的 `name` 经过 slug 归一化后,必须等于 manifest 中的 `slug`。
|
||||
- `SKILL.md` 中的 `version` 必须等于 manifest 中的 `version`。
|
||||
|
|
@ -99,15 +99,16 @@ skillhub-hello-1.0.0.zip
|
|||
└── check.js
|
||||
```
|
||||
|
||||
不推荐的结构:
|
||||
同样支持标准单目录 Skill 包:
|
||||
|
||||
```text
|
||||
skillhub-hello-1.0.0.zip
|
||||
└── skillhub-hello/
|
||||
└── SKILL.md
|
||||
├── SKILL.md
|
||||
└── README.md
|
||||
```
|
||||
|
||||
原因是内置 Skill 同步要求根目录存在 `SKILL.md`,不会把嵌套目录中的 `SKILL.md` 当作入口。
|
||||
如果 zip 中存在多个顶层目录,或在多个目录中同时出现 `SKILL.md`,同步器会跳过该项并记录错误,避免误选入口。
|
||||
|
||||
## 4. URL 安全限制
|
||||
|
||||
|
|
@ -146,7 +147,7 @@ skillhub-hello-1.0.0.zip
|
|||
7. 按 manifest 顺序处理每一个 item。
|
||||
8. 下载前先检查 `@global/{slug}` 和目标版本是否已经存在;如果已经确定应跳过,则不发起远程下载。
|
||||
9. 只有需要发布新 Skill 或新版本时,才下载对应 zip 包。
|
||||
10. 解包并校验根目录 `SKILL.md`。
|
||||
10. 解包并校验 Skill 入口 `SKILL.md`。
|
||||
11. 校验 manifest 中的 `slug`、`version` 与包内元数据一致。
|
||||
12. 发布前再次检查是否已存在同名 Skill 或同版本,处理并发启动场景。
|
||||
13. 需要发布时调用现有 `SkillPublishService.publishFromEntries(...)`。
|
||||
|
|
@ -209,7 +210,7 @@ SKILLHUB_BUILTIN_SKILLS_ENABLED=false
|
|||
|
||||
新增一个内置 Skill 的推荐步骤:
|
||||
|
||||
1. 准备 Skill 包,并确认 zip 根目录包含 `SKILL.md`。
|
||||
1. 准备 Skill 包,并确认 zip 根目录直接包含 `SKILL.md`,或只有一个顶层 Skill 目录且该目录包含 `SKILL.md`。
|
||||
2. 检查 `SKILL.md` 中的 `name` 和 `version`。
|
||||
3. 上传 zip 到 `bjcdn.openstorage.cn` 或其子域名下的官方云存储路径。
|
||||
4. 在 `server/skillhub-app/src/main/resources/builtin-skills/manifest.json` 中新增一项。
|
||||
|
|
@ -246,7 +247,7 @@ SKILLHUB_BUILTIN_SKILLS_ENABLED=false
|
|||
| slug is invalid | 检查 slug 是否符合 SkillHub slug 规则 |
|
||||
| URL is not allowed | 检查 URL 是否为 HTTPS、host 是否为 `bjcdn.openstorage.cn` 或其子域名 |
|
||||
| package download failed | 检查云存储对象是否存在、是否返回 HTTP 200、是否超时 |
|
||||
| package must contain SKILL.md | 检查 zip 根目录是否存在 `SKILL.md` |
|
||||
| package must contain SKILL.md | 检查 zip 是否存在唯一可识别的 `SKILL.md` 入口 |
|
||||
| manifest version does not match package version | 检查 manifest `version` 和 `SKILL.md version` 是否一致 |
|
||||
| slug is already published by another user | 说明 `@global/{slug}` 已被非内置发布者发布,内置同步不会覆盖 |
|
||||
| published fingerprint differs | 并发发布异常后发现同一内置版本已存在但内容不同,需要人工确认是否发生了版本冲突 |
|
||||
|
|
@ -260,7 +261,7 @@ SKILLHUB_BUILTIN_SKILLS_ENABLED=false
|
|||
- manifest JSON 格式合法。
|
||||
- 每个 item 都包含 `slug`、`version`、`url`。
|
||||
- URL 使用 `https://bjcdn.openstorage.cn/...` 或可信子域名。
|
||||
- zip 根目录包含 `SKILL.md`。
|
||||
- zip 根目录直接包含 `SKILL.md`,或只有一个顶层 Skill 目录且该目录包含 `SKILL.md`。
|
||||
- `SKILL.md name` 归一化后的 slug 与 manifest `slug` 一致。
|
||||
- `SKILL.md version` 与 manifest `version` 一致。
|
||||
- 启动日志没有该 item 的 warning 或 error。
|
||||
|
|
|
|||
|
|
@ -8,8 +8,6 @@ import org.springframework.web.multipart.MultipartFile;
|
|||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
|
||||
@Component
|
||||
public class BuiltinSkillPackageExtractor {
|
||||
|
|
@ -21,30 +19,16 @@ public class BuiltinSkillPackageExtractor {
|
|||
}
|
||||
|
||||
public SkillPackageArchiveExtractor.ExtractionResult extract(byte[] zipBytes) throws IOException {
|
||||
assertRootSkillMd(zipBytes);
|
||||
SkillPackageArchiveExtractor.ExtractionResult result =
|
||||
archiveExtractor.extractWithWarnings(new ByteArrayMultipartFile(zipBytes));
|
||||
boolean hasRootSkillMd = result.entries().stream()
|
||||
boolean hasSkillMd = result.entries().stream()
|
||||
.anyMatch(entry -> SkillPackagePolicy.SKILL_MD_PATH.equals(entry.path()));
|
||||
if (!hasRootSkillMd) {
|
||||
throw new IllegalArgumentException("Built-in skill package must contain root " + SkillPackagePolicy.SKILL_MD_PATH);
|
||||
if (!hasSkillMd) {
|
||||
throw new IllegalArgumentException("Built-in skill package must contain " + SkillPackagePolicy.SKILL_MD_PATH);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private void assertRootSkillMd(byte[] zipBytes) throws IOException {
|
||||
try (ZipInputStream zipInputStream = new ZipInputStream(new ByteArrayInputStream(zipBytes))) {
|
||||
ZipEntry entry;
|
||||
while ((entry = zipInputStream.getNextEntry()) != null) {
|
||||
if (!entry.isDirectory() && SkillPackagePolicy.SKILL_MD_PATH.equals(entry.getName())) {
|
||||
return;
|
||||
}
|
||||
zipInputStream.closeEntry();
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("Built-in skill package must contain root " + SkillPackagePolicy.SKILL_MD_PATH);
|
||||
}
|
||||
|
||||
private record ByteArrayMultipartFile(byte[] bytes) implements MultipartFile {
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -49,18 +49,20 @@ class BuiltinSkillPackageExtractorTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
void rejectsZipWithOnlyNestedSkillMd() throws Exception {
|
||||
void acceptsZipWithSingleTopLevelSkillDirectory() throws Exception {
|
||||
byte[] zip = zip(entry("skillhub-hello/SKILL.md", """
|
||||
---
|
||||
name: skillhub-hello
|
||||
version: 1.0.0
|
||||
---
|
||||
# SkillHub Hello
|
||||
"""));
|
||||
"""), entry("skillhub-hello/README.md", "# Readme"));
|
||||
|
||||
assertThatThrownBy(() -> extractor.extract(zip))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining(SkillPackagePolicy.SKILL_MD_PATH);
|
||||
SkillPackageArchiveExtractor.ExtractionResult result = extractor.extract(zip);
|
||||
|
||||
assertThat(result.entries())
|
||||
.extracting(entry -> entry.path())
|
||||
.containsExactly("SKILL.md", "README.md");
|
||||
}
|
||||
|
||||
private static ZipSource entry(String path, String content) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue