feat(catalog): 将官方模型目录迁入公开Git仓库
Test deployment public URL (Linux) / test-linux (push) Canceled after 0s
Test start.ps1 (Windows) / test-windows (push) Canceled after 0s
Test deployment public URL (Linux) / test-linux (pull_request) Canceled after 0s
Test start.ps1 (Windows) / test-windows (pull_request) Canceled after 0s

This commit is contained in:
2026-10-15 11:53:42 +08:00
parent dad460581e
commit 9547a027a2
15 changed files with 30405 additions and 0 deletions
+197
View File
@@ -0,0 +1,197 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import test from "node:test";
import {
assertNoMissingModels,
buildRelease,
checkArtifacts,
prepareArtifacts,
publicBaseUrl,
validateSource,
} from "./catalog.mjs";
const directory = path.dirname(new URL(import.meta.url).pathname);
const source = JSON.parse(
fs.readFileSync(path.join(directory, "catalog.source.json"), "utf8"),
);
function fixture(t) {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "easyai-git-catalog-"));
t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
const data = structuredClone(source);
data.baseModels = data.baseModels.slice(0, 2);
const save = () =>
fs.writeFileSync(
path.join(dir, "catalog.source.json"),
JSON.stringify(data, null, 2) + "\n",
);
save();
return { dir, data, save };
}
test("唯一维护源通过完整 Schema 和规范格式校验", () => {
const data = validateSource();
assert.ok(data.baseModels.length >= 220);
assert.equal(data.providers.length, 27);
});
test("Nano Banana 2.1 保留唯一原身份和图像能力", () => {
const rows = source.baseModels.filter(
(model) => model.defaultRealModelName === "gemini-nano-banana-2.1",
);
assert.equal(rows.length, 1);
assert.equal(
rows[0].catalogKey,
"base:runtime:google-gemini:Nano Banana 2.1:gemini-nano-banana-2.1",
);
assert.equal(rows[0].alias, "Nano Banana 2.1");
assert.equal(rows[0].status, "active");
assert.deepEqual(rows[0].modelTypes, ["image_generate", "image_edit"]);
assert.deepEqual(rows[0].capabilities.image_generate.output_resolutions, [
"1K",
"2K",
"4K",
]);
assert.equal(rows[0].capabilities.image_edit.input_max_images_count, 14);
});
test("重复模型身份拒绝保存", (t) => {
const { dir, data, save } = fixture(t);
data.baseModels.push(structuredClone(data.baseModels[0]));
save();
assert.throws(
() => validateSource(path.join(dir, "catalog.source.json")),
/重复/,
);
});
test("能力字段拼写和类型受 Schema 约束", (t) => {
const { dir, data, save } = fixture(t);
const type = data.baseModels[0].modelTypes[0];
data.baseModels[0].capabilities[type].misspelledCapability = true;
save();
assert.throws(
() => validateSource(path.join(dir, "catalog.source.json")),
/additional properties/,
);
});
test("准备版本时同步生成公开指针和校验和匹配的快照", (t) => {
const { dir } = fixture(t);
const { pointer } = prepareArtifacts(dir, { version: "test-v1" });
assert.equal(
pointer.catalogUrl,
publicBaseUrl + "/releases/test-v1/catalog.json",
);
assert.equal(checkArtifacts(dir).version, "test-v1");
});
test("只改维护源而未重新生成快照会被检查拒绝", (t) => {
const { dir, data, save } = fixture(t);
prepareArtifacts(dir, { version: "test-v1" });
data.baseModels[0].displayName = "修改后的名称";
save();
assert.throws(() => checkArtifacts(dir), /不一致/);
});
test("篡改版本快照会被拒绝", (t) => {
const { dir } = fixture(t);
prepareArtifacts(dir, { version: "test-v1" });
const file = path.join(dir, "releases/test-v1/catalog.json");
const release = JSON.parse(fs.readFileSync(file));
release.baseModels[0].displayName = "篡改";
fs.writeFileSync(file, JSON.stringify(release));
assert.throws(() => checkArtifacts(dir), /不一致/);
});
test("遗漏已发布模型默认拒绝,正常下线保留原身份", (t) => {
const { data } = fixture(t);
const previous = buildRelease(data, { version: "test-v1" });
const reduced = structuredClone(data);
reduced.baseModels.pop();
assert.throws(
() => assertNoMissingModels(previous, reduced),
/遗漏已发布模型/,
);
data.baseModels[0].status = "offline";
assert.deepEqual(assertNoMissingModels(previous, data), []);
});
test("物理移除必须同时声明允许和原因,并记录在新版本中", (t) => {
const { dir, data, save } = fixture(t);
const { release: previous } = prepareArtifacts(dir, { version: "test-v1" });
data.baseModels.pop();
save();
assert.throws(
() => prepareArtifacts(dir, { previous, allowRemovals: true }),
/遗漏/,
);
assert.throws(
() => prepareArtifacts(dir, { previous, removalReason: "迁移" }),
/遗漏/,
);
const { release } = prepareArtifacts(dir, {
previous,
version: "test-v2",
allowRemovals: true,
removalReason: "明确移除",
});
assert.equal(release.removals.models.length, 1);
assert.equal(release.removals.reason, "明确移除");
assert.equal(checkArtifacts(dir).version, "test-v2");
});
test("历史快照不能覆盖且版本不能包含目录穿越", (t) => {
const { dir } = fixture(t);
prepareArtifacts(dir, { version: "test-v1" });
assert.throws(
() => prepareArtifacts(dir, { version: "test-v1" }),
/不能覆盖/,
);
assert.throws(
() => prepareArtifacts(dir, { version: "../outside" }),
/版本格式/,
);
});
test("公开指针不能重新指向原 CDN 或其他仓库", (t) => {
const { dir } = fixture(t);
prepareArtifacts(dir, { version: "test-v1" });
const file = path.join(dir, "latest.json");
const pointer = JSON.parse(fs.readFileSync(file));
pointer.catalogUrl =
"https://aliyuncdn.51easyai.com/model-catalog/official/v1/catalog.json";
fs.writeFileSync(file, JSON.stringify(pointer));
assert.throws(() => checkArtifacts(dir), /easyai 仓库公开地址/);
});
test("恢复 Git 历史定义后生成新回退版本,保留全部快照", (t) => {
const { dir, data, save } = fixture(t);
const original = structuredClone(data);
const first = prepareArtifacts(dir, { version: "test-v1" });
data.baseModels[0].displayName = "新版";
save();
const second = prepareArtifacts(dir, {
previous: first.release,
version: "test-v2",
});
fs.writeFileSync(
path.join(dir, "catalog.source.json"),
JSON.stringify(original, null, 2) + "\n",
);
const third = prepareArtifacts(dir, {
previous: second.release,
version: "test-v3",
note: "恢复 test-v1 的定义",
});
assert.equal(third.release.previousVersion, "test-v2");
assert.equal(third.release.checksum, first.release.checksum);
assert.equal(checkArtifacts(dir).version, "test-v3");
assert.deepEqual(fs.readdirSync(path.join(dir, "releases")).sort(), [
"test-v1",
"test-v2",
"test-v3",
]);
});