> ## Documentation Index
> Fetch the complete documentation index at: https://dripart-matt-router-pricing-pika-and-guard.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# ComfyUI 子图

> 在 ComfyUI 扩展中使用子图：节点 ID、图遍历、事件、控件提升和清理。

## 概述

子图让用户可以将节点分组为可复用、可嵌套的组件。每个子图都是一个独立的 `LGraph`，带有 UUID。面向用户的指南请参阅[子图](/zh/interface/features/subgraph)。

## 节点标识符

ComfyUI 使用三种不同的节点标识符类型。使用了错误的类型会导致静默失败。

| 类型 | 格式 | 用途 |
| - | - | - |
| `node.id` | `42`（数字） | 仅限于当前直接图层级。`graph.getNodeById(id)` |
| 执行 ID | `"1:2:3"`（冒号分隔的字符串） | 后端进度消息、`UNIQUE_ID` |
| 定位器 ID | `"<uuid>:<localId>"` 或 `"<localId>"` | UI 状态：徽章、错误、图像 |

从扩展内部构造节点的定位器 ID：

```javascript theme={null}
function getLocatorId(node) {
  const graphId = node.graph?.id
  return graphId ? `${graphId}:${node.id}` : String(node.id)
}
```

## 遍历节点

### 仅当前层级

```javascript theme={null}
for (const node of app.graph.nodes) {
  console.log(node.id, node.type)
}
```

### 递归遍历所有节点

要进入嵌套子图，使用递归辅助函数，对每个节点调用回调：

```javascript theme={null}
function walkGraph(graph, callback) {
  for (const node of graph.nodes ?? []) {
    callback(node, graph)
    if (node.subgraph) walkGraph(node.subgraph, callback)
  }
}
```

完整示例：

```javascript theme={null}
import { app } from "../../scripts/app.js"

function walkGraph(graph, callback) {
  for (const node of graph.nodes ?? []) {
    callback(node, graph)
    if (node.subgraph) walkGraph(node.subgraph, callback)
  }
}

app.registerExtension({
  name: "MyExtension.SubgraphWalker",
  async afterConfigureGraph() {
    walkGraph(app.graph, (node, graph) => {
      console.log(`[${graph.id ?? "root"}] node ${node.id}: ${node.type}`)
    })
  }
})
```

## 根图与当前活动图

| 需求 | 使用 |
| - | - |
| 对工作流中所有节点进行操作 | `app.graph`（根图） |
| 仅对当前可见层级进行操作 | `app.canvas?.graph` |
| 访问特定子图 | `someNode.subgraph` |

```javascript theme={null}
// 所有节点（包括嵌套子图）
walkGraph(app.graph, (node) => { /* ... */ })

// 仅用户当前可见的节点
for (const node of app.canvas?.graph?.nodes ?? []) { /* ... */ }
```

## 事件

### 子图层级事件

通过 `subgraph.events` 分发：

| 事件 | 载荷 | 触发时机 |
| - | - | - |
| `widget-promoted` | `{ widget, subgraphNode }` | 控件提升到父节点 |
| `widget-demoted` | `{ widget, subgraphNode }` | 控件从父节点移除 |
| `input-added` | `{ input }` | 输入槽添加 |
| `removing-input` | `{ input, index }` | 输入槽正在被移除 |
| `output-added` | `{ output }` | 输出槽添加 |
| `removing-output` | `{ output, index }` | 输出槽正在被移除 |
| `renaming-input` | `{ input, index, oldName, newName }` | 输入槽重命名 |
| `renaming-output` | `{ output, index, oldName, newName }` | 输出槽重命名 |

### 画布层级事件

通过 `app.canvas.canvas`（HTML canvas 元素）分发：

| 事件 | 载荷 | 触发时机 |
| - | - | - |
| `subgraph-opened` | `{ subgraph, closingGraph, fromNode }` | 用户导航进入一个子图 |
| `subgraph-converted` | `{ subgraphNode }` | 选区被转换为子图 |

### 监听模式

```javascript theme={null}
import { app } from "../../scripts/app.js"

app.registerExtension({
  name: "MyExtension.SubgraphEvents",
  async setup() {
    app.canvas.canvas.addEventListener("subgraph-opened", (e) => {
      const { subgraph, fromNode } = e.detail
      console.log(`Opened subgraph from node ${fromNode.id}`)
    })
  }
})
```

## 控件提升

当 `SubgraphInput` 连接到子图内部的某个控件时，该控件的一个副本会出现在父级子图节点上。这会触发 `widget-promoted` 事件。断开连接则会触发 `widget-demoted`。

<Warning>
  控件提升行为仍在演进中，未来版本可能会发生变化。
</Warning>

```javascript theme={null}
import { app } from "../../scripts/app.js"

function walkGraph(graph, callback) {
  for (const node of graph.nodes ?? []) {
    callback(node, graph)
    if (node.subgraph) walkGraph(node.subgraph, callback)
  }
}

app.registerExtension({
  name: "MyExtension.WidgetPromotion",
  async afterConfigureGraph() {
    walkGraph(app.graph, (node) => {
      if (!node.subgraph) return
      if (node._promCleanup) node._promCleanup.abort()
      const controller = new AbortController()
      node._promCleanup = controller
      const { signal } = controller

      node.subgraph.events.addEventListener("widget-promoted", (e) => {
        console.log(`Widget "${e.detail.widget.name}" promoted`)
      }, { signal })

      node.subgraph.events.addEventListener("widget-demoted", (e) => {
        console.log(`Widget "${e.detail.widget.name}" demoted`)
      }, { signal })

      const origRemoved = node.onRemoved
      node.onRemoved = function () {
        controller.abort()
        origRemoved?.apply(this, arguments)
      }
    })
  }
})
```

## 清理

使用 `AbortController` 在节点被移除时清理所有事件监听器。

```javascript theme={null}
import { app } from "../../scripts/app.js"

app.registerExtension({
  name: "MyExtension.Cleanup",
  async nodeCreated(node) {
    if (!node.subgraph) return

    const controller = new AbortController()
    const { signal } = controller

    node.subgraph.events.addEventListener("input-added", (e) => {
      console.log(`Input added: ${e.detail.input.name}`)
    }, { signal })

    node.subgraph.events.addEventListener("removing-input", (e) => {
      console.log(`Input removing: ${e.detail.input.name}`)
    }, { signal })

    const origRemoved = node.onRemoved
    node.onRemoved = function () {
      controller.abort()
      origRemoved?.apply(this, arguments)
    }
  }
})
```

<Tip>
  `onRemoved` 不仅会在删除时触发，也可能在子图转换时触发。如果需要在重构过程中保留状态，请对销毁逻辑进行防护。
</Tip>

## 参见

* [子图（用户指南）](/zh/interface/features/subgraph)
* [扩展钩子](/zh/custom-nodes/js/javascript_hooks)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.