refactor: ZenMux 节点移除 vendor 参数,模型下拉直接全量选择

- 模型列表本身按厂商排序聚类,配合 ComfyUI 下拉搜索(输入 "qwen/" 等
  前缀过滤)已能满足筛选需求,vendor 下拉冗余
- 一并移除只为级联服务的前端脚本 web/zenmux_cascade.js 与 WEB_DIRECTORY

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
rui40000
2026-07-21 13:33:42 +08:00
co-authored by Claude Fable 5
parent c8c546a873
commit cc5852dc18
5 changed files with 13 additions and 92 deletions
+1 -2
View File
@@ -719,14 +719,13 @@ ComfyUI-SDMatte 传 `aux_input="trimap"`,把模型推到了没训练过的输
连接 [ZenMux](https://zenmux.ai) 聚合平台(OpenAI 兼容协议),一个节点即可调用其收录的**所有文本类模型**(Anthropic、OpenAI、Google、DeepSeek、Qwen 等 20 家厂商、130+ 模型)。支持文本生成与多模态图像理解(最多 6 张图)。
**特色功能**:
- **厂商 → 模型二级选择**: 先在 `vendor` 下拉选厂商,`model` 下拉会自动收窄为该厂商的模型(由前端脚本 `web/zenmux_cascade.js` 联动)
- **价格直接标在选项上**: 每个模型后缀形如 `[入$0.2/M 出$1.25/M]`,即输入/输出每百万 token 的美元价格,选型时一目了然
- **快速筛选**: 模型列表按「厂商/模型名」排序聚类,同厂商模型天然相邻;在下拉的搜索框输入厂商前缀(如 `qwen/`、`anthropic/`)即可只看该厂商的模型
- **离线可用的模型清单**: 模型与价格来自随包分发的 `zenmux/models_snapshot.json`;价格有变动时运行 `python zenmux/build_snapshot.py` 即可重新拉取更新
- **旧工作流兼容**: 价格快照更新后,旧工作流里保存的带旧价格标签仍能正确解析出模型 id,不会失效
**输入参数**:
- `api_key` (STRING): ZenMux 平台的 API Key(在 zenmux.ai 控制台获取)
- `vendor` (选择): 厂商筛选,默认 `openai`
- `model` (选择): 模型(带价格标注),默认 `openai/gpt-5.4-nano`
- `system_prompt` (STRING): 系统提示词
- `user_prompt` (STRING): 用户提示词
+1 -4
View File
@@ -93,7 +93,4 @@ NODE_DISPLAY_NAME_MAPPINGS.update(IMAGESPLITTER_NODE_DISPLAY_NAME_MAPPINGS)
NODE_DISPLAY_NAME_MAPPINGS.update(SDMATTE_NODE_DISPLAY_NAME_MAPPINGS)
NODE_DISPLAY_NAME_MAPPINGS.update(ZENMUX_NODE_DISPLAY_NAME_MAPPINGS)
# 前端扩展目录(ZenMux 节点的「厂商→模型」级联脚本等)
WEB_DIRECTORY = "./web"
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS', 'WEB_DIRECTORY']
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS']
-60
View File
@@ -1,60 +0,0 @@
// ZenMux 节点「厂商 → 模型」二级级联
// ==================================
// 后端把全量模型标签(含价格)都放进 model 下拉以通过校验;
// 本扩展在前端按 vendor 收窄 model 的候选列表。
// 约定:模型标签第一个空格前是 "vendor/model" 形式的 id,
// 故 label.split("/")[0] 即厂商名,与 vendor 下拉的取值同源。
import { app } from "../../scripts/app.js";
app.registerExtension({
name: "Ruinode.ZenMuxCascade",
async beforeRegisterNodeDef(nodeType, nodeData, _app) {
if (nodeData.name !== "ZenMuxAPINode") return;
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
const r = onNodeCreated?.apply(this, arguments);
const vendorW = this.widgets?.find((w) => w.name === "vendor");
const modelW = this.widgets?.find((w) => w.name === "model");
if (!vendorW || !modelW) return r;
// 全量标签快照(此时 options.values 是后端给的完整列表)
const allLabels = (modelW.options.values || []).slice();
const vendorOf = (label) => String(label).split("/")[0];
// resetIfMissing:当前值不在收窄后的列表时是否重置为列表首项。
// 手动切厂商 → 重置;加载旧工作流 → 保留(旧价格标签后端能解析)。
const applyFilter = (vendor, resetIfMissing) => {
const filtered = allLabels.filter((l) => vendorOf(l) === vendor);
modelW.options.values = filtered.length ? filtered : allLabels.slice();
if (resetIfMissing && !modelW.options.values.includes(modelW.value)) {
modelW.value = modelW.options.values[0];
modelW.callback?.(modelW.value);
}
this.setDirtyCanvas?.(true, true);
};
const origCallback = vendorW.callback;
vendorW.callback = function (value) {
const r2 = origCallback?.apply(this, arguments);
applyFilter(value, true);
return r2;
};
// 新建节点:按默认厂商先收窄一次
applyFilter(vendorW.value, false);
// 加载已保存的工作流:configure 恢复 widget 值后再按真实厂商收窄,
// 但不动用户保存的 model 选择
const onConfigure = this.onConfigure;
this.onConfigure = function () {
const r3 = onConfigure?.apply(this, arguments);
applyFilter(vendorW.value, false);
return r3;
};
return r;
};
},
});
+2 -9
View File
@@ -4,14 +4,13 @@ ZenMux 模型注册表
=================
从随包分发的 models_snapshot.json 加载模型清单,为节点提供:
- all_vendors() 厂商列表(模型 id 的前缀,如 "openai")
- all_model_labels() 全量「带价格」下拉标签,按厂商 → 模型排序
- all_model_labels() 全量「带价格」下拉标签,按厂商 → 模型排序聚类
- default_model_label() 默认模型 openai/gpt-5.4-nano 对应的标签
- label_to_model_id() 从下拉标签解析真实 model id(对旧标签/纯 id 也兼容)
标签格式(价格单位:USD / 百万 token):
openai/gpt-5.4-nano [入$0.04/M 出$0.32/M]
标签第一个空格前恒为 model id,前端级联 JS 与后端解析都依赖这一点。
标签第一个空格前恒为 model id,后端解析依赖这一点。
快照可用 build_snapshot.py 随时重新拉取更新。
"""
@@ -64,12 +63,6 @@ _MODELS = _load_models()
_LABELS = [_make_label(m) for m in _MODELS]
_LABEL_TO_ID = {lb: m["id"] for lb, m in zip(_LABELS, _MODELS)}
_KNOWN_IDS = {m["id"] for m in _MODELS}
_VENDORS = sorted({m.get("vendor") or m["id"].split("/")[0] for m in _MODELS},
key=str.lower)
def all_vendors():
return list(_VENDORS)
def all_model_labels():
+9 -17
View File
@@ -5,8 +5,8 @@ ZenMux API 连接节点
通过 ZenMux 聚合平台(https://zenmux.ai)调用其收录的所有文本类模型。
特性:
- 模型选择做成「厂商 → 模型」两级级联:先在 vendor 下拉里选厂商,
再在 model 下拉里选该厂商的模型(联动由配套前端 zenmux_cascade.js 完成)。
- 模型下拉覆盖 ZenMux 全部文本模型,按「厂商/模型名」排序聚类;
ComfyUI 下拉自带搜索,输入厂商前缀(如 "anthropic/")即可快速过滤。
- 每个模型选项后面直接标注输入/输出价格(USD / 百万 token)。
- 具备常规 API 节点的完整参数:api_key、system/user prompt、seed、
temperature、top_p、max_tokens、以及可选的多模态图像输入与代理。
@@ -31,7 +31,6 @@ from .model_registry import (
DEFAULT_MODEL_ID,
default_model_label,
all_model_labels,
all_vendors,
label_to_model_id,
)
@@ -83,7 +82,6 @@ class ZenMuxNode:
# ────────── 输入定义 ──────────
@classmethod
def INPUT_TYPES(cls):
vendors = all_vendors()
model_labels = all_model_labels()
default_label = default_model_label()
return {
@@ -92,13 +90,8 @@ class ZenMuxNode:
"default": "",
"multiline": False,
}),
# vendor 只用于前端级联筛选;后端不依赖它(真实模型从 model 解析)。
# 默认选中 openai。
"vendor": (vendors, {
"default": "openai" if "openai" in vendors else (vendors[0] if vendors else "openai"),
}),
# model 的候选是「全量」模型标签(含价格),保证 ComfyUI 后端校验通过;
# 前端 JS 会按 vendor 把可见项收窄到该厂商。
# 全量模型标签(含价格),已按厂商排序聚类;
# 下拉搜索框输入厂商前缀(如 "qwen/")即可过滤。
"model": (model_labels, {
"default": default_label,
}),
@@ -169,12 +162,12 @@ class ZenMuxNode:
# ────────── 宽松校验 ──────────
@classmethod
def VALIDATE_INPUTS(cls, vendor, model):
def VALIDATE_INPUTS(cls, model):
"""
接管 vendor / model 两个下拉的校验,替代 ComfyUI 内置的
「值必须在候选列表里」检查:价格快照更新后,旧工作流里保存的
标签(带旧价格)不再逐字匹配新列表,但只要能解析出 model id
就应放行,避免整个工作流被判为无效。
接管 model 下拉的校验,替代 ComfyUI 内置的「值必须在候选列表里」
检查:价格快照更新后,旧工作流里保存的标签(带旧价格)不再逐字
匹配新列表,但只要能解析出 model id 就应放行,避免整个工作流
被判为无效。
"""
if label_to_model_id(model) is None:
return f"无法从 '{model}' 解析出 ZenMux 模型 id"
@@ -198,7 +191,6 @@ class ZenMuxNode:
def generate(
self,
api_key,
vendor,
model,
system_prompt,
user_prompt,