关于 comfyui-node-frontend
comfyui-node-frontend 是为 ComfyUI 自定义节点开发者准备的前端扩展指南,说明如何用 JavaScript 扩展界面和节点行为。内容覆盖生命周期钩子、控件、命令、设置及图表加载事件。
主要功能
- 01注册前端扩展与生命周期钩子
- 02创建自定义控件和界面标签页
- 03添加命令、快捷键与设置
- 04监听执行事件并处理图表加载
- 05通过 API 与服务端通信
适用场景
- 01为自定义节点添加交互控件
- 02在 ComfyUI 中加入扩展面板和命令
- 03响应节点执行状态或图表加载
- 04将 Python 服务端事件传给前端
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ jtydhr88/ comfyui-custom-node-skills --skill comfyui-node-frontend安装后在 Claude Code 中输入/comfyui-node-frontend确认已加载。
附带文件api-reference.md
详细介绍
ComfyUI 前端扩展
自定义节点可通过 JavaScript 扩展 ComfyUI 前端。扩展可注册钩子、小部件、命令、设置和 UI 组件。
快速开始
1. 在 Python 中导出 WEB_DIRECTORY
# __init__.py
WEB_DIRECTORY = "./js"
__all__ = ["WEB_DIRECTORY"]
2. 创建 JavaScript 扩展
// js/my_extension.js
import { app } from "../../scripts/app.js";
app.registerExtension({
name: "my_nodes.my_extension",
async setup() {
console.log("Extension loaded!");
},
});
ComfyUI 启动时会自动加载 WEB_DIRECTORY 中的所有 .js 文件。
扩展钩子(生命周期顺序)
init — 创建画布后、节点加载前
app.registerExtension({
name: "my.ext",
async init(app) {
// Modify core behavior, add global listeners
},
});
addCustomNodeDefs — 修改节点定义
async addCustomNodeDefs(defs, app) {
// defs is a dict of all node definitions
// Can add or modify definitions before registration
defs["MyFrontendNode"] = {
input: { required: { text: ["STRING", {}] } },
output: ["STRING"],
output_name: ["text"],
name: "MyFrontendNode",
display_name: "My Frontend Node",
category: "custom",
};
},
getCustomWidgets — 注册自定义小部件类型
getCustomWidgets(app) {
return {
MY_WIDGET(node, inputName, inputData, app) {
const widget = node.addWidget("text", inputName, "", () => {});
widget.serializeValue = () => widget.value;
return { widget }; // or { widget, minWidth, minHeight }; v1.54+ also accepts `return widget`
},
};
},
beforeRegisterNodeDef — 修改节点原型
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name === "MyNode") {
// Chain onto prototype methods
const origOnCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
origOnCreated?.apply(this, arguments);
// Add custom widget, modify behavior, etc.
this.addWidget("button", "Run", null, () => {
console.log("Button clicked!");
});
};
}
},
nodeCreated — 创建节点实例后
nodeCreated(node, app) {
if (node.comfyClass === "MyNode") {
// Modify this specific node instance
node.color = "#335";
}
},
setup — 应用完全加载后
async setup(app) {
// Add event listeners, register UI components
app.api.addEventListener("executed", (event) => {
console.log("Node executed:", event.detail);
});
},
loadedGraphNode — 加载已保存的图时
loadedGraphNode(node, app) {
if (node.comfyClass === "MyNode") {
// Restore state from saved graph
}
},
registerCustomNodes — 注册其他节点类型
registerCustomNodes(app) {
// Register custom LiteGraph node types
},
beforeRegisterVueAppNodeDefs — Vue 注册前修改节点定义
beforeRegisterVueAppNodeDefs(defs, app) {
// Modify definitions before they reach the Vue app
},
beforeConfigureGraph / afterConfigureGraph
async beforeConfigureGraph(graphData, missingNodeTypes, app) {
// Before graph data is applied
},
async afterConfigureGraph(missingNodeTypes, app) {
// After graph is fully configured
},
beforeLoadGraph / afterLoadGraph(v1.53.5+)/ onGraphLoadError(v1.54.2+)
对整个工作流加载过程进行封装(包括会跳过 beforeConfigureGraph 的 API-JSON 和 A1111 导入)。顺序:beforeLoadGraph → beforeConfigureGraph → afterConfigureGraph → afterLoadGraph;加载失败时会运行 onGraphLoadError,而不会运行 after* 钩子。
beforeLoadGraph(app) { /* e.g. suspend your own listeners */ },
afterLoadGraph(app) { /* graph fully loaded */ },
onGraphLoadError(error, app) { /* undo whatever beforeLoadGraph started */ },
即将推出(尚未发布):图加载将进入队列。在任何图加载钩子中调用 app.loadGraphData() / loadApiJson() / handleFile() 都会被拒绝(“cannot start a nested graph load”)。如果必须加载另一张图,请延迟调用(使用 setTimeout)。
getSelectionToolboxCommands — 向选区工具箱添加命令
getSelectionToolboxCommands(selectedItem) {
// Return array of command IDs to show when item is selected
return ["my.ext.doSomething"];
},
onNodeOutputsUpdated — 执行输出发生变化时
onNodeOutputsUpdated(nodeOutputs) {
// nodeOutputs: Record<NodeLocatorId, output data>, fired when node outputs update
},
身份验证钩子
onAuthUserResolved(user, app) {
// Fires when user authentication resolves
},
onAuthTokenRefreshed() {
// Fires when auth token is refreshed
},
onAuthUserLogout() {
// Fires when user logs out
},
自定义小部件
添加 DOM 小部件
beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name === "MyNode") {
const origOnCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
origOnCreated?.apply(this, arguments);
const container = document.createElement("div");
container.innerHTML = `<input type="color" value="#ff0000">`;
container.querySelector("input").addEventListener("change", (e) => {
this.widgets.find(w => w.name === "color").value = e.target.value;
});
this.addDOMWidget("colorPicker", "custom", container, {
serialize: true,
getValue() { return container.querySelector("input").value; },
setValue(v) { container.querySelector("input").value = v; },
});
};
}
},
小部件钩子
// Called before prompt is queued
// isPartialExecution is true when only selected nodes are queued
widget.beforeQueued = function ({ isPartialExecution } = {}) {
// Prepare widget value
};
// Called after prompt is queued
widget.afterQueued = function ({ isPartialExecution } = {}) {
// Reset or update widget. Core control_after_generate now also applies
// on partial runs (v1.53.5+); the flag is still passed for your own logic.
};
// Custom serialization (may be async; it is awaited)
widget.serializeValue = function (node, index) {
return JSON.stringify(this.value);
};
小部件规则(由存储管理的小部件,v1.53+)
小部件值存储在以 graphId:nodeId:widgetName 为键的存储中:
- 每个节点中的名称必须唯一。 重复的名称会被改为
name#1、name#2……;如果无法重命名,则会被拒绝(v1.58+)。在此之前,重复名称会静默覆盖第一个小部件的值。 - 添加小部件后不要重命名它——存储的值仍会保留在旧名称下。请先移除,再添加新小部件。
- 使用
node.removeWidget(widget)移除小部件,不要通过拼接node.widgets来移除。 - 首次赋值前,
node.widgets可能是undefined:node.widgets ??= []、node.widgets?.length ?? 0。 - 通过
addWidget/addCustomWidget/addDOMWidget(或BaseWidget子类)创建小部件。 推入node.widgets的普通对象永远不会被注册。
隐藏小部件
widget.hidden = true; // hide everywhere at runtime (still serialized; works in Nodes 2.0)
widget.advanced = true; // show only when "show advanced" is on
// Per-surface policy at creation (v1.55.6+); all three keys are required
node.addWidget("text", "debug", "", null, {
surfaces: { canvas: "shown", vueNode: "never", panel: "never" }, // "shown" | "advanced" | "never"
});
options.canvasOnly(=vueNode/panel: "never")已弃用,但仍受支持;options.hideInPanel 仅会将小部件从右侧面板中隐藏。
声明式扩展属性
命令
app.registerExtension({
name: "my.ext",
commands: [
{
id: "my.ext.doSomething",
label: "Do Something",
icon: "pi pi-bolt",
function: () => { console.log("Executed!"); },
// v1.55.14+: refuse to run while the canvas is select-only
mutatesGraph: true, // or () => boolean
},
],
});
键绑定
keybindings: [
{
commandId: "my.ext.doSomething",
combo: { key: "d", ctrl: true, shift: true },
},
],
设置
settings: [
{
id: "my.ext.mySetting",
name: "My Setting",
type: "boolean",
defaultValue: true,
onChange: (value) => { console.log("Setting changed:", value); },
},
{
id: "my.ext.mode",
name: "Processing Mode",
type: "combo",
options: ["fast", "quality", "balanced"], // or (currentValue) => [...] for dynamic options
defaultValue: "balanced",
},
],
onChange(newValue, oldValue) 可以是异步函数。
设置类型:boolean、number、slider、knob、combo、radio、text、image、color、url、hidden、backgroundImage
侧边栏标签页
async setup(app) {
app.extensionManager.registerSidebarTab({
id: "my-sidebar",
title: "My Panel",
icon: "pi pi-cog",
type: "custom",
render: (container) => {
container.innerHTML = "<h3>My Custom Panel</h3>";
},
destroy: () => {
// Cleanup
},
// v1.58+: return true to handle the click yourself (skips default open/close)
onToggle: async () => false,
});
},
type: "vue" 标签页(component: MyVueComponent)必须自行打包 Vue UI 库——宿主将不再提供 PrimeVue(即将推出;请参阅“破坏性变更”)。
底部面板标签页
bottomPanelTabs: [
{
id: "my-panel",
title: "My Panel",
type: "custom",
render: (container) => {
container.innerHTML = "<div>Panel content</div>";
},
},
],
菜单命令
menuCommands: [
{
path: ["My Extension"],
commands: ["my.ext.doSomething"],
},
],
关于页面徽章
aboutPageBadges: [
{ label: "v1.0.0", url: "https://github.com/...", icon: "pi pi-github", severity: "warn" },
// severity is optional: "danger" | "warn"
],
顶栏徽章
topbarBadges: [
{
text: "My Extension", // required
label: "BETA", // optional badge label
variant: "info", // "info" | "warning" | "error"
icon: "pi pi-star", // optional icon
tooltip: "Extension info", // optional tooltip
},
],
操作栏按钮
actionBarButtons: [
{
icon: "pi pi-bolt", // required
label: "My Action", // optional label
tooltip: "Run my action", // optional tooltip
onClick: () => { /* ... */ }, // required click handler
},
],
API 事件
监听执行事件:
// Node execution completed
app.api.addEventListener("executed", ({ detail }) => {
const { node, output } = detail;
// output contains images, text, etc.
});
// Execution progress
app.api.addEventListener("progress", ({ detail }) => {
const { value, max, node } = detail;
});
// Execution started/completed
app.api.addEventListener("execution_start", ({ detail }) => {});
app.api.addEventListener("execution_success", ({ detail }) => {});
app.api.addEventListener("execution_error", ({ detail }) => {});
// Status updates
app.api.addEventListener("status", ({ detail }) => {
const { exec_info } = detail;
});
服务端到客户端通信
Python(服务端):
from server import PromptServer
PromptServer.instance.send_sync(
"my_extension.update",
{"status": "complete", "data": result}
)
JavaScript(客户端):
app.api.addEventListener("my_extension.update", ({ detail }) => {
console.log("Received:", detail);
});
调用自己的路由
// fetchApi prefixes /api and adds auth headers
const res = await app.api.fetchApi("/my_extension/process", {
method: "POST",
body: JSON.stringify(payload),
timeoutMs: null, // v1.55.2+: default aborts after 60 s without response headers (TimeoutError)
});
对于耗时较长的同步路由,请传入 timeoutMs: null(或更大的数值);你自己的 signal 仍会生效。
Toast 通知
v1.58+ API——success / info / warning / error / loading(title, options?) 会返回 ToastId:
const toast = app.extensionManager.toast;
const id = toast.loading("Processing…");
// Reusing `id` replaces that toast in place
toast.success("Done", {
id,
description: "3 images saved",
duration: 3000, // ms; default is no auto-dismiss
closable: true,
action: { label: "Open", onClick: () => { /* ... */ } }, // does not auto-dismiss
});
toast.dismiss(id); // returns true if a toast was removed
toast.dismissAll();
较旧的前端只有 add()。它在 v1.58+ 上仍然可用,但已弃用;请进行功能检测:
if (toast.success) toast.success("Saved", { description: "Done", duration: 3000 });
else toast.add({ severity: "success", summary: "Saved", detail: "Done", life: 3000 });
// add() severities: "success" | "info" | "warn" | "error" ("secondary"/"contrast" → info).
// group/styleClass are ignored and remove()/removeAll() are gone on v1.58+.
对话框
// Confirmation dialog — resolves true / false, or null if dismissed
const result = await app.extensionManager.dialog.confirm({
title: "Confirm Action",
message: "Are you sure?",
type: "delete", // optional: "default" | "overwrite" | "delete" | "info" | ...
itemList: ["a.png"], // optional bullet list under the message
hint: "Cannot be undone", // optional
key: "my-ext-confirm", // v1.53+: own dialog key so an already-open shared prompt can't swallow it
});
// Prompt dialog
const value = await app.extensionManager.dialog.prompt({
title: "Enter Value",
message: "Provide a name:",
defaultValue: "default",
});
ExtensionManager 实用工具
设置访问
// Read a setting value
const val = app.extensionManager.setting.get("my.ext.mySetting");
// Write a setting value
app.extensionManager.setting.set("my.ext.mySetting", newValue);
执行错误(只读)
// Last node-level errors (keyed by node ID)
const nodeErrors = app.extensionManager.lastNodeErrors;
// Last execution-level error
const execError = app.extensionManager.lastExecutionError;
Markdown 渲染
// Render markdown to sanitized HTML (marked + DOMPurify, safe for innerHTML)
const html = app.extensionManager.renderMarkdownToHtml(markdownStr, baseUrl);
上下文菜单项
app.registerExtension({
name: "my.ext",
// Canvas right-click menu
getCanvasMenuItems(canvas) {
return [{
content: "My Action",
callback: () => { console.log("Canvas menu clicked"); },
}];
},
// Node right-click menu
getNodeMenuItems(node) {
if (node.comfyClass === "MyNode") {
return [{
content: "Custom Action",
callback: () => { console.log("Node:", node.id); },
}];
}
return [];
},
});
节点实例属性(LGraphNode 扩展)
// Available on node instances:
node.comfyClass // ComfyUI node type name
node.isVirtualNode // true for frontend-only nodes
node.imgs // preview images array
node.imageIndex // current preview image index
// Callbacks:
node.onExecuted = function(output) { /* execution result */ };
node.onExecutionStart = function() { /* about to execute */ };
node.onDragOver = function(event) { /* file drag over */ };
node.onDragDrop = function(event) { /* file dropped */ };
最新重大变更(frontend 1.4x–1.58,以及即将推出的版本)
- 即将移除 PrimeVue(v1.58 之后) — 不再提供
$primevue、PrimeVue 主题或全局--p-*CSS 变量。请改用设计令牌:--primary-background、--base-foreground、--muted-foreground、--base-background、--secondary-background、--border-default。如需继续使用 PrimeVue 组件,请自行挂载安装了 PrimeVue 的 Vue 应用。pi pi-*图标类仍可使用。 - PrimeVue 工具提示已移除(v1.58+) — 不再提供
v-tooltip/.p-tooltip。已打开的工具提示位于document.body上的[data-slot="tooltip-content"]。 - Toast API 已替换(v1.58+) — 请参阅 Toast Notifications。
add()/addAlert()已弃用;remove()/removeAll()已移除。 - Widget 值由存储支持(v1.53+) — 请参阅 Widget Rules(名称必须唯一、使用
removeWidget、node.widgets ??= [])。 input.link/output.links已弃用(v1.53+) — 读取仍可用,但会发出警告,且可能无法反映图的最新状态。写入不再改变连接,只有input.link = null仍会断开连接。请使用node.isInputConnected(i)、node.getInputLink(i)、node.isOutputConnected(i)、node.getOutputNodes(i)、node.connect()、node.disconnectInput(i)。node:slot-links:changed和node:slot-errors:changed图事件已移除。如需支持多个订阅者的添加/移除通知,请使用graph.events.addEventListener("node:added" | "node:before-removed" | "node:removed", ...)。onConfigure(data)接收的是浅拷贝(v1.55.2+) — 修改data的顶层键不会产生任何效果。请将自定义状态保存在node.properties/graph.extra中。- 核心节点字段已改为访问器(v1.53+) —
id、type、title、mode、flags、color、bgcolor、shape不再出现在Object.keys(node)/{...node}中;请使用node.serialize()。构造后修改node.type会发出警告。 node.size表示请求的尺寸 —getBounding()/boundingRect返回渲染后的占用范围(可能更大)。canvas.repositionNodesVueMode()→canvas.applyNodePositions()(旧名称仍作为已弃用的别名保留)。canvas.selected_nodes即将弃用 — 使用时会发出警告,并返回一个快照。请读取canvas.selectedItems(一个快照 Set;执行add/delete后需重新读取),并通过canvas.selectItems(items, addToCurrent?)更改选择。- 次要弃用项 —
node.badgePosition(徽章始终显示在右上角;赋值会被忽略)、node.widgets_up(请改用widgets_start_y)、options.canvasOnly(请改用options.surfaces)。 - 群组节点已移除 — 无法再创建群组节点;包含群组节点的工作流会在加载时自动转换为子图。
GroupNodeHandler/GroupNodeConfig仍为向后兼容而重新导出。新开发请使用子图。 LiteGraph.use_uuids已移除 — 节点 ID 始终为数字。请勿读取或设置此标志(UUID 仍在内部用于子图/插槽 ID)。LGraph.onGetNodeMenuOptions已移除;LGraph.onBeforeChange已弃用 — 赋值仍可用,但会发出警告;请改用LGraphCanvas.onBeforeChange。IBaseWidget.entityId已移除 — Widget 标识由带品牌标记的键widgetId(graphId:nodeId:name)表示;提升的子图 Widget 由存储支持。node.size由 Proxy 支持 — 现在无论使用node.size[1] = h还是node.size = [w, h],Vue 渲染的节点都会正确重新布局;无需轮询变通方案。
前端脚本 API
自定义节点 JavaScript 可以从前端的 src/scripts/ 模块导入内容。导入使用 Vite shim 模式:
import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js";
也可以通过 window.comfyAPI.<module>.<export> 访问符号。只有直接声明的导出(export const|let|var|function|class)会通过 shim 提供;像 export { x } from "..." 这样的重新导出无法通过此方式访问。
稳定性级别
| 级别 | 模块 | 说明 |
|---|---|---|
| 稳定 | scripts/app, scripts/api | 保证为公共 API |
| 内部(控制台警告) | scripts/widgets, scripts/domWidget, scripts/utils, scripts/pnginfo, scripts/changeTracker, scripts/defaultGraph, scripts/metadata/* | 可以使用,但可能会变更 |
| 已弃用 | scripts/ui | 将被移除;请使用 Vue 替代方案 |
关键模块
scripts/api—ComfyApi类:fetchApi()、queuePrompt()、getNodeDefs()、WebSocket 事件、设置、用户数据、系统统计信息scripts/app—ComfyApp单例(app):图操作、registerExtension()、extensionManager、剪贴板、坐标转换scripts/widgets—ComfyWidgets注册表(INT、FLOAT、STRING、BOOLEAN、COMBO、IMAGEUPLOAD 等)、addValueControlWidget(s)()、updateControlWidgetLabel()(自 v1.58 起不再导出IS_CONTROL_WIDGET)scripts/domWidget—addDOMWidget()、DOMWidgetImpl、ComponentWidgetImpl(Vue 组件封装)scripts/utils—clone()、addStylesheet()、uploadFile()、存储辅助工具scripts/pnginfo—getPngMetadata()、getWebpMetadata()、importA1111()、特定格式的提取器
完整的 API 详情请参阅 API 参考。
另请参阅
comfyui-node-basics- 后端节点结构comfyui-node-packaging- 包含 JS 扩展的项目结构comfyui-node-inputs- 后端输入类型