feat: support GitHub skill batch import
Pipelines as Code CI / hwlab-web-probe-sentinel-nc01- Success
Pipelines as Code CI / platform-infra-gitea-nc01- Success
Pipelines as Code CI / unidesk-host- Success

This commit is contained in:
pikastech
2026-07-19 05:33:26 +02:00
parent c6f69f8af8
commit e8864d24b3
3 changed files with 60 additions and 7 deletions
+1 -1
View File
@@ -122,4 +122,4 @@ targets:
- id: superapi
configRef: /root/superapi/config/superapi.yaml
path: publicExposure
sourceCommit: d42b2c525885b319bbc5b75ee081c78657c1cedb
sourceCommit: 7e14618621bcaef0602e8c7236ae6197ee68cb38
@@ -291,3 +291,29 @@ L1 应通过项目 CLI 启动既有独立 API、worker、Web 与 gateway 进程
提供上传、frontmatter 预览、补齐字段、发布 URL 和一次性读取命令;公开 `/skills/*`
由 API 进程提供且不经过管理 Basic Auth。浏览器验收复用项目 typed web-probe。
公网 shared edge 只通过 owning YAML 和自动交付链收敛,不执行人工确认式 apply。
### 4.17 USER-KEY-SKILL-REQ-017 GitHub 多 Skill 扫描与导入
管理入口和 CLI 应支持输入公开 GitHub 仓库 URL,扫描仓库中的多个文档型 Skill,
预览后一次批量发布。首期只接受 `https://github.com/{owner}/{repo}` 形式的公开
仓库,不接受任意 host、URL userinfo、查询参数、fragment 或重定向后的非 GitHub
下载源。
扫描前应通过 GitHub API 解析默认分支或显式 ref,并固定为不可变 commit SHA
归档下载、扫描结果和发布结果必须披露 owner、repository、请求 ref 与解析后的
commit。扫描器应把每个 `SKILL.md` 所在目录视为一个独立 Skill 根,并只关联该根
下的 `references/**/*.md`。单个 Skill 的 frontmatter、文档标准化和安全限制继续
遵循 USER-KEY-SKILL-REQ-014。
扫描预览应返回发现数量,以及每个 Skill 的名称、description 摘要、相对根目录、
revision 和 references 数量。超过 owning YAML 声明的仓库归档大小、展开总量、
文件数、单文件大小或 Skill 数量时必须在持久化前拒绝。
批量发布必须在一个 PostgreSQL 事务中完成。同名 Skill 按既有发布语义更新;
任意 Skill 校验或持久化失败时整批回滚。成功后只刷新一次 gateway snapshot
并返回逐项公开 URL 与三平台一次性读取命令。
L0 应使用内存多 Skill ZIP 验证多根扫描、references 归属、重复名称拒绝和容量
限制,不启动 API、worker 或网络请求。L1 应通过正式 API 与 Temporal worker
使用 `https://github.com/RT-Thread/rtthread-skills` 验证真实扫描和批量发布;
Web 应提供 GitHub URL 输入、扫描预览和明确的批量发布动作。
+33 -6
View File
@@ -12,7 +12,7 @@ interface SmokePage {
path: string;
readySelector: string;
screenshotName: string;
interaction: "none" | "api-key-bindings" | "history-detail";
interaction: "none" | "github-scan" | "api-key-bindings" | "history-detail";
}
interface SmokeProfile {
@@ -225,7 +225,32 @@ export default async function superApiManagementSmoke({ page, goto, wait, screen
const interactionShot = await screenshot("superapi-api-key-bindings.png");
screenshots.push(interactionShot);
interactions.push({ id: item.interaction, ok, ...bindingEvidence, screenshot: interactionShot });
await dialog.getByRole("button", { name: "关闭" }).click();
await dialog.getByLabel("关闭", { exact: true }).click();
}
if (item.interaction === "github-scan") {
await page.getByRole("button", { name: "从 GitHub 导入" }).click();
const dialog = page.getByRole("dialog", { name: "从 GitHub 批量导入 Skill" });
await dialog.waitFor({ state: "visible", timeout: config.navigationTimeoutMs });
await dialog.getByRole("button", { name: "扫描仓库" }).click();
await dialog.locator(".github-skill-list > div").first().waitFor({ state: "visible", timeout: config.navigationTimeoutMs });
const scanEvidence = await dialog.evaluate((root) => {
const rows = Array.from(root.querySelectorAll(".github-skill-list > div"));
const names = rows.map((row) => row.querySelector("strong")?.textContent?.trim() || "");
const commit = root.querySelector(".github-scan > header code")?.textContent?.trim() || "";
return { count: rows.length, names, commit };
});
const expectedNames = [
"rtthread-create-bsp", "rtthread-create-pkg", "rtthread-env",
"rtthread-git-commit", "rtthread-git-rebase", "rtthread-review", "rtthread-simplify",
];
const ok = scanEvidence.count === 7
&& scanEvidence.commit.length === 40
&& expectedNames.every((name) => scanEvidence.names.includes(name));
if (!ok) addFailure("GitHub Skill 扫描结果不完整");
const interactionShot = await screenshot("superapi-github-skill-scan.png");
screenshots.push(interactionShot);
interactions.push({ id: item.interaction, ok, ...scanEvidence, screenshot: interactionShot });
await dialog.locator("footer.modal-actions").getByRole("button", { name: "关闭", exact: true }).click();
}
if (item.interaction === "history-detail" && dom.rowCount > 0) {
await page.locator(item.readySelector + " tbody tr").first().click();
@@ -281,12 +306,14 @@ export default async function superApiManagementSmoke({ page, goto, wait, screen
readableRestored,
screenshot: interactionShot,
});
await dialog.getByRole("button", { name: "关闭" }).click();
await dialog.getByLabel("关闭", { exact: true }).click();
}
pages.push({ id: item.id, path: new URL(page.url()).pathname, dom, screenshot: shot });
recordStep("读取 SuperAPI " + item.id + " 页面", { ok: dom.pageId === item.id && !dom.error, path: new URL(page.url()).pathname, rowCount: dom.rowCount });
}
const mutatingRequests = network.filter((item) => !["GET", "HEAD"].includes(item.method));
const mutatingRequests = network.filter((item) =>
!["GET", "HEAD"].includes(item.method)
&& !(item.method === "POST" && item.path === "/api/admin/skills/github/scan"));
if (mutatingRequests.length > 0) addFailure("smoke 观察到管理写请求");
const evidence = {
profileName: config.profileName,
@@ -386,8 +413,8 @@ function smokeProfile(value: unknown, name: string): SmokeProfile {
function interaction(value: unknown): SmokePage["interaction"] {
if (value === undefined) return "none";
if (value === "api-key-bindings" || value === "history-detail") return value;
throw new Error("page.interaction 必须是 api-key-bindings 或 history-detail");
if (value === "github-scan" || value === "api-key-bindings" || value === "history-detail") return value;
throw new Error("page.interaction 必须是 github-scan、api-key-bindings 或 history-detail");
}
function record(value: unknown, name: string): Record<string, unknown> {