feat: 新增 ZenMux API 连接节点(厂商→模型级联选择,选项含价格标注)

- 一个节点调用 ZenMux 聚合平台全部文本类模型(20 厂商 / 138 模型)
- vendor→model 二级下拉:前端 web/zenmux_cascade.js 按厂商实时收窄模型列表
- 模型选项后缀标注输入/输出价格(USD/百万token),如 [入$0.2/M 出$1.25/M]
- 默认模型 openai/gpt-5.4-nano;具备 api_key/system/user prompt/seed/
  temperature/top_p/max_tokens/多模态图像×6/detail/base_url/proxy 等完整参数
- 模型清单随包分发(models_snapshot.json),离线可列出;
  build_snapshot.py 可随时重拉最新价格
- VALIDATE_INPUTS 宽松校验:价格快照更新后旧工作流的模型标签仍可解析执行
- base_url 默认值沿用仓库约定不含 "://",规避 ComfyUI 前端吞协议片段的老问题
- 导出 WEB_DIRECTORY 以装载前端级联脚本

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
rui40000
2026-07-21 11:37:00 +08:00
co-authored by Claude Fable 5
parent a826b42576
commit ca61c48dbd
8 changed files with 2322 additions and 1 deletions
+60
View File
@@ -0,0 +1,60 @@
// 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;
};
},
});