mcpField
C
comfyui-node-frontend

comfyui-node-frontend

github.com/jtydhr88開発308126 回インストール2 週間前に更新
リポジトリ

comfyui-node-frontend について

comfyui-node-frontendは、JavaScriptでComfyUIのフロントエンドを拡張するカスタムノード開発者向けガイドです。ライフサイクルフック、ウィジェット、コマンド、設定、グラフ読み込み、サーバーとの通信を扱います。

主な機能

  1. 01拡張機能とライフサイクルフックの登録
  2. 02ウィジェットと画面タブの作成
  3. 03コマンド、キー割り当て、設定の追加
  4. 04実行イベントとグラフ読み込みの処理
  5. 05サーバーとフロントエンド間の通信

活用シーン

  1. 01カスタムノードへの操作コントロール追加
  2. 02ComfyUIへの拡張パネルやコマンドの追加
  3. 03ノード実行やグラフ読み込みへの対応
  4. 04Pythonサーバーのイベントをフロントエンドへ送信

インストール

コマンドをコピーしてターミナルで実行します。

$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。読み込みに失敗した場合は、after* フックの代わりに onGraphLoadError が実行されます。

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.widgets の配列を直接編集するのではなく、node.removeWidget(widget) で削除してください。
  • 最初に割り当てるまでは、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"],
    },
],
About ページのバッジ
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 も引き続き有効です。

トースト通知

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 */ };

最近の破壊的変更(フロントエンド 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() は削除されました。
  • ウィジェットの値をストアで管理(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 を削除 — ウィジェットの識別には、ブランド付きキー widgetId(graphId:nodeId:name)を使用します。昇格されたサブグラフウィジェットはストアで管理されます。
  • node.size は Proxy で管理 — node.size[1] = h と node.size = [w, h] のどちらも、Vue で描画されたノードのレイアウトを正しく再計算するようになりました。ポーリングによる回避策は不要です。

フロントエンドスクリプト API

カスタムノードの JavaScript から、フロントエンドの src/scripts/ モジュールをインポートできます。インポートには Vite のシムパターンを使用します。

import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js";

シンボルには window.comfyAPI.<module>.<export> からもアクセスできます。直接宣言されたエクスポート(export const|let|var|function|class)のみがシム化されます。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 - バックエンドの入力タイプ

コメント