Compare commits

17 Commits
Author SHA1 Message Date
easyai03 895d92317e chore(release): 将3.30.2.20261017提升为稳定版 2026-10-17 09:56:57 +08:00
easyai03 be086ab2cc chore(release): 登记 3.30.2.20261017 固定版本产物 2026-10-17 03:20:50 +08:00
easyai03 e15fb6b0f1 chore(release): 将成功结果恢复修复提升为官方稳定版 2026-10-16 11:23:53 +08:00
easyai03 2b15588caf chore(release): 记录成功结果恢复修复的官方固定镜像 2026-10-16 11:09:28 +08:00
easyai03 4df7625052 chore(deploy): 补齐前后端固定镜像测试入口
合入已审查并通过定向验证的剩余改动,保留当前主线与已上线修复。
2026-10-16 09:36:55 +08:00
easyai03 1f12419742 chore(deploy): 集成定向测试入口并同步主线
Test deployment public URL (Linux) / test-linux (push) Canceled after 0s
Test start.ps1 (Windows) / test-windows (push) Canceled after 0s
2026-10-16 09:34:44 +08:00
easyai03 24d7efbb1b chore(release): 登记 3.30.0.20261016 固定版本产物 2026-10-16 03:29:42 +08:00
wangbo 9a55fcfe5b chore(release): 合并结果恢复与转存修复发布清单
纳入已验收固定版本及正式发布元数据。
2026-10-15 20:27:13 +08:00
wangbo 8d60d463ec chore(release): 登记结果恢复与转存修复固定版本 2026-10-15 20:25:30 +08:00
wangbo 3641e91346 feat(catalog): 将官方模型目录迁入公开Git仓库 (#11)
Test deployment public URL (Linux) / test-linux (push) Canceled after 0s
Test start.ps1 (Windows) / test-windows (push) Canceled after 0s
合入已通过定向验收的官方模型目录 Git 维护与消费迁移。
2026-10-15 11:57:20 +08:00
wangbo 9547a027a2 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
2026-10-15 11:53:42 +08:00
easyai03 dad460581e chore(release): 将 3.29.0.20261015 提升为稳定版 2026-10-15 09:56:52 +08:00
easyai03 e78c183888 chore(release): 记录 3.29.0.20261015 固定版本产物 2026-10-15 03:26:33 +08:00
easyai03 dbb6bbce02 fix(release): 按服务核验 Compose 固定镜像并保留线上修复 2026-10-15 03:09:51 +08:00
wangbo 2db18f5804 chore(deploy): 补充前端正式产物的测试验收入口 2026-10-14 12:09:37 +08:00
wangbo ef471e2106 chore(deploy): 提供保留现有配置的后端测试镜像入口 2026-10-14 11:43:55 +08:00
wangbo bf0ac6db54 fix(deploy): 按服务读取镜像定义避免依赖镜像误判 2026-10-14 11:24:13 +08:00
27 changed files with 30853 additions and 24 deletions
+4
View File
@@ -746,3 +746,7 @@ apt remove docker-compose
```bash ```bash
cd ~/easyai && cp docker-compose.yml docker-compose.yml.backup && wget -q https://git.51easyai.com/wangbo/easyai/raw/main/docker-compose.yml -O docker-compose.yml && echo "✅ docker-compose.yml 更新完成" cd ~/easyai && cp docker-compose.yml docker-compose.yml.backup && wget -q https://git.51easyai.com/wangbo/easyai/raw/main/docker-compose.yml -O docker-compose.yml && echo "✅ docker-compose.yml 更新完成"
``` ```
## 官方模型目录
官方基础模型定义由本仓库独立维护,无需重建后端即可通过 Git 发布。维护源、公开地址、版本准备、校验和回退流程见 [官方模型目录](model-catalog/official/README.md)。平台后端和独立 AI Gateway 只读消费;不再使用原 OSS/CDN 目录发布流程。
+5
View File
@@ -0,0 +1,5 @@
# 自有测试环境的后端候选验收。追加到既有 COMPOSE_FILE,仅选择主后端镜像。
# EASYAI_TEST_SERVER_IMAGE 必须为已发布并核验的完整版本镜像。
services:
easyai-server:
image: ${EASYAI_TEST_SERVER_IMAGE:?EASYAI_TEST_SERVER_IMAGE is required}
+5
View File
@@ -0,0 +1,5 @@
# 自有测试环境与已发布前端产物配对验收,仅选择前端镜像。
# 不参与生产或客户的默认更新配置。
services:
easyai-web:
image: ${EASYAI_TEST_WEB_IMAGE:?EASYAI_TEST_WEB_IMAGE is required}
+2
View File
@@ -88,6 +88,8 @@ services:
environment: environment:
# Node.js V8 old-space 堆上限(MiB),可在 .env 中覆盖 # Node.js V8 old-space 堆上限(MiB),可在 .env 中覆盖
- NODE_OPTIONS=--max-old-space-size=${NODE_MAX_OLD_SPACE_SIZE:-8192} - NODE_OPTIONS=--max-old-space-size=${NODE_MAX_OLD_SPACE_SIZE:-8192}
# 官方目录由公开 Git 仓库维护,模型数据更新无需重新构建服务镜像。
- CONFIG_OFFICIAL_MODEL_CATALOG_URL=${CONFIG_OFFICIAL_MODEL_CATALOG_URL:-https://git.51easyai.com/wangbo/easyai/raw/branch/main/model-catalog/official/latest.json}
# MongoDB配置,用户管理,不会配置的话保持默认即可 # MongoDB配置,用户管理,不会配置的话保持默认即可
- CONFIG_DB_MONGO_URI=mongodb://172.21.0.3:27017 - CONFIG_DB_MONGO_URI=mongodb://172.21.0.3:27017
- CONFIG_DB_MONGO_USERNAME=${MONGO_INITDB_ROOT_USERNAME} - CONFIG_DB_MONGO_USERNAME=${MONGO_INITDB_ROOT_USERNAME}
+1
View File
@@ -0,0 +1 @@
node_modules/
+9
View File
@@ -0,0 +1,9 @@
# 目录维护与消费验收
1. 执行 `yarn format`、`yarn validate`、`yarn test`、`yarn prepare-release --note "变更说明"`、`yarn run check`。
2. 对照当前公开版本逐条核对模型身份和定义,确认新增、修改、下线范围;未知缺失必须停止。
3. PR 合入 `main` 后,免登录读取公开维护源、`latest.json` 及指针中的版本快照;重新计算摘要并核对 Git 提交内容。
4. 使用平台真实 HTTP 和隔离 MongoDB 验证同步、重复同步、恢复停用模型、本地覆盖、主动停用、隐藏、绑定和价格保留。
5. 独立网关用真实公开 HTTP 验证指针、同源 HTTPS、内容摘要和模型解析;自定义源仍可配置。
6. 回退测试恢复历史定义并生成新版本,保留所有已有快照与 Git 历史。
7. 普通应用发布不修改模型定义。迁移不删除客户绑定、价格、权限,也不自动发起付费模型任务。
+8
View File
@@ -0,0 +1,8 @@
# 官方模型目录维护规则
- 唯一维护源为本目录 `catalog.source.json`。平台后端和独立 AI Gateway 都是只读消费者,不在应用仓库另存维护源。
- 内容和版本指针通过 Git 提交发布,公开地址见 README。禁止使用旧 OSS/CDN 上传流程;应用镜像发布不自动授权修改模型定义或生命周期。
- 变更前同步最新主线,检查新增、修改和下线差异;执行格式、Schema、测试和版本一致性检查,将维护源、版本快照和 latest.json 一并提交。
- 恢复历史定义时生成新的回退版本;不覆盖历史快照,不强推改写 Git 历史。
- 正常下线保留原目录身份并设置 offline;缺失模型默认拒绝准备版本。
- 不将客户绑定、价格、权限、账号或密钥放入公开目录。
+50
View File
@@ -0,0 +1,50 @@
# EasyAI 官方模型目录
本目录是官方基础模型定义的唯一维护位置。平台后端与独立 AI Gateway 只读消费,不另存维护源。目录内容不包含客户密钥、账号、平台绑定、价格、权限或运行策略。
- 维护 JSON:`catalog.source.json`
- 公开维护源:[catalog.source.json](https://git.51easyai.com/wangbo/easyai/raw/branch/main/model-catalog/official/catalog.source.json)
- 消费端入口:[latest.json](https://git.51easyai.com/wangbo/easyai/raw/branch/main/model-catalog/official/latest.json)
- Git 历史:[目录提交记录](https://git.51easyai.com/wangbo/easyai/commits/branch/main/model-catalog/official)
公开地址免登录。只有进入 `main` 的 Git 内容才会对外生效;未提交文件、临时 JSON 和普通分支不会替换正式目录。旧 OSS/CDN 上传流程已停用。
## 修改和发布
使用 Node 22.22.0、Yarn 1.22.22,在最新主线创建工作分支:
```bash
cd model-catalog/official
yarn install --frozen-lockfile --ignore-scripts
# 编辑 catalog.source.json
yarn format
yarn validate
yarn test
yarn prepare-release --note "本次模型变更说明"
yarn run check
git diff
```
`prepare-release` 只生成 Git 文件,不上传、不推送,也不切换任何线上对象。它会对照当前 Git 提交的上一版目录,默认拒绝缺少已有模型的候选。新增、能力修改和下线分别审查;正常下线保留原 `catalogKey`,将 `status` 设为 `offline`,不要通过删记录表达退役。确需物理移除时,必须同时声明 `--allow-removals --removal-reason "具体原因"`,原因及移除项会进入版本快照。
确认差异后,将维护源、`latest.json` 和本次 `releases/<版本>/catalog.json` 一起提交,经 PR 合入 `main`:
```bash
git add catalog.source.json latest.json releases/
git commit -m 'feat(catalog): 说明本次模型变更'
git push -u origin HEAD
```
平台后端配置项为 `CONFIG_OFFICIAL_MODEL_CATALOG_URL`,独立网关为 `AI_GATEWAY_OFFICIAL_MODEL_CATALOG_URL`,均使用上面的 `latest.json` 地址。目录同步后还须验收模型状态、身份和本地绑定、价格、权限不变;收录模型不代表上游账号已经可用。
## 回退
使用 `git log -- catalog.source.json` 查看历史,恢复选定提交中的定义,再准备一个新版本:
```bash
git restore --source=<历史提交> -- catalog.source.json
yarn prepare-release --note "恢复指定历史提交的模型定义"
yarn run check
```
审查差异、测试、提交并合入 `main` 后生效。回退也必须审查模型移除;历史中没有的新模型不会被静默删除。保留所有历史版本快照,不覆盖旧文件,不强推或重置公开分支。Git 提交记录用于确定修改人、时间、评审和内容差异;版本快照记录内容摘要及上一版本。
+397
View File
@@ -0,0 +1,397 @@
import crypto from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import process from "node:process";
import { execFileSync } from "node:child_process";
import { createRequire } from "node:module";
import { fileURLToPath } from "node:url";
const require = createRequire(import.meta.url);
const Ajv = require("ajv");
const directory = path.dirname(fileURLToPath(import.meta.url));
const repository = path.resolve(directory, "../..");
const defaultSourcePath = path.join(directory, "catalog.source.json");
const schemaPath = path.join(directory, "catalog.schema.json");
export const publicBaseUrl =
"https://git.51easyai.com/wangbo/easyai/raw/branch/main/model-catalog/official";
const args = process.argv.slice(2);
const option = (name) => {
const index = args.indexOf("--" + name);
return index < 0 ? "" : String(args[index + 1] || "");
};
const stableValue = (value) => {
if (Array.isArray(value)) return value.map(stableValue);
if (value && typeof value === "object") {
return Object.keys(value)
.sort()
.reduce((result, key) => {
result[key] = stableValue(value[key]);
return result;
}, {});
}
return value;
};
const checksum = (payload) =>
crypto
.createHash("sha256")
.update(JSON.stringify(stableValue(payload)))
.digest("hex");
const readJson = (file) => JSON.parse(fs.readFileSync(file, "utf8"));
const serializeJson = (value) => `${JSON.stringify(value, null, 2)}\n`;
function canonicalizeSource(source) {
return {
...source,
baseModels: source.baseModels.map((model) => {
const current = model.capabilities || {};
const capabilityKeys = [
...model.modelTypes,
...Object.keys(current).filter(
(key) => !model.modelTypes.includes(key),
),
];
const capabilities = Object.fromEntries(
capabilityKeys.map((type) => {
const { type: _ignoredType, ...config } = current[type] || {};
return [type, { type, ...config }];
}),
);
return { ...model, capabilities };
}),
};
}
function assertOrderedRanges(value, pathParts = []) {
if (Array.isArray(value)) {
const field = pathParts.at(-1) || "";
if (
(field.endsWith("_range") || field === "thinkingBudgetTokensRange") &&
value.length === 2 &&
value.every((item) => typeof item === "number") &&
value[0] > value[1]
) {
throw new Error(`范围上下限颠倒: ${pathParts.join(".")}`);
}
value.forEach((item, index) =>
assertOrderedRanges(item, [...pathParts, String(index)]),
);
return;
}
if (!value || typeof value !== "object") return;
if (
typeof value.min === "number" &&
typeof value.max === "number" &&
value.min > value.max
) {
throw new Error(`范围上下限颠倒: ${pathParts.join(".")}`);
}
Object.entries(value).forEach(([key, item]) =>
assertOrderedRanges(item, [...pathParts, key]),
);
}
export function validateSource(file = defaultSourcePath) {
const sourceText = fs.readFileSync(file, "utf8");
const source = JSON.parse(sourceText);
const ajv = new Ajv({ allErrors: true, strict: false });
const validate = ajv.compile(readJson(schemaPath));
if (!validate(source)) {
throw new Error(
`官方目录格式无效: ${ajv.errorsText(validate.errors, { separator: "; " })}`,
);
}
const providerCodes = new Set(source.providers.map((item) => item.code));
if (providerCodes.size !== source.providers.length) {
throw new Error("官方目录包含重复厂商 code");
}
const keys = new Set();
const identities = new Set();
for (const model of source.baseModels) {
if (keys.has(model.catalogKey))
throw new Error(`重复 catalogKey: ${model.catalogKey}`);
keys.add(model.catalogKey);
const identity = `${model.alias}\u0000${model.defaultRealModelName}`;
if (identities.has(identity))
throw new Error(
`重复模型身份: ${model.alias} / ${model.defaultRealModelName}`,
);
identities.add(identity);
if (model.providerCode && !providerCodes.has(model.providerCode)) {
throw new Error(
`模型 ${model.catalogKey} 引用了不存在的厂商 ${model.providerCode}`,
);
}
const capabilityKeys = Object.keys(model.capabilities);
const missingCapabilities = model.modelTypes.filter(
(type) => !capabilityKeys.includes(type),
);
const extraCapabilities = capabilityKeys.filter(
(type) => !model.modelTypes.includes(type),
);
if (missingCapabilities.length || extraCapabilities.length) {
throw new Error(
`模型 ${model.catalogKey} 的 modelTypes 与 capabilities 不一致` +
`(缺少: ${missingCapabilities.join(", ") || "无"};` +
`多余: ${extraCapabilities.join(", ") || "无"})`,
);
}
for (const [type, capability] of Object.entries(model.capabilities)) {
if (capability.type !== type) {
throw new Error(
`模型 ${model.catalogKey} 的能力 ${type}.type 必须等于 ${type}`,
);
}
}
const textToSpeech = model.capabilities.text_to_speech;
if (textToSpeech) {
const voices = Array.isArray(textToSpeech.voices)
? textToSpeech.voices
: [];
const voiceIds = voices.map((voice) => voice.id);
if (new Set(voiceIds).size !== voiceIds.length) {
throw new Error(`模型 ${model.catalogKey} 包含重复音色 ID`);
}
if (
textToSpeech.default_voice_id &&
!voiceIds.includes(textToSpeech.default_voice_id)
) {
throw new Error(
`模型 ${model.catalogKey} 的默认音色不在 voices 中: ${textToSpeech.default_voice_id}`,
);
}
}
if (model.catalogKey.startsWith("base:seed:")) {
if (!model.seedKey)
throw new Error(`种子模型 ${model.catalogKey} 必须包含 seedKey`);
} else if (model.seedKey !== undefined) {
throw new Error(`运行时模型 ${model.catalogKey} 不应包含 seedKey`);
}
assertOrderedRanges(model.capabilities, [model.catalogKey, "capabilities"]);
}
const canonicalText = serializeJson(canonicalizeSource(source));
if (sourceText !== canonicalText) {
throw new Error("官方目录格式不规范,请运行 yarn format 后再提交");
}
return source;
}
export function buildRelease(
source,
{ version, note = "", previousVersion = null } = {},
) {
const publishedAt = new Date().toISOString();
const selectedVersion = version || publishedAt.replace(/[-:.]/g, "");
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,95}$/.test(selectedVersion))
throw new Error("目录版本格式无效");
const payload = {
schemaVersion: source.schemaVersion,
providers: source.providers,
baseModels: source.baseModels,
providerModelPresets: [],
};
return {
version: selectedVersion,
publishedAt,
generatedAt: publishedAt,
...payload,
checksum: checksum(payload),
count: source.baseModels.length,
ttlSeconds: 21600,
previousVersion,
sourceRepository: "https://git.51easyai.com/wangbo/easyai",
sourcePath: "model-catalog/official/catalog.source.json",
...(note ? { note } : {}),
};
}
export function assertNoMissingModels(
previous,
source,
{ allowRemovals = false, removalReason = "" } = {},
) {
const keys = new Set(source.baseModels.map((model) => model.catalogKey));
const removed = (previous?.baseModels || [])
.filter((model) => !keys.has(model.catalogKey))
.map((model) => ({ catalogKey: model.catalogKey, alias: model.alias }));
if (removed.length && (!allowRemovals || !removalReason.trim())) {
throw new Error(
`维护源遗漏已发布模型:${JSON.stringify(removed)}。下线请保留原身份并设置 status=offline;确需移除时须声明 --allow-removals --removal-reason。`,
);
}
return removed;
}
export function prepareArtifacts(targetDirectory = directory, options = {}) {
const source = validateSource(
path.join(targetDirectory, "catalog.source.json"),
);
const removed = assertNoMissingModels(options.previous, source, options);
const release = buildRelease(source, {
...options,
previousVersion: options.previous?.version || null,
});
if (removed.length)
release.removals = {
models: removed,
reason: options.removalReason.trim(),
};
const releasePath = path.join(
targetDirectory,
"releases",
release.version,
"catalog.json",
);
if (fs.existsSync(releasePath))
throw new Error("该版本快照已存在,请生成新版本,不能覆盖历史文件");
const pointer = {
schemaVersion: 1,
version: release.version,
checksum: release.checksum,
publishedAt: release.publishedAt,
catalogUrl: `${publicBaseUrl}/releases/${release.version}/catalog.json`,
};
fs.mkdirSync(path.dirname(releasePath), { recursive: true });
fs.writeFileSync(releasePath, serializeJson(release));
fs.writeFileSync(
path.join(targetDirectory, "latest.json"),
serializeJson(pointer),
);
return { pointer, release };
}
export function checkArtifacts(targetDirectory = directory) {
const source = validateSource(
path.join(targetDirectory, "catalog.source.json"),
);
const pointer = readJson(path.join(targetDirectory, "latest.json"));
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,95}$/.test(pointer.version))
throw new Error("目录版本格式无效");
const release = readJson(
path.join(targetDirectory, "releases", pointer.version, "catalog.json"),
);
const payload = {
schemaVersion: source.schemaVersion,
providers: source.providers,
baseModels: source.baseModels,
providerModelPresets: [],
};
if (
pointer.version !== release.version ||
pointer.checksum !== release.checksum ||
release.checksum !== checksum(payload) ||
release.checksum !==
checksum({
schemaVersion: release.schemaVersion,
providers: release.providers,
baseModels: release.baseModels,
providerModelPresets: release.providerModelPresets || [],
})
) {
throw new Error(
"维护源、最新指针和版本快照不一致,请重新准备版本并一并提交",
);
}
if (
pointer.catalogUrl !==
`${publicBaseUrl}/releases/${release.version}/catalog.json`
)
throw new Error("目录快照必须指向 easyai 仓库公开地址");
return {
version: release.version,
checksum: release.checksum,
baseModelCount: release.baseModels.length,
catalogUrl: pointer.catalogUrl,
};
}
function previousCommittedRelease() {
const git = (file) =>
JSON.parse(
execFileSync("git", ["show", `HEAD:model-catalog/official/${file}`], {
cwd: repository,
encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"],
maxBuffer: 4 * 1024 * 1024,
}),
);
let pointer;
try {
pointer = git("latest.json");
} catch (error) {
// A missing pointer is only allowed for this repository's first catalog commit.
try {
execFileSync(
"git",
["cat-file", "-e", "HEAD:model-catalog/official/latest.json"],
{ cwd: repository, stdio: "ignore" },
);
} catch {
return null;
}
throw error;
}
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,95}$/.test(pointer.version))
throw new Error("已提交目录版本格式无效");
return git(`releases/${pointer.version}/catalog.json`);
}
function main() {
const command = args[0] || "validate";
if (command === "format") {
fs.writeFileSync(
defaultSourcePath,
serializeJson(canonicalizeSource(readJson(defaultSourcePath))),
);
validateSource();
console.log(JSON.stringify({ action: "formatted" }));
} else if (command === "validate") {
const source = validateSource();
console.log(
JSON.stringify({
action: "validated",
baseModelCount: source.baseModels.length,
providerCount: source.providers.length,
}),
);
} else if (command === "prepare") {
const result = prepareArtifacts(directory, {
previous: previousCommittedRelease(),
version: option("version") || undefined,
note: option("note"),
allowRemovals: args.includes("--allow-removals"),
removalReason: option("removal-reason"),
});
console.log(
JSON.stringify(
{
action: "prepared-for-git-commit",
version: result.pointer.version,
checksum: result.pointer.checksum,
baseModelCount: result.release.count,
instruction:
"检查 Git 差异后,将维护源、latest.json 和版本快照一并提交并推送;公开 JSON 只来自 Git 已提交内容。",
},
null,
2,
),
);
} else if (command === "check") {
console.log(JSON.stringify(checkArtifacts(), null, 2));
} else {
throw new Error(
"支持 format、validate、prepare、check;目录通过 Git 提交发布,不再使用 OSS/CDN 上传命令。",
);
}
}
if (
process.argv[1] &&
path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)
) {
try {
main();
} catch (error) {
console.error(error.message);
process.exitCode = 1;
}
}
+846
View File
@@ -0,0 +1,846 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "EasyAI Official Base Model Catalog Source",
"type": "object",
"required": ["schemaVersion", "providers", "baseModels"],
"additionalProperties": false,
"properties": {
"schemaVersion": { "const": 1 },
"providers": {
"type": "array",
"minItems": 1,
"items": { "$ref": "#/definitions/provider" }
},
"baseModels": {
"type": "array",
"minItems": 1,
"items": { "$ref": "#/definitions/baseModel" }
}
},
"definitions": {
"nonEmptyString": { "type": "string", "minLength": 1 },
"nonNegativeInteger": { "type": "integer", "minimum": 0 },
"positiveInteger": { "type": "integer", "minimum": 1 },
"stringList": {
"type": "array",
"items": { "type": "string", "minLength": 1 },
"uniqueItems": true
},
"numberList": {
"type": "array",
"items": { "type": "number" }
},
"integerList": {
"type": "array",
"items": { "type": "integer" },
"uniqueItems": true
},
"numberRange": {
"type": "array",
"items": { "type": "number" },
"minItems": 2,
"maxItems": 2
},
"numberRangeOrMap": {
"oneOf": [
{ "$ref": "#/definitions/numberRange" },
{
"type": "object",
"minProperties": 1,
"additionalProperties": { "$ref": "#/definitions/numberRange" }
}
]
},
"numberListOrMap": {
"oneOf": [
{ "$ref": "#/definitions/numberList" },
{
"type": "object",
"minProperties": 1,
"additionalProperties": { "$ref": "#/definitions/numberList" }
}
]
},
"stringListOrMap": {
"oneOf": [
{ "$ref": "#/definitions/stringList" },
{
"type": "object",
"minProperties": 1,
"additionalProperties": { "$ref": "#/definitions/stringList" }
}
]
},
"capabilityName": {
"enum": [
"audio_generate",
"audio_understanding",
"digital_human_generate",
"image_analysis",
"image_edit",
"image_generate",
"image_to_model",
"image_to_video",
"image_vectorize",
"mesh_edit",
"multiview_to_model",
"omni",
"omni_video",
"speech_to_text",
"text_embedding",
"text_generate",
"text_rerank",
"text_to_model",
"text_to_speech",
"tools_call",
"video_enhance",
"video_generate",
"video_understanding",
"voice_clone"
]
},
"provider": {
"type": "object",
"required": ["code", "name", "displayName"],
"properties": {
"code": { "$ref": "#/definitions/nonEmptyString" },
"name": { "$ref": "#/definitions/nonEmptyString" },
"displayName": { "$ref": "#/definitions/nonEmptyString" },
"iconPath": { "$ref": "#/definitions/nonEmptyString" }
},
"additionalProperties": false
},
"baseModel": {
"type": "object",
"required": [
"catalogKey",
"providerCode",
"displayName",
"alias",
"defaultRealModelName",
"modelTypes",
"capabilities",
"status"
],
"properties": {
"catalogKey": {
"type": "string",
"pattern": "^base:(runtime|seed):.+"
},
"seedKey": { "$ref": "#/definitions/nonEmptyString" },
"providerCode": { "$ref": "#/definitions/nonEmptyString" },
"displayName": { "$ref": "#/definitions/nonEmptyString" },
"alias": { "$ref": "#/definitions/nonEmptyString" },
"defaultRealModelName": { "$ref": "#/definitions/nonEmptyString" },
"modelTypes": {
"type": "array",
"minItems": 1,
"uniqueItems": true,
"items": { "$ref": "#/definitions/capabilityName" }
},
"iconPath": { "$ref": "#/definitions/nonEmptyString" },
"description": { "$ref": "#/definitions/nonEmptyString" },
"capabilities": { "$ref": "#/definitions/capabilities" },
"status": { "enum": ["active", "deprecated", "offline"] }
},
"additionalProperties": false
},
"languageCapability": {
"type": "object",
"required": ["type"],
"properties": {
"type": { "$ref": "#/definitions/capabilityName" },
"max_context_tokens": { "$ref": "#/definitions/nonNegativeInteger" },
"max_input_tokens": { "$ref": "#/definitions/nonNegativeInteger" },
"max_output_tokens": { "$ref": "#/definitions/nonNegativeInteger" },
"max_thinking_tokens": { "$ref": "#/definitions/nonNegativeInteger" },
"outputTokenLimitScope": { "enum": ["total", "answer_only"] },
"outputTokenParameter": {
"enum": ["max_tokens", "max_completion_tokens"]
},
"defaultOutputTokenBudget": { "const": "candidate_max" },
"supportThinking": { "type": "boolean" },
"supportThinkingModeSwitch": { "type": "boolean" },
"supportStructuredOutput": { "type": "boolean" },
"supportTool": { "type": "boolean" },
"supportWebSearch": { "type": "boolean" },
"thinkingBudgetTokensRange": { "$ref": "#/definitions/numberRange" },
"thinkingEffortLevels": { "$ref": "#/definitions/stringList" },
"supportedApiProtocols": { "$ref": "#/definitions/stringList" }
},
"additionalProperties": false
},
"textEmbeddingCapability": {
"type": "object",
"required": ["type"],
"properties": {
"type": { "const": "text_embedding" },
"dimensions": {
"type": "array",
"items": { "$ref": "#/definitions/positiveInteger" },
"uniqueItems": true
}
},
"additionalProperties": false
},
"textRerankCapability": {
"type": "object",
"required": ["type"],
"properties": {
"type": { "const": "text_rerank" },
"maxDocuments": { "$ref": "#/definitions/positiveInteger" },
"maxRequestTokens": { "$ref": "#/definitions/positiveInteger" },
"maxTokensPerDocument": { "$ref": "#/definitions/positiveInteger" },
"supportInstruct": { "type": "boolean" },
"supportTopN": { "type": "boolean" }
},
"additionalProperties": false
},
"inputParameter": {
"type": "object",
"required": ["type", "parameter"],
"properties": {
"type": { "$ref": "#/definitions/nonEmptyString" },
"parameter": { "$ref": "#/definitions/nonEmptyString" }
},
"additionalProperties": false
},
"imageCapability": {
"type": "object",
"required": ["type"],
"properties": {
"type": { "enum": ["image_generate", "image_edit"] },
"allow_custom_width_height_size": { "type": "boolean" },
"aspect_ratio_allowed": { "$ref": "#/definitions/stringList" },
"aspect_ratio_range": { "$ref": "#/definitions/numberRange" },
"input_format_allowed": { "$ref": "#/definitions/stringList" },
"input_max_file_size_bytes": {
"$ref": "#/definitions/nonNegativeInteger"
},
"input_max_images_count": {
"$ref": "#/definitions/nonNegativeInteger"
},
"input_multiple_images": { "type": "boolean" },
"input_output_max_images_count": {
"$ref": "#/definitions/nonNegativeInteger"
},
"input_parameters": {
"type": "array",
"items": { "$ref": "#/definitions/inputParameter" }
},
"input_size_range": { "$ref": "#/definitions/stringList" },
"output_max_images_count": {
"$ref": "#/definitions/nonNegativeInteger"
},
"output_max_size": { "$ref": "#/definitions/nonNegativeInteger" },
"output_multiple_images": { "type": "boolean" },
"output_resolutions": { "$ref": "#/definitions/stringList" },
"output_size_range": { "$ref": "#/definitions/numberRange" },
"quality_options": { "$ref": "#/definitions/stringList" },
"size_param_format": { "$ref": "#/definitions/nonEmptyString" },
"support_quality_control": { "type": "boolean" },
"width_height_multiple": { "$ref": "#/definitions/positiveInteger" },
"width_height_range": { "$ref": "#/definitions/numberRange" }
},
"additionalProperties": false
},
"promptLengthLimit": {
"type": "object",
"required": ["max"],
"properties": {
"max": { "$ref": "#/definitions/positiveInteger" },
"count_mode": { "$ref": "#/definitions/nonEmptyString" },
"label": { "$ref": "#/definitions/nonEmptyString" }
},
"additionalProperties": false
},
"resolutionRoute": {
"type": "object",
"required": [
"mode",
"generation_resolution",
"target_resolution",
"display_resolution",
"enhance_model",
"fallback_policy",
"billing_mode"
],
"properties": {
"mode": { "$ref": "#/definitions/nonEmptyString" },
"generation_resolution": { "$ref": "#/definitions/nonEmptyString" },
"target_resolution": { "$ref": "#/definitions/nonEmptyString" },
"display_resolution": { "$ref": "#/definitions/nonEmptyString" },
"enhance_model": { "$ref": "#/definitions/nonEmptyString" },
"fallback_policy": { "$ref": "#/definitions/nonEmptyString" },
"billing_mode": { "$ref": "#/definitions/nonEmptyString" }
},
"additionalProperties": false
},
"resolutionRoutes": {
"type": "object",
"minProperties": 1,
"additionalProperties": { "$ref": "#/definitions/resolutionRoute" }
},
"modeConstraint": {
"type": "object",
"properties": {
"aspect_ratio_allowed": { "$ref": "#/definitions/stringList" },
"forced_aspect_ratio": { "$ref": "#/definitions/nonEmptyString" },
"prompt_required": { "type": "boolean" }
},
"additionalProperties": false
},
"modeConstraints": {
"type": "object",
"minProperties": 1,
"additionalProperties": { "$ref": "#/definitions/modeConstraint" }
},
"mediaConstraint": {
"type": "object",
"properties": {
"formats": { "$ref": "#/definitions/stringList" },
"codecs": { "$ref": "#/definitions/stringList" },
"max_size_mb": { "type": "number", "minimum": 0 },
"max_count": { "$ref": "#/definitions/nonNegativeInteger" },
"min_dimension": { "$ref": "#/definitions/nonNegativeInteger" },
"max_dimension": { "$ref": "#/definitions/nonNegativeInteger" },
"aspect_ratio_range": { "$ref": "#/definitions/numberRange" },
"duration_range": { "$ref": "#/definitions/numberRange" },
"max_total_duration_seconds": { "type": "number", "minimum": 0 },
"fps_range": { "$ref": "#/definitions/numberRange" },
"requires_visual_reference": { "type": "boolean" }
},
"additionalProperties": false
},
"inputMediaConstraints": {
"type": "object",
"required": ["max_request_size_mb"],
"properties": {
"max_request_size_mb": { "type": "number", "minimum": 0 },
"image": { "$ref": "#/definitions/mediaConstraint" },
"video": { "$ref": "#/definitions/mediaConstraint" },
"audio": { "$ref": "#/definitions/mediaConstraint" }
},
"additionalProperties": false
},
"providerOperations": {
"type": "object",
"properties": {
"context_ir": { "type": "boolean" },
"regeneration": {
"type": "object",
"required": [
"source_resolutions",
"target_resolutions",
"requires_audio",
"source_fps"
],
"properties": {
"source_resolutions": { "$ref": "#/definitions/stringList" },
"target_resolutions": { "$ref": "#/definitions/stringList" },
"requires_audio": { "type": "boolean" },
"source_fps": { "type": "number", "minimum": 0 }
},
"additionalProperties": false
}
},
"additionalProperties": false
},
"limitsWithVideo": {
"type": "object",
"properties": {
"max_images_and_elements": {
"$ref": "#/definitions/nonNegativeInteger"
},
"duration_options": { "$ref": "#/definitions/numberList" }
},
"additionalProperties": false
},
"videoDraftCapability": {
"type": "object",
"additionalProperties": false,
"required": [
"enabled",
"contract_version",
"draft_resolution",
"final_resolutions",
"validity_seconds",
"input_modes"
],
"properties": {
"enabled": {
"type": "boolean"
},
"contract_version": {
"enum": [
1,
2
]
},
"draft_resolution": {
"enum": [
"480p",
"720p"
]
},
"final_resolutions": {
"type": "array",
"minItems": 1,
"maxItems": 1,
"items": {
"enum": [
"1080p",
"1440p"
]
}
},
"validity_seconds": {
"type": "integer", "minimum": 0
},
"input_modes": {
"$ref": "#/definitions/stringList"
},
"finalization_protocol": {
"enum": [
"minimax_h3_media_context",
"easyai_result_reference"
]
}
},
"allOf": [
{
"if": {
"properties": {
"contract_version": {
"const": 1
}
}
},
"then": {
"properties": {
"validity_seconds": { "const": 604800 },
"draft_resolution": {
"const": "480p"
},
"final_resolutions": {
"items": {
"const": "1080p"
}
}
}
},
"else": {
"required": [
"finalization_protocol"
],
"properties": {
"draft_resolution": {
"const": "720p"
},
"final_resolutions": {
"items": {
"const": "1440p"
}
}
}
}
}
]
},
"videoCapability": {
"type": "object",
"required": ["type"],
"properties": {
"type": { "enum": ["video_generate", "image_to_video", "omni_video"] },
"draft_mode": { "$ref": "#/definitions/videoDraftCapability" },
"aspect_ratio_allowed": { "$ref": "#/definitions/stringListOrMap" },
"aspect_ratio_range": { "$ref": "#/definitions/numberRange" },
"duration_options": { "$ref": "#/definitions/numberListOrMap" },
"duration_range": { "$ref": "#/definitions/numberRangeOrMap" },
"input_audio": { "type": "boolean" },
"input_first_frame": { "type": "boolean" },
"input_first_last_frame": { "type": "boolean" },
"input_last_frame": { "type": "boolean" },
"input_media_constraints": {
"$ref": "#/definitions/inputMediaConstraints"
},
"input_reference_generate_multiple": { "type": "boolean" },
"input_reference_generate_single": { "type": "boolean" },
"input_size_limit": { "type": "number", "minimum": 0 },
"input_smart_multi_frame": { "type": "boolean" },
"input_width_height_range": { "$ref": "#/definitions/numberRange" },
"limits_with_video": { "$ref": "#/definitions/limitsWithVideo" },
"max_audios": { "$ref": "#/definitions/nonNegativeInteger" },
"max_elements": { "$ref": "#/definitions/nonNegativeInteger" },
"max_file_size_mb": { "type": "number", "minimum": 0 },
"max_images": { "$ref": "#/definitions/nonNegativeInteger" },
"max_images_and_elements": {
"$ref": "#/definitions/nonNegativeInteger"
},
"max_images_for_last_frame": {
"$ref": "#/definitions/nonNegativeInteger"
},
"max_videos": { "$ref": "#/definitions/nonNegativeInteger" },
"mode_constraints": { "$ref": "#/definitions/modeConstraints" },
"output_audio": { "type": "boolean" },
"output_audio_mode": { "$ref": "#/definitions/nonEmptyString" },
"output_bgm": { "type": "boolean" },
"output_resolutions": { "$ref": "#/definitions/stringListOrMap" },
"output_support_return_last_frame": { "type": "boolean" },
"output_video_continuation": { "type": "boolean" },
"prompt_length_limit": { "$ref": "#/definitions/promptLengthLimit" },
"provider_operations": { "$ref": "#/definitions/providerOperations" },
"resolution_routes": { "$ref": "#/definitions/resolutionRoutes" },
"size_allowed": { "$ref": "#/definitions/stringListOrMap" },
"smart_multi_frame_mode": { "$ref": "#/definitions/nonEmptyString" },
"smart_multi_frame_range": { "$ref": "#/definitions/numberRange" },
"support_instruction_edit": { "type": "boolean" },
"support_video_effect_template": { "type": "boolean" },
"support_video_effect_template_list": {
"$ref": "#/definitions/stringList"
},
"supported_modes": { "$ref": "#/definitions/stringList" },
"supports_portrait_asset_reference": { "type": "boolean" },
"supportWebSearch": { "type": "boolean" }
},
"additionalProperties": false
},
"faceLimitRange": {
"type": "object",
"required": ["min", "max"],
"properties": {
"min": { "$ref": "#/definitions/nonNegativeInteger" },
"max": { "$ref": "#/definitions/nonNegativeInteger" }
},
"additionalProperties": false
},
"faceLimitByGeometryQuality": {
"type": "object",
"minProperties": 1,
"additionalProperties": {
"type": "object",
"properties": {
"triangle": { "$ref": "#/definitions/nonNegativeInteger" },
"quad": { "$ref": "#/definitions/nonNegativeInteger" }
},
"additionalProperties": false
}
},
"faceLimitRangeByTopology": {
"type": "object",
"minProperties": 1,
"additionalProperties": { "$ref": "#/definitions/faceLimitRange" }
},
"crossProviderEdit": {
"type": "object",
"properties": {
"tripo3d_import_supported": { "type": "boolean" }
},
"additionalProperties": false
},
"multiviewInput": {
"type": "object",
"required": ["min_images", "max_images", "views"],
"properties": {
"min_images": { "$ref": "#/definitions/nonNegativeInteger" },
"max_images": { "$ref": "#/definitions/nonNegativeInteger" },
"views": {
"type": "array",
"items": {
"type": "object",
"required": ["type", "order"],
"properties": {
"type": { "$ref": "#/definitions/nonEmptyString" },
"required": { "type": "boolean" },
"order": { "$ref": "#/definitions/nonNegativeInteger" }
},
"additionalProperties": false
}
},
"file_constraints": {
"type": "object",
"properties": {
"allowed_mime_types": { "$ref": "#/definitions/stringList" },
"min_side": { "$ref": "#/definitions/nonNegativeInteger" },
"max_side": { "$ref": "#/definitions/nonNegativeInteger" },
"per_file_max_bytes": {
"$ref": "#/definitions/nonNegativeInteger"
},
"auxiliary_total_max_bytes": {
"$ref": "#/definitions/nonNegativeInteger"
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
"model3dCapability": {
"type": "object",
"required": ["type"],
"properties": {
"type": {
"enum": ["text_to_model", "image_to_model", "multiview_to_model"]
},
"cross_provider_edit": { "$ref": "#/definitions/crossProviderEdit" },
"default_geometry_instruct_mode": {
"$ref": "#/definitions/nonEmptyString"
},
"default_output_format": { "$ref": "#/definitions/nonEmptyString" },
"face_limit_by_geometry_quality": {
"$ref": "#/definitions/faceLimitByGeometryQuality"
},
"face_limit_range": { "$ref": "#/definitions/faceLimitRange" },
"face_limit_range_by_topology": {
"$ref": "#/definitions/faceLimitRangeByTopology"
},
"geometry_instruct_modes": { "$ref": "#/definitions/stringList" },
"geometry_quality_options": { "$ref": "#/definitions/stringList" },
"max_face_limit": { "$ref": "#/definitions/nonNegativeInteger" },
"max_face_limit_quad": { "$ref": "#/definitions/nonNegativeInteger" },
"multiview_input": { "$ref": "#/definitions/multiviewInput" },
"multiview_max_images": { "$ref": "#/definitions/nonNegativeInteger" },
"multiview_min_images": { "$ref": "#/definitions/nonNegativeInteger" },
"multiview_required_slots": { "$ref": "#/definitions/stringList" },
"output_formats": { "$ref": "#/definitions/stringList" },
"pbr_required_when_texture": { "type": "boolean" },
"provider_generation_only": { "type": "boolean" },
"support_export_uv": { "type": "boolean" },
"support_image_autofix": { "type": "boolean" },
"support_micro_detail": { "type": "boolean" },
"support_part_generation": { "type": "boolean" },
"support_pbr": { "type": "boolean" },
"support_public_image_url_only": { "type": "boolean" },
"support_quad": { "type": "boolean" },
"support_smart_low_poly": { "type": "boolean" },
"support_ta_pose": { "type": "boolean" },
"support_texture": { "type": "boolean" },
"support_texture_delight": { "type": "boolean" },
"texture_quality_options": { "$ref": "#/definitions/stringList" }
},
"additionalProperties": false
},
"meshEditCapability": {
"type": "object",
"required": ["type"],
"properties": {
"type": { "const": "mesh_edit" },
"supported_operations": {
"type": "object",
"minProperties": 1,
"additionalProperties": {
"type": "object",
"required": ["supported"],
"properties": {
"supported": { "type": "boolean" },
"supported_formats": { "$ref": "#/definitions/stringList" },
"supported_texture_prompt_modes": {
"$ref": "#/definitions/stringList"
}
},
"additionalProperties": false
}
}
},
"additionalProperties": false
},
"audioGenerateCapability": {
"type": "object",
"required": ["type"],
"properties": {
"type": { "const": "audio_generate" },
"output_formats": { "$ref": "#/definitions/stringList" },
"supports_instrumental": { "type": "boolean" },
"supports_lyrics": { "type": "boolean" },
"supports_lyrics_optimizer": { "type": "boolean" },
"supports_prompt": { "type": "boolean" }
},
"additionalProperties": false
},
"speechToTextCapability": {
"type": "object",
"required": ["type"],
"properties": {
"type": { "const": "speech_to_text" },
"endpoint_path": { "$ref": "#/definitions/nonEmptyString" },
"input_key": { "$ref": "#/definitions/nonEmptyString" },
"max_duration_seconds": { "type": "number", "minimum": 0 },
"max_file_size_mb": { "type": "number", "minimum": 0 },
"mode": { "$ref": "#/definitions/nonEmptyString" },
"poll_interval_ms": { "$ref": "#/definitions/nonNegativeInteger" },
"poll_timeout_ms": { "$ref": "#/definitions/nonNegativeInteger" },
"query_url": { "$ref": "#/definitions/nonEmptyString" },
"resource_id": { "$ref": "#/definitions/nonEmptyString" },
"submit_url": { "$ref": "#/definitions/nonEmptyString" },
"supported_formats": { "$ref": "#/definitions/stringList" },
"supports_diarization": { "type": "boolean" },
"supports_emotion": { "type": "boolean" },
"supports_hotwords": { "type": "boolean" },
"supports_prompt_context": { "type": "boolean" },
"supports_timestamp": { "type": "boolean" }
},
"additionalProperties": false
},
"voice": {
"type": "object",
"required": ["id", "name"],
"properties": {
"accent": { "type": "string" },
"age": { "type": "string" },
"audio": { "type": "string" },
"desc": { "type": "string" },
"gender": { "type": "string" },
"icon": { "type": "string" },
"id": { "$ref": "#/definitions/nonEmptyString" },
"lang": { "type": "string" },
"name": { "$ref": "#/definitions/nonEmptyString" },
"provider": { "$ref": "#/definitions/nonEmptyString" },
"tags": { "$ref": "#/definitions/stringList" }
},
"additionalProperties": false
},
"ttsParamsCapability": {
"type": "object",
"properties": {
"output_audio": { "type": "boolean" },
"voice_modify": { "type": "boolean" },
"emotion": { "type": "boolean" },
"pronunciation_dict": { "type": "boolean" },
"language_boost": { "type": "boolean" },
"language_controls": { "type": "boolean" },
"text_filters": { "type": "boolean" },
"voice_instruction": { "type": "boolean" },
"audio_formats": { "$ref": "#/definitions/stringList" },
"sample_rates": { "$ref": "#/definitions/integerList" },
"opus_sample_rates": { "$ref": "#/definitions/integerList" },
"bit_rates": { "$ref": "#/definitions/integerList" },
"volume_range": {
"type": "object",
"required": ["min", "max", "step"],
"properties": {
"min": { "type": "number", "minimum": 0 },
"max": { "type": "number", "minimum": 0 },
"step": { "type": "number", "exclusiveMinimum": 0 }
},
"additionalProperties": false
}
},
"additionalProperties": false
},
"textToSpeechCapability": {
"type": "object",
"required": ["type"],
"properties": {
"type": { "const": "text_to_speech" },
"tts_params": {
"oneOf": [{ "const": false }, { "$ref": "#/definitions/ttsParamsCapability" }]
},
"voice_compatibility_group": {
"$ref": "#/definitions/nonEmptyString"
},
"default_voice_id": { "$ref": "#/definitions/nonEmptyString" },
"resource_id": { "$ref": "#/definitions/nonEmptyString" },
"voices": {
"type": "array",
"items": { "$ref": "#/definitions/voice" }
}
},
"additionalProperties": false
},
"audioInputConstraint": {
"type": "object",
"required": ["formats", "max_duration_seconds", "max_size_mb"],
"properties": {
"formats": { "$ref": "#/definitions/stringList" },
"min_duration_seconds": { "type": "number", "minimum": 0 },
"max_duration_seconds": { "type": "number", "minimum": 0 },
"max_size_mb": { "type": "number", "minimum": 0 }
},
"additionalProperties": false
},
"voiceCloneCapability": {
"type": "object",
"required": ["type"],
"properties": {
"type": { "const": "voice_clone" },
"preview_models": { "$ref": "#/definitions/stringList" },
"prompt_audio": { "$ref": "#/definitions/audioInputConstraint" },
"source_audio": { "$ref": "#/definitions/audioInputConstraint" }
},
"additionalProperties": false
},
"imageVectorizeCapability": {
"type": "object",
"required": ["type"],
"properties": {
"type": { "const": "image_vectorize" },
"async_task": { "type": "boolean" },
"cleanup_levels": { "$ref": "#/definitions/stringList" },
"input_format_allowed": { "$ref": "#/definitions/stringList" },
"max_colors_options": { "$ref": "#/definitions/integerList" },
"output_format_allowed": { "$ref": "#/definitions/stringList" },
"support_base64_input": { "type": "boolean" },
"support_url_input": { "type": "boolean" },
"supports_extra_format_download": { "type": "boolean" },
"supports_preview": { "type": "boolean" }
},
"additionalProperties": false
},
"videoEnhanceCapability": {
"type": "object",
"required": ["type"],
"properties": {
"type": { "const": "video_enhance" },
"async_task": { "type": "boolean" },
"input_resolutions": { "$ref": "#/definitions/stringList" },
"input_video_required": { "type": "boolean" },
"max_file_size_mb": { "type": "number", "minimum": 0 },
"operations": { "$ref": "#/definitions/stringList" },
"output_resolutions": { "$ref": "#/definitions/stringList" },
"preserve_audio": { "type": "boolean" }
},
"additionalProperties": false
},
"digitalHumanCapability": {
"type": "object",
"required": ["type"],
"properties": {
"type": { "const": "digital_human_generate" }
},
"additionalProperties": false
},
"capabilities": {
"type": "object",
"minProperties": 1,
"properties": {
"audio_generate": { "$ref": "#/definitions/audioGenerateCapability" },
"audio_understanding": { "$ref": "#/definitions/languageCapability" },
"digital_human_generate": {
"$ref": "#/definitions/digitalHumanCapability"
},
"image_analysis": { "$ref": "#/definitions/languageCapability" },
"image_edit": { "$ref": "#/definitions/imageCapability" },
"image_generate": { "$ref": "#/definitions/imageCapability" },
"image_to_model": { "$ref": "#/definitions/model3dCapability" },
"image_to_video": { "$ref": "#/definitions/videoCapability" },
"image_vectorize": { "$ref": "#/definitions/imageVectorizeCapability" },
"mesh_edit": { "$ref": "#/definitions/meshEditCapability" },
"multiview_to_model": { "$ref": "#/definitions/model3dCapability" },
"omni": { "$ref": "#/definitions/languageCapability" },
"omni_video": { "$ref": "#/definitions/videoCapability" },
"speech_to_text": { "$ref": "#/definitions/speechToTextCapability" },
"text_embedding": { "$ref": "#/definitions/textEmbeddingCapability" },
"text_generate": { "$ref": "#/definitions/languageCapability" },
"text_rerank": { "$ref": "#/definitions/textRerankCapability" },
"text_to_model": { "$ref": "#/definitions/model3dCapability" },
"text_to_speech": { "$ref": "#/definitions/textToSpeechCapability" },
"tools_call": { "$ref": "#/definitions/languageCapability" },
"video_enhance": { "$ref": "#/definitions/videoEnhanceCapability" },
"video_generate": { "$ref": "#/definitions/videoCapability" },
"video_understanding": { "$ref": "#/definitions/languageCapability" },
"voice_clone": { "$ref": "#/definitions/voiceCloneCapability" }
},
"additionalProperties": false
}
}
}
File diff suppressed because it is too large Load Diff
+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",
]);
});
+7
View File
@@ -0,0 +1,7 @@
{
"schemaVersion": 1,
"version": "20261015T034833896Z",
"checksum": "b196b44afb78ff349cc1384d0fb97d1c7d33198e8f95bbe0bd6e075dd8354e98",
"publishedAt": "2026-10-15T03:48:33.896Z",
"catalogUrl": "https://git.51easyai.com/wangbo/easyai/raw/branch/main/model-catalog/official/releases/20261015T034833896Z/catalog.json"
}
+20
View File
@@ -0,0 +1,20 @@
{
"name": "easyai-official-model-catalog",
"private": true,
"version": "1.0.0",
"type": "module",
"packageManager": "yarn@1.22.22",
"engines": {
"node": ">=22.22.0"
},
"scripts": {
"format": "node catalog.mjs format",
"validate": "node catalog.mjs validate",
"prepare-release": "node catalog.mjs prepare",
"check": "node catalog.mjs check",
"test": "node --test catalog.test.mjs"
},
"dependencies": {
"ajv": "8.17.1"
}
}
File diff suppressed because it is too large Load Diff
+33
View File
@@ -0,0 +1,33 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
ajv@8.17.1:
version "8.17.1"
resolved "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz#37d9a5c776af6bc92d7f4f9510eba4c0a60d11a6"
integrity sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==
dependencies:
fast-deep-equal "^3.1.3"
fast-uri "^3.0.1"
json-schema-traverse "^1.0.0"
require-from-string "^2.0.2"
fast-deep-equal@^3.1.3:
version "3.1.3"
resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
fast-uri@^3.0.1:
version "3.1.8"
resolved "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz#f7db8d942e20eead3cfe93b0beeb4eb0169e938b"
integrity sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==
json-schema-traverse@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2"
integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==
require-from-string@^2.0.2:
version "2.0.2"
resolved "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909"
integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==
+10 -3
View File
@@ -52,10 +52,17 @@ function Test-ApplicationVersionImages {
param([object[]]$Plan, [string[]]$ComposeCommand) param([object[]]$Plan, [string[]]$ComposeCommand)
foreach ($item in $Plan) { foreach ($item in $Plan) {
if (-not $item.Service) { continue } if (-not $item.Service) { continue }
$args = @($ComposeCommand | Select-Object -Skip 1) + @('config','--images',$item.Service) $args = @($ComposeCommand | Select-Object -Skip 1) + @('config','--format','json',$item.Service)
$executable = $ComposeCommand[0] $executable = $ComposeCommand[0]
$actual = & $executable @args $configuration = & $executable @args
if ($LASTEXITCODE -ne 0 -or "$actual".Trim() -ne "$($item.Image):$($item.Version)") { if ($LASTEXITCODE -ne 0) { throw '无法读取应用的有效镜像定义;尚未拉取或重建服务' }
try {
# Compose includes dependencies even when a service is selected.
# Keep the configuration in memory; do not log its environment.
$resolved = ($configuration -join "`n") | ConvertFrom-Json -ErrorAction Stop
$actual = $resolved.services.PSObject.Properties[$item.Service].Value.image
} catch { throw '无法读取应用的有效镜像定义;尚未拉取或重建服务' }
if ("$actual".Trim() -ne "$($item.Image):$($item.Version)") {
throw "$($item.Service) 的有效镜像未使用环境中的固定版本;请通过部署仓库的受控迁移恢复镜像定义,尚未拉取或重建服务" throw "$($item.Service) 的有效镜像未使用环境中的固定版本;请通过部署仓库的受控迁移恢复镜像定义,尚未拉取或重建服务"
} }
} }
+13 -2
View File
@@ -58,11 +58,22 @@ apply_application_versions() {
} }
verify_application_version_images() { verify_application_version_images() {
local file="$1" compose="$2" service key option image version actual local file="$1" compose="$2" service key option image version actual configuration
while read -r service key option image; do while read -r service key option image; do
application_selected "$service" || continue application_selected "$service" || continue
version="$(read_env_value "$file" "$key")" version="$(read_env_value "$file" "$key")"
actual="$(sudo $compose config --images "$service")" || return 1 # --images SERVICE includes dependency images on Compose 5. Read the
# selected service from normalized config instead of comparing the list.
configuration="$(sudo $compose config --format json "$service")" || return 1
actual="$(printf '%s' "$configuration" | python3 -c '
import json, sys
try:
image = json.load(sys.stdin)["services"][sys.argv[1]]["image"]
if not isinstance(image, str): raise ValueError()
print(image)
except (ValueError, KeyError, TypeError):
raise SystemExit("无法读取应用的有效镜像定义;尚未拉取或重建服务")
' "$service")" || return 1
if [ "$actual" != "$image:$version" ]; then if [ "$actual" != "$image:$version" ]; then
echo "❌ $service 的有效镜像未使用 .env 中的固定版本 $key=$version;请通过部署仓库的受控迁移恢复镜像定义,尚未拉取或重建服务" >&2 echo "❌ $service 的有效镜像未使用 .env 中的固定版本 $key=$version;请通过部署仓库的受控迁移恢复镜像定义,尚未拉取或重建服务" >&2
return 1 return 1
@@ -0,0 +1,51 @@
#!/usr/bin/env python3
"""Exercise Bash and PowerShell image verification with real Compose config.
No images are pulled and no containers are created. Fixture settings are fake.
"""
import argparse
import json
import os
from pathlib import Path
import subprocess
import tempfile
import time
parser = argparse.ArgumentParser()
parser.add_argument('--source', type=Path, default=Path(__file__).resolve().parent.parent)
args = parser.parse_args()
source = args.source.resolve()
started = time.monotonic()
version = '1.0.0.20261014'
image = 'registry.cn-shanghai.aliyuncs.com/comfy-ai/comfy-server'
secret = 'synthetic-config-value-must-not-be-printed'
with tempfile.TemporaryDirectory(prefix='easyai-image-config-') as temporary:
root = Path(temporary)
(root / '.env').write_text(f'SERVER_VERSION={version}\nFIXTURE_SECRET={secret}\n')
binaries = root / 'bin'
binaries.mkdir()
sudo = binaries / 'sudo'
sudo.write_text('#!/bin/sh\nexec "$@"\n')
sudo.chmod(0o755)
env = {**os.environ, 'PATH': str(binaries) + os.pathsep + os.environ['PATH'], 'UPDATE_SERVICES': 'easyai-server'}
reports = []
for correct in (True, False):
selected = f'{image}:${{SERVER_VERSION}}' if correct else f'{image}:latest'
# On the negative case a dependency deliberately has the expected image;
# checking membership in --images output would wrongly approve it.
dependency = 'redis:7-alpine' if correct else f'{image}:${{SERVER_VERSION}}'
(root / 'compose.yml').write_text(f'services:\n dependency:\n image: {dependency}\n easyai-server:\n image: {selected}\n depends_on:\n - dependency\n environment:\n FIXTURE_SECRET: ${{FIXTURE_SECRET}}\n')
config = subprocess.run(['docker','compose','config','--format','json','easyai-server'], cwd=root, env=env, text=True, capture_output=True, check=True)
assert json.loads(config.stdout)['services']['easyai-server']['image'] == (f'{image}:{version}' if correct else f'{image}:latest')
images = subprocess.run(['docker','compose','config','--images','easyai-server'], cwd=root, env=env, text=True, capture_output=True, check=True)
assert len(images.stdout.splitlines()) > 1, 'Compose fixture must include dependency images'
bash = ['bash','-c', '. "$1/scripts/init-security-env.sh"; . "$1/scripts/init-application-version-env.sh"; verify_application_version_images .env "docker compose"', 'fixture', str(source)]
powershell = ['pwsh','-NoProfile','-NonInteractive','-Command', f". '{source}/scripts/Initialize-ApplicationVersions.ps1'; Test-ApplicationVersionImages @([pscustomobject]@{{Service='easyai-server';Image='{image}';Version='{version}'}}) @('docker','compose')"]
for name, command in [('bash', bash), ('powershell', powershell)]:
result = subprocess.run(command, cwd=root, env=env, text=True, errors='backslashreplace', capture_output=True, timeout=30)
assert (result.returncode == 0) == correct, f'{name} incorrect verification: {result.stdout} {result.stderr}'
assert secret not in result.stdout + result.stderr, 'configuration leaked'
reports.append({'shell':name, 'correctImage':correct, 'expectedExitObserved':True})
print(json.dumps({'cases':reports,'seconds':round(time.monotonic()-started,3)}))
+3 -2
View File
@@ -57,11 +57,12 @@ if args[:1] == ['inspect']:
sys.exit(0) sys.exit(0)
sys.exit(1) sys.exit(1)
if args[:3] == ['compose', 'config', '--services']: print('easyai-server\\neasyai-web\\nws-gateway\\neasyai-updater\\nmodel-conversion-worker\\nmongo\\nredis') if args[:3] == ['compose', 'config', '--services']: print('easyai-server\\neasyai-web\\nws-gateway\\neasyai-updater\\nmodel-conversion-worker\\nmongo\\nredis')
elif args[:3] == ['compose', 'config', '--images']: elif args[:4] == ['compose', 'config', '--format', 'json']:
names = {'easyai-server':('SERVER_VERSION','comfy-ai/comfy-server'), 'easyai-web':('WEB_VERSION','comfy-ai/one-ai'), 'ws-gateway':('WS_GATEWAY_VERSION','easyaigc/wsgateway'), 'easyai-updater':('UPDATER_VERSION','easyaigc/easyai-updater'), 'model-conversion-worker':('MODEL_CONVERSION_WORKER_VERSION','easyaigc/model-conversion-worker')} names = {'easyai-server':('SERVER_VERSION','comfy-ai/comfy-server'), 'easyai-web':('WEB_VERSION','comfy-ai/one-ai'), 'ws-gateway':('WS_GATEWAY_VERSION','easyaigc/wsgateway'), 'easyai-updater':('UPDATER_VERSION','easyaigc/easyai-updater'), 'model-conversion-worker':('MODEL_CONVERSION_WORKER_VERSION','easyaigc/model-conversion-worker')}
values = dict(line.split('=',1) for line in open('.env').read().splitlines() if '=' in line and not line.startswith('#')) values = dict(line.split('=',1) for line in open('.env').read().splitlines() if '=' in line and not line.startswith('#'))
key,image = names[args[-1]] key,image = names[args[-1]]
print('registry.cn-shanghai.aliyuncs.com/'+image+':'+('latest' if os.environ.get('UPDATE_TEST_IMAGE_OVERRIDE') == '1' else values[key])) actual = 'registry.cn-shanghai.aliyuncs.com/'+image+':'+('latest' if os.environ.get('UPDATE_TEST_IMAGE_OVERRIDE') == '1' else values[key])
print(json.dumps({'services': {args[-1]: {'image':actual}, 'dependency': {'image':'redis:7-alpine'}}}))
elif args[:3] == ['compose', 'ps', '-q']: print('container-' + args[-1]) elif args[:3] == ['compose', 'ps', '-q']: print('container-' + args[-1])
sys.exit(0) sys.exit(0)
''') ''')
@@ -0,0 +1,2 @@
# 只补充缺失或空值,自定义目录地址仍由部署管理员控制。
CONFIG_OFFICIAL_MODEL_CATALOG_URL=https://git.51easyai.com/wangbo/easyai/raw/branch/main/model-catalog/official/latest.json
+29 -17
View File
@@ -1,10 +1,10 @@
{ {
"schemaVersion": 1, "schemaVersion": 1,
"channel": "stable", "channel": "stable",
"releaseId": "3.29.0.20261014.2-610c1e33ae02", "releaseId": "3.30.2.20261017-1e180b4dc7d1",
"productVersion": "3.29.0.20261014.2", "productVersion": "3.30.2.20261017",
"platform": "linux/amd64", "platform": "linux/amd64",
"publishedAt": "2026-10-13T16:59:32.229224+00:00", "publishedAt": "2026-10-17T01:56:17.039Z",
"managedUpdateAllowed": true, "managedUpdateAllowed": true,
"scheduledUpdateAllowed": true, "scheduledUpdateAllowed": true,
"rollbackSafe": false, "rollbackSafe": false,
@@ -15,23 +15,35 @@
{ {
"key": "server-main", "key": "server-main",
"image": "registry.cn-shanghai.aliyuncs.com/comfy-ai/comfy-server", "image": "registry.cn-shanghai.aliyuncs.com/comfy-ai/comfy-server",
"digest": "sha256:a146cd64452f830ba589497be77edce10014a64de6d61abeb32533652dfc7cdc", "tag": "3.30.2.20261017",
"buildId": "b7d82b76bd49c02aacad50bfe778dc256a41c4c0", "digest": "sha256:391e2d16bcf3812a3b76e4eacc15c7587588b2c65effc2e8c7d078a7f3c7335a",
"imageSizeBytes": 990412653 "buildId": "17e301d5f4253a3e5f5affe0ca579ae9d15da24e",
}, "imageSizeBytes": 989169330,
{ "platform": "linux/amd64"
"key": "ws-gateway",
"image": "registry.cn-shanghai.aliyuncs.com/easyaigc/wsgateway",
"digest": "sha256:1dd4247cf6a8a21d336876b5d97ecdc3e4ce43199b2f31aff18e0fab47bb4dca",
"buildId": "retained-formal-image",
"imageSizeBytes": 751971874
}, },
{ {
"key": "web", "key": "web",
"image": "registry.cn-shanghai.aliyuncs.com/comfy-ai/one-ai", "image": "registry.cn-shanghai.aliyuncs.com/comfy-ai/one-ai",
"digest": "sha256:0140956e880d0c9eec0da1ebd1e3bcba20312d96b0161b5978c8c13f16335e5b", "tag": "3.30.2.20261017",
"buildId": "c685301b169bdcd662a97769a33bf69e3668f4c3", "digest": "sha256:6c7f6083731dca23f7e0ac4b1a96b1ea8b4b66b00f5e23f73afd7a6b22c2ef0a",
"imageSizeBytes": 348004004 "buildId": "e0aa6375fe621ee06f964d6a988fbae350ecaa01",
"imageSizeBytes": 348157489,
"platform": "linux/amd64"
},
{
"key": "ws-gateway",
"image": "registry.cn-shanghai.aliyuncs.com/easyaigc/wsgateway",
"tag": "3.25.0.20261009",
"digest": "sha256:1dd4247cf6a8a21d336876b5d97ecdc3e4ce43199b2f31aff18e0fab47bb4dca",
"buildId": "retained-formal-image",
"imageSizeBytes": 751971874,
"platform": "linux/amd64"
} }
] ],
"recordSource": "本轮主分支源码实际构建的固定版本;网关和模型转换Worker复用原固定产物及原构建来源",
"releaseNotes": "修复画布节点和智能体计费预占、消费冲突证据及大语言模型消费平台筛选;兼容仅支持Base64的媒体输入,优化图像超时与视频等待;修复后台首开、模型弹窗、消费参数预览与超分尺寸。",
"additionalPublishedAliases": [
"registry.cn-shanghai.aliyuncs.com/comfy-ai/one-ai:3.30.2.20261017-hmg-ai"
],
"promotedFromPublishedAt": "2026-10-17T03:20:48.454614+08:00"
} }
@@ -0,0 +1,61 @@
{
"schemaVersion": 1,
"channel": "versioned",
"releaseId": "3.29.0.20261015.2-248ca4e3a2e5",
"productVersion": "3.29.0.20261015.2",
"platform": "linux/amd64",
"publishedAt": "2026-10-15T12:24:36.871239+00:00",
"managedUpdateAllowed": true,
"scheduledUpdateAllowed": true,
"rollbackSafe": false,
"minimumUpdaterVersion": "1.1.0",
"minimumDeploymentSchemaVersion": 2,
"requiredFreeBytes": 8589934592,
"components": [
{
"key": "server-main",
"image": "registry.cn-shanghai.aliyuncs.com/comfy-ai/comfy-server",
"digest": "sha256:c53dfbc97881c3073496ad3afc3b4e81d72122a5125258e3b975ea3af1171380",
"buildId": "ae0008ba394043d2d1a233a2ff708c0a96ace6ab",
"imageSizeBytes": 990484390,
"tag": "3.29.0.20261015.2",
"platform": "linux/amd64"
},
{
"key": "web",
"image": "registry.cn-shanghai.aliyuncs.com/comfy-ai/one-ai",
"digest": "sha256:de63ed93d3a7b81e52a0cb12db519eadbc33513f74af6cf01d30fcb22547e45a",
"buildId": "343e8cc816b0f9cd7449faa44680035dd47900ec",
"imageSizeBytes": 348011223,
"tag": "3.29.0.20261015.2",
"platform": "linux/amd64"
},
{
"key": "ws-gateway",
"image": "registry.cn-shanghai.aliyuncs.com/easyaigc/wsgateway",
"digest": "sha256:1dd4247cf6a8a21d336876b5d97ecdc3e4ce43199b2f31aff18e0fab47bb4dca",
"buildId": "retained-formal-image",
"imageSizeBytes": 751971874,
"tag": "3.25.0.20261009",
"platform": "linux/amd64"
},
{
"key": "web-no-ssr",
"image": "registry.cn-shanghai.aliyuncs.com/comfy-ai/one-ai",
"digest": "sha256:03a8bb6c2f3a5200354a263b8ea12b0422c8f04b4e778d79da9be73746adcf1b",
"buildId": "343e8cc816b0f9cd7449faa44680035dd47900ec",
"imageSizeBytes": 314313406,
"tag": "3.29.0.20261015.2-no-ssr",
"platform": "linux/amd64"
}
],
"recordSource": "由已合并源码构建并发布的固定版本;WebSocket 网关复用未变更的正式产物",
"releaseNotes": "取回已有结果后自动同步画布;浏览器通过预签名上传补存并同步历史与画布,保留原结果、失败记录及结算状态。",
"sourcePullRequests": [
"https://git.51easyai.com/BCAI/comfy-server/pulls/109",
"https://git.51easyai.com/BCAI/comfy-AI2B/pulls/153"
],
"additionalPublishedAliases": [
"registry.cn-shanghai.aliyuncs.com/comfy-ai/one-ai:3.29.0.20261015.2-hmg-ai"
]
}
@@ -0,0 +1,76 @@
{
"schemaVersion": 1,
"channel": "versioned",
"releaseId": "3.29.0.20261015-48691f9f4be4",
"productVersion": "3.29.0.20261015",
"platform": "linux/amd64",
"publishedAt": "2026-10-15T03:26:33.815531+08:00",
"managedUpdateAllowed": true,
"scheduledUpdateAllowed": true,
"rollbackSafe": false,
"minimumUpdaterVersion": "1.1.0",
"minimumDeploymentSchemaVersion": 2,
"requiredFreeBytes": 8589934592,
"components": [
{
"key": "server-main",
"image": "registry.cn-shanghai.aliyuncs.com/comfy-ai/comfy-server",
"tag": "3.29.0.20261015",
"digest": "sha256:df3ced2b5a1d6724a4612cb9373b60e33fc413dc9efd1516014ce26360459b6a",
"buildId": "fa5ba3fd063677ff11b7e9eddce12da476ea10cc",
"imageSizeBytes": 989102531,
"platform": "linux/amd64"
},
{
"key": "web",
"image": "registry.cn-shanghai.aliyuncs.com/comfy-ai/one-ai",
"tag": "3.29.0.20261015",
"digest": "sha256:cba3a7aeabfaecfae59982b134cec3b44438ac8b7c582ece84c0fa5b5447f3da",
"buildId": "917dd4080cea4c99be3d98780093a1acd51c6407",
"imageSizeBytes": 348069167,
"platform": "linux/amd64"
},
{
"key": "web-no-ssr",
"image": "registry.cn-shanghai.aliyuncs.com/comfy-ai/one-ai",
"tag": "3.29.0.20261015-no-ssr",
"digest": "sha256:2b2c1616dd58489f12d1db82daf870e831d52de51804b3c717ce5a6528a52351",
"buildId": "917dd4080cea4c99be3d98780093a1acd51c6407",
"imageSizeBytes": 312779834,
"platform": "linux/amd64"
},
{
"key": "hmg-ai",
"image": "registry.cn-shanghai.aliyuncs.com/comfy-ai/one-ai",
"tag": "3.29.0.20261015-hmg-ai",
"digest": "sha256:2b2c1616dd58489f12d1db82daf870e831d52de51804b3c717ce5a6528a52351",
"buildId": "917dd4080cea4c99be3d98780093a1acd51c6407",
"imageSizeBytes": 312779834,
"platform": "linux/amd64"
},
{
"key": "ws-gateway",
"image": "registry.cn-shanghai.aliyuncs.com/easyaigc/wsgateway",
"tag": "3.25.0.20261009",
"digest": "sha256:1dd4247cf6a8a21d336876b5d97ecdc3e4ce43199b2f31aff18e0fab47bb4dca",
"buildId": "retained-formal-image",
"imageSizeBytes": 751971874,
"platform": "linux/amd64",
"reusedFromTag": "3.25.0.20261009",
"reuseSource": "existing successful production artifact; unchanged runtime source and dependencies"
},
{
"key": "model-conversion-worker",
"image": "registry.cn-shanghai.aliyuncs.com/easyaigc/model-conversion-worker",
"tag": "3.25.0.20261009.2",
"digest": "sha256:c281a3ad7ae2ae9a2e47411fb565e5fa1c35a756c09d1e281d01803ed090932e",
"buildId": "83cefbfdf2bc909d11ace9f020b108145844051a",
"imageSizeBytes": 118327684,
"platform": "linux/amd64",
"amd64Digest": "sha256:085fa49d99a06562ce9634bfdd8567efdfe4e38754e9688de81288d2e2578526",
"reusedFromTag": "3.25.0.20261009.2",
"reuseSource": "existing successful production artifact; unchanged runtime source and dependencies"
}
],
"recordSource": "2026-10-15 daily fixed-version publication; stable promotion was not executed"
}
@@ -0,0 +1,66 @@
{
"schemaVersion": 1,
"channel": "versioned",
"releaseId": "3.30.0.20261016-3e558e56d78e",
"productVersion": "3.30.0.20261016",
"platform": "linux/amd64",
"publishedAt": "2026-10-16T03:29:39.247892+08:00",
"managedUpdateAllowed": true,
"scheduledUpdateAllowed": true,
"rollbackSafe": false,
"minimumUpdaterVersion": "1.1.0",
"minimumDeploymentSchemaVersion": 2,
"requiredFreeBytes": 8589934592,
"components": [
{
"key": "server-main",
"image": "registry.cn-shanghai.aliyuncs.com/comfy-ai/comfy-server",
"tag": "3.30.0.20261016",
"digest": "sha256:57cac83fb29180fd10591a5960b1e258359f26af5d2d31370674e8e14aaf107e",
"buildId": "e99b636982fc654fdb7f3dcbe08db57bd3b047bc",
"imageSizeBytes": 989161034,
"platform": "linux/amd64"
},
{
"key": "web",
"image": "registry.cn-shanghai.aliyuncs.com/comfy-ai/one-ai",
"tag": "3.30.0.20261016",
"digest": "sha256:c2adf0577536f7eed402ce0043f2d708c2294700e8be9a51fbd29112ec1f644d",
"buildId": "b6850f138cd831972fcb35ac8fc456f30fe98609",
"imageSizeBytes": 348127493,
"platform": "linux/amd64"
},
{
"key": "web-no-ssr",
"image": "registry.cn-shanghai.aliyuncs.com/comfy-ai/one-ai",
"tag": "3.30.0.20261016-no-ssr",
"digest": "sha256:9240e9689aa9862ebd21035499235e7a3a0817de93274a72dca0b821ea1fbb8d",
"buildId": "b6850f138cd831972fcb35ac8fc456f30fe98609",
"imageSizeBytes": 312836808,
"platform": "linux/amd64"
},
{
"key": "ws-gateway",
"image": "registry.cn-shanghai.aliyuncs.com/easyaigc/wsgateway",
"tag": "3.25.0.20261009",
"digest": "sha256:1dd4247cf6a8a21d336876b5d97ecdc3e4ce43199b2f31aff18e0fab47bb4dca",
"buildId": "retained-formal-image",
"imageSizeBytes": 751971874,
"platform": "linux/amd64"
},
{
"key": "model-conversion-worker",
"image": "registry.cn-shanghai.aliyuncs.com/easyaigc/model-conversion-worker",
"tag": "3.25.0.20261009.2",
"digest": "sha256:c281a3ad7ae2ae9a2e47411fb565e5fa1c35a756c09d1e281d01803ed090932e",
"buildId": "83cefbfdf2bc909d11ace9f020b108145844051a",
"imageSizeBytes": 118327684,
"platform": "linux/amd64"
}
],
"recordSource": "本轮主分支源码实际构建的固定版本;网关和模型转换Worker复用原固定产物及原构建来源",
"releaseNotes": "消费记录按实际执行主平台筛选并统一明细与导出;生成结果自动恢复并支持浏览器转存兜底;本地素材拖入后先上传持久化;优化3D轮询与聊天审计。画布归档代码交付,当前生产归档开关保持关闭。",
"additionalPublishedAliases": [
"registry.cn-shanghai.aliyuncs.com/comfy-ai/one-ai:3.30.0.20261016-hmg-ai"
]
}
@@ -0,0 +1,63 @@
{
"schemaVersion": 1,
"channel": "versioned",
"releaseId": "3.30.1.20261016-280cb14aee11",
"productVersion": "3.30.1.20261016",
"platform": "linux/amd64",
"publishedAt": "2026-10-16T11:05:49.779738+08:00",
"managedUpdateAllowed": true,
"scheduledUpdateAllowed": true,
"rollbackSafe": false,
"minimumUpdaterVersion": "1.1.0",
"minimumDeploymentSchemaVersion": 2,
"requiredFreeBytes": 8589934592,
"components": [
{
"key": "server-main",
"image": "registry.cn-shanghai.aliyuncs.com/comfy-ai/comfy-server",
"tag": "3.30.1.20261016",
"digest": "sha256:280cb14aee113fe3e73ffb74d6a18ee48520615af46b69e46c52fe97b3aa6143",
"buildId": "30cb3019eecd5479b0ed0ff1e3949005098c2f20",
"imageSizeBytes": 989163608,
"platform": "linux/amd64"
},
{
"key": "web",
"image": "registry.cn-shanghai.aliyuncs.com/comfy-ai/one-ai",
"tag": "3.30.0.20261016",
"digest": "sha256:c2adf0577536f7eed402ce0043f2d708c2294700e8be9a51fbd29112ec1f644d",
"buildId": "b6850f138cd831972fcb35ac8fc456f30fe98609",
"imageSizeBytes": 348127493,
"platform": "linux/amd64"
},
{
"key": "web-no-ssr",
"image": "registry.cn-shanghai.aliyuncs.com/comfy-ai/one-ai",
"tag": "3.30.1.20261016-no-ssr",
"digest": "sha256:9240e9689aa9862ebd21035499235e7a3a0817de93274a72dca0b821ea1fbb8d",
"buildId": "b6850f138cd831972fcb35ac8fc456f30fe98609",
"imageSizeBytes": 312836808,
"platform": "linux/amd64"
},
{
"key": "ws-gateway",
"image": "registry.cn-shanghai.aliyuncs.com/easyaigc/wsgateway",
"tag": "3.25.0.20261009",
"digest": "sha256:1dd4247cf6a8a21d336876b5d97ecdc3e4ce43199b2f31aff18e0fab47bb4dca",
"buildId": "retained-formal-image",
"imageSizeBytes": 751971874,
"platform": "linux/amd64"
},
{
"key": "model-conversion-worker",
"image": "registry.cn-shanghai.aliyuncs.com/easyaigc/model-conversion-worker",
"tag": "3.25.0.20261009.2",
"digest": "sha256:c281a3ad7ae2ae9a2e47411fb565e5fa1c35a756c09d1e281d01803ed090932e",
"buildId": "83cefbfdf2bc909d11ace9f020b108145844051a",
"imageSizeBytes": 118327684,
"platform": "linux/amd64"
}
],
"recordSource": "后端从主线构建 30cb3019;前端、WebSocket 网关及模型转换 Worker 复用既有正式固定产物",
"releaseNotes": "生成成功后缺失用量或计费、转存异常保留原结果和原任务,避免换平台重复生成;优化消费筛选查询"
}
@@ -0,0 +1,66 @@
{
"schemaVersion": 1,
"channel": "versioned",
"releaseId": "3.30.2.20261017-1e180b4dc7d1",
"productVersion": "3.30.2.20261017",
"platform": "linux/amd64",
"publishedAt": "2026-10-17T03:20:48.454614+08:00",
"managedUpdateAllowed": true,
"scheduledUpdateAllowed": true,
"rollbackSafe": false,
"minimumUpdaterVersion": "1.1.0",
"minimumDeploymentSchemaVersion": 2,
"requiredFreeBytes": 8589934592,
"components": [
{
"key": "server-main",
"image": "registry.cn-shanghai.aliyuncs.com/comfy-ai/comfy-server",
"tag": "3.30.2.20261017",
"digest": "sha256:391e2d16bcf3812a3b76e4eacc15c7587588b2c65effc2e8c7d078a7f3c7335a",
"buildId": "17e301d5f4253a3e5f5affe0ca579ae9d15da24e",
"imageSizeBytes": 989169330,
"platform": "linux/amd64"
},
{
"key": "web",
"image": "registry.cn-shanghai.aliyuncs.com/comfy-ai/one-ai",
"tag": "3.30.2.20261017",
"digest": "sha256:6c7f6083731dca23f7e0ac4b1a96b1ea8b4b66b00f5e23f73afd7a6b22c2ef0a",
"buildId": "e0aa6375fe621ee06f964d6a988fbae350ecaa01",
"imageSizeBytes": 348157489,
"platform": "linux/amd64"
},
{
"key": "web-no-ssr",
"image": "registry.cn-shanghai.aliyuncs.com/comfy-ai/one-ai",
"tag": "3.30.2.20261017-no-ssr",
"digest": "sha256:6b72ac71499e9bbcdfb0a94a82258c68121aab46e44dc4825a52c54f8bb41935",
"buildId": "e0aa6375fe621ee06f964d6a988fbae350ecaa01",
"imageSizeBytes": 312866555,
"platform": "linux/amd64"
},
{
"key": "ws-gateway",
"image": "registry.cn-shanghai.aliyuncs.com/easyaigc/wsgateway",
"tag": "3.25.0.20261009",
"digest": "sha256:1dd4247cf6a8a21d336876b5d97ecdc3e4ce43199b2f31aff18e0fab47bb4dca",
"buildId": "retained-formal-image",
"imageSizeBytes": 751971874,
"platform": "linux/amd64"
},
{
"key": "model-conversion-worker",
"image": "registry.cn-shanghai.aliyuncs.com/easyaigc/model-conversion-worker",
"tag": "3.25.0.20261009.2",
"digest": "sha256:c281a3ad7ae2ae9a2e47411fb565e5fa1c35a756c09d1e281d01803ed090932e",
"buildId": "83cefbfdf2bc909d11ace9f020b108145844051a",
"imageSizeBytes": 118327684,
"platform": "linux/amd64"
}
],
"recordSource": "本轮主分支源码实际构建的固定版本;网关和模型转换Worker复用原固定产物及原构建来源",
"releaseNotes": "修复画布节点和智能体计费预占、消费冲突证据及大语言模型消费平台筛选;兼容仅支持Base64的媒体输入,优化图像超时与视频等待;修复后台首开、模型弹窗、消费参数预览与超分尺寸。",
"additionalPublishedAliases": [
"registry.cn-shanghai.aliyuncs.com/comfy-ai/one-ai:3.30.2.20261017-hmg-ai"
]
}