mcpField
C
comfyui-node-frontend

comfyui-node-frontend

github.com/jtydhr88开发308126 次安装2 周前更新
代码仓库

关于 comfyui-node-frontend

comfyui-node-frontend 是为 ComfyUI 自定义节点开发者准备的前端扩展指南,说明如何用 JavaScript 扩展界面和节点行为。内容覆盖生命周期钩子、控件、命令、设置及图表加载事件。

主要功能

  1. 01注册前端扩展与生命周期钩子
  2. 02创建自定义控件和界面标签页
  3. 03添加命令、快捷键与设置
  4. 04监听执行事件并处理图表加载
  5. 05通过 API 与服务端通信

适用场景

  1. 01为自定义节点添加交互控件
  2. 02在 ComfyUI 中加入扩展面板和命令
  3. 03响应节点执行状态或图表加载
  4. 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,以及即将推出的版本)

扩展作者应注意的变更。上游迁移说明详见:https://github.com/Comfy-Org/ComfyUI_frontend/tree/main/docs/extensions(`widgets-migration.md`、`connection-callbacks-migration.md`、`serialization-callbacks-migration.md`、`node-geometry-migration.md`、`link-registration-migration.md`、`primevue-removal-migration.md`、`tooltip-migration.md`)。

  • 即将移除 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 - 后端输入类型

评论

支持 Markdown