fix(browser): limpiar previews/outputs residuales al cargar workflow ComfyUI

app.loadApiJson (lo que usa comfyui_load_workflow_ui) reconstruye el grafo pero
no llama a app.clean(), por lo que no resetea el store app.nodeOutputs ni los
previews de los nodos. Cuando un workflow nuevo reusa un node_id existente en el
store, el preview cacheado del workflow anterior se re-pinta sobre el nodo nuevo
(visto: imagen 3D pegada bajo un CheckpointLoaderSimple/SaveGLB).

- Nueva funcion comfyui_clear_node_outputs_ui: limpieza no destructiva del store
  app.nodeOutputs + node.imgs/images, sin tocar la topologia del grafo.
- comfyui_load_workflow_ui v1.1.0: anade clear_outputs=True (default) que invoca
  la limpieza antes de loadApiJson, replicando la garantia de loadGraphData.

Reproducido y verificado en la UI real (CDP 9222) con evidencia antes/despues.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-24 17:37:07 +02:00
parent 6d1b66167d
commit 5f0df32728
4 changed files with 207 additions and 4 deletions
@@ -0,0 +1,67 @@
---
name: comfyui_clear_node_outputs_ui
kind: function
lang: py
domain: browser
version: "1.0.0"
purity: impure
signature: "def comfyui_clear_node_outputs_ui(*, port: int = 9222, server_url_substr: str = '8188', timeout_s: float = 15.0) -> dict"
description: "Limpia outputs/previews residuales de TODOS los nodos del grafo de ComfyUI en la UI via CDP: vacia app.nodeOutputs (store de previews keyed by node_id) y borra imgs/images de cada nodo vivo, sin tocar la topologia del grafo (no borra nodos ni links). Arregla el bug de imagenes pegadas a nodos que no corresponden tras cargar un workflow nuevo con app.loadApiJson. Compone cdp_eval. Impura: red (CDP) + muta la UI."
tags: [comfyui, browser, cdp, ml, ui-automation, image-generation]
uses_functions: ["cdp_eval_py_browser"]
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- name: port
desc: "Puerto de remote debugging del Chrome diario. Default 9222."
- name: server_url_substr
desc: "Substring de la URL de la pestana de ComfyUI (default '8188', el puerto del server). Identifica la pestana entre las abiertas."
- name: timeout_s
desc: "Timeout de la conexion CDP en segundos. Default 15.0."
output: "dict {ok, cleared, error, store_cleared, nodes_touched, nodes}. ok/cleared True si la limpieza termino sin excepcion. store_cleared = entradas borradas de app.nodeOutputs; nodes_touched = nodos a los que se les quito un preview; nodes = total de nodos del grafo."
tested: false
tests: []
test_file_path: ""
file_path: "python/functions/browser/comfyui_clear_node_outputs_ui.py"
---
## Ejemplo
```python
import sys, os
sys.path.insert(0, os.path.join("python", "functions"))
from browser.comfyui_clear_node_outputs_ui import comfyui_clear_node_outputs_ui
# Requiere la UI de ComfyUI abierta en el Chrome con CDP en el puerto 9222.
print(comfyui_clear_node_outputs_ui())
# -> {'ok': True, 'cleared': True, 'error': '', 'store_cleared': 6, 'nodes_touched': 2, 'nodes': 12}
```
## Cuando usarla
Cuando ves previews/outputs de imagenes pegados a nodos que no los produjeron
(una imagen bajo un `CheckpointLoaderSimple`, un `SaveGLB`, etc.) tras haber
cargado varios workflows seguidos en la misma pestana. Es la limpieza no
destructiva: borra los previews residuales del grafo actual SIN recargarlo ni
perder la topologia. `comfyui_load_workflow_ui(..., clear_outputs=True)` la
invoca automaticamente antes de cargar, asi que normalmente no hace falta
llamarla a mano; usala solo para limpiar un grafo ya cargado sin recargarlo.
## Gotchas
- Requiere la pestana de ComfyUI abierta en un Chrome con
`--remote-debugging-port=9222`. Si no hay target que matchee
`server_url_substr`, `cdp_eval` devuelve error y aqui `ok=False`.
- Borra TODOS los previews del grafo, incluidos los legitimos de la ultima
ejecucion. Si quieres conservar un preview concreto, no la llames; el residuo
cross-workflow se evita de raiz cargando con
`comfyui_load_workflow_ui(..., clear_outputs=True)`.
- No es `app.clean()`: a proposito NO hace `rootGraph.clear()`, por eso es
segura sobre el grafo vivo del usuario (no borra nodos ni conexiones).
- El store que vacia es `app.nodeOutputs`; el nombre interno puede variar entre
versiones de ComfyUI. Si una version renombra el store, el borrado del store
no aplica pero el barrido de `node.imgs`/`node.images` sigue limpiando los
previews visibles.
@@ -0,0 +1,105 @@
"""Limpia los outputs/previews residuales de los nodos de ComfyUI en la UI via CDP.
ComfyUI cachea los outputs de cada ejecucion en `app.nodeOutputs`, un store
indexado por node_id. La ruta de carga `app.loadApiJson` (la que usa
comfyui_load_workflow_ui) reconstruye el grafo pero NO resetea ese store ni los
previews de los nodos. Cuando un workflow nuevo reusa un node_id que ya existia
en el store, el preview cacheado del workflow anterior se vuelve a pintar sobre
el nodo nuevo, que muchas veces es de otro tipo (ej. una imagen pegada bajo un
`CheckpointLoaderSimple` o un `SaveGLB`).
Esta funcion vacia `app.nodeOutputs` y borra `imgs`/`images` de todos los nodos
vivos del grafo, sin tocar la topologia del grafo (no borra nodos ni links), y
marca el canvas dirty para repintar. Es la version no destructiva de
`app.clean()` (que ademas haria `rootGraph.clear()`).
Funcion impura: hace red (CDP WebSocket) y muta el estado de la UI.
"""
try: # ejecucion directa del archivo / fn run (browser/ en sys.path[0])
from cdp_eval import cdp_eval
except ImportError: # importado como paquete (sys.path = python/functions)
from browser.cdp_eval import cdp_eval
def comfyui_clear_node_outputs_ui(
*,
port: int = 9222,
server_url_substr: str = "8188",
timeout_s: float = 15.0,
) -> dict:
"""Limpia previews/outputs residuales de todos los nodos del grafo de ComfyUI.
Args:
port: puerto de remote debugging del Chrome diario. Default 9222.
server_url_substr: substring de la URL de la pestana de ComfyUI (default
"8188", el puerto del server). Identifica la pestana entre las
abiertas.
timeout_s: timeout de la conexion CDP en segundos.
Returns:
dict {ok: bool, cleared: bool, error: str, store_cleared: int,
nodes_touched: int, nodes: int}. ok/cleared True si la limpieza termino
sin excepcion en la pagina. `store_cleared` es el numero de entradas
eliminadas de `app.nodeOutputs`; `nodes_touched` los nodos a los que se
les quito un preview; `nodes` el total de nodos del grafo.
"""
expr = (
"(function(){"
" if(!window.app){ return {ok:false, cleared:false, error:'window.app no disponible en la pestana'}; }"
" try{"
" var store=0;"
" if(app.nodeOutputs){ for(var k in app.nodeOutputs){ if(Object.prototype.hasOwnProperty.call(app.nodeOutputs,k)){ delete app.nodeOutputs[k]; store++; } } }"
" var nodes=(app.graph && app.graph._nodes)? app.graph._nodes : [];"
" var touched=0;"
" for(var i=0;i<nodes.length;i++){"
" var nd=nodes[i];"
" if(nd.imgs!==undefined || nd.images!==undefined){ touched++; }"
" nd.imgs=undefined;"
" nd.images=undefined;"
" nd.imageIndex=null;"
" nd.overIndex=null;"
" if('animatedImages' in nd){ nd.animatedImages=undefined; }"
" }"
" if(app.graph && app.graph.setDirtyCanvas){ app.graph.setDirtyCanvas(true,true); }"
" return {ok:true, cleared:true, error:'', store_cleared:store, nodes_touched:touched, nodes:nodes.length};"
" }catch(e){ return {ok:false, cleared:false, error:String(e)}; }"
"})()"
)
r = cdp_eval(
expr,
port=port,
target_url_substr=server_url_substr,
await_promise=False,
timeout_s=timeout_s,
)
if not r["ok"]:
return {
"ok": False,
"cleared": False,
"error": r["error"],
"store_cleared": 0,
"nodes_touched": 0,
"nodes": 0,
}
val = r["value"] or {}
return {
"ok": bool(val.get("cleared")),
"cleared": bool(val.get("cleared")),
"error": val.get("error", ""),
"store_cleared": int(val.get("store_cleared", 0)),
"nodes_touched": int(val.get("nodes_touched", 0)),
"nodes": int(val.get("nodes", 0)),
}
if __name__ == "__main__":
import json
print(
json.dumps(
comfyui_clear_node_outputs_ui(),
ensure_ascii=False,
indent=2,
)
)
@@ -3,12 +3,12 @@ name: comfyui_load_workflow_ui
kind: function kind: function
lang: py lang: py
domain: browser domain: browser
version: "1.0.0" version: "1.1.0"
purity: impure purity: impure
signature: "def comfyui_load_workflow_ui(workflow: dict, *, port: int = 9222, server_url_substr: str = '8188', filename: str = 'workflow.json', timeout_s: float = 20.0) -> dict" signature: "def comfyui_load_workflow_ui(workflow: dict, *, port: int = 9222, server_url_substr: str = '8188', filename: str = 'workflow.json', clear_outputs: bool = True, timeout_s: float = 20.0) -> dict"
description: "Carga un workflow ComfyUI (API format) en la UI del navegador via CDP: inyecta app.loadApiJson(<workflow>, filename) en la pestana de ComfyUI abierta y reconstruye el grafo visual. Compone cdp_eval (transport CDP). Impura: red (CDP WebSocket) + muta el grafo de la UI." description: "Carga un workflow ComfyUI (API format) en la UI del navegador via CDP: inyecta app.loadApiJson(<workflow>, filename) en la pestana de ComfyUI abierta y reconstruye el grafo visual. Por defecto (clear_outputs=True) limpia antes los previews/outputs residuales para que un preview cacheado del workflow anterior no se pegue a un nodo nuevo que reusa el mismo node_id. Compone cdp_eval + comfyui_clear_node_outputs_ui. Impura: red (CDP WebSocket) + muta el grafo de la UI."
tags: [comfyui, browser, cdp, ml, image-generation, stable-diffusion, ui-automation] tags: [comfyui, browser, cdp, ml, image-generation, stable-diffusion, ui-automation]
uses_functions: ["cdp_eval_py_browser"] uses_functions: ["cdp_eval_py_browser", "comfyui_clear_node_outputs_ui_py_browser"]
uses_types: [] uses_types: []
returns: [] returns: []
returns_optional: false returns_optional: false
@@ -23,6 +23,8 @@ params:
desc: "Substring de la URL de la pestana de ComfyUI (default '8188', el puerto del server). Identifica la pestana entre las abiertas." desc: "Substring de la URL de la pestana de ComfyUI (default '8188', el puerto del server). Identifica la pestana entre las abiertas."
- name: filename - name: filename
desc: "Nombre que ComfyUI asocia al workflow cargado. Default 'workflow.json'." desc: "Nombre que ComfyUI asocia al workflow cargado. Default 'workflow.json'."
- name: clear_outputs
desc: "Si True (default) limpia previews/outputs residuales (app.nodeOutputs + node.imgs) antes de cargar, evitando que un preview cacheado de un workflow anterior se pegue a un nodo nuevo que reusa el mismo node_id. False conserva los previews previos a proposito."
- name: timeout_s - name: timeout_s
desc: "Timeout de la conexion CDP en segundos. Default 20.0." desc: "Timeout de la conexion CDP en segundos. Default 20.0."
output: "dict {ok: bool, loaded: bool, error: str}. ok/loaded True si app.loadApiJson termino sin excepcion en la pagina." output: "dict {ok: bool, loaded: bool, error: str}. ok/loaded True si app.loadApiJson termino sin excepcion en la pagina."
@@ -66,3 +68,17 @@ editarlo en la UI del navegador del usuario antes de encolarlo. Es el puente
necesitas. necesitas.
- Espera la Promise de carga (`await_promise=True`). El conteo de nodos cargados - Espera la Promise de carga (`await_promise=True`). El conteo de nodos cargados
se puede verificar con `cdp_eval("app.graph._nodes.length", target_url_substr="8188")`. se puede verificar con `cdp_eval("app.graph._nodes.length", target_url_substr="8188")`.
- `app.loadApiJson` (a diferencia de la ruta del menu `app.loadGraphData`) NO
llama a `app.clean()`, asi que NO resetea el store `app.nodeOutputs` ni los
previews de los nodos. Sin `clear_outputs=True`, un preview cacheado de un
workflow anterior se re-pinta sobre el nodo nuevo que reuse el mismo node_id
(visto: imagen 3D pegada bajo un `CheckpointLoaderSimple`/`SaveGLB`). El
default `clear_outputs=True` lo evita delegando en
`comfyui_clear_node_outputs_ui`.
## Capability growth log
- v1.1.0 (2026-06-24) — anade `clear_outputs=True` (default): limpia los
previews/outputs residuales (`app.nodeOutputs` + `node.imgs`) antes de cargar,
delegando en `comfyui_clear_node_outputs_ui`. Fija el bug de imagenes
residuales pegadas a nodos que reusan node_id entre workflows.
@@ -11,8 +11,10 @@ import json
try: # ejecucion directa del archivo / fn run (browser/ en sys.path[0]) try: # ejecucion directa del archivo / fn run (browser/ en sys.path[0])
from cdp_eval import cdp_eval from cdp_eval import cdp_eval
from comfyui_clear_node_outputs_ui import comfyui_clear_node_outputs_ui
except ImportError: # importado como paquete (sys.path = python/functions) except ImportError: # importado como paquete (sys.path = python/functions)
from browser.cdp_eval import cdp_eval from browser.cdp_eval import cdp_eval
from browser.comfyui_clear_node_outputs_ui import comfyui_clear_node_outputs_ui
def comfyui_load_workflow_ui( def comfyui_load_workflow_ui(
@@ -21,6 +23,7 @@ def comfyui_load_workflow_ui(
port: int = 9222, port: int = 9222,
server_url_substr: str = "8188", server_url_substr: str = "8188",
filename: str = "workflow.json", filename: str = "workflow.json",
clear_outputs: bool = True,
timeout_s: float = 20.0, timeout_s: float = 20.0,
) -> dict: ) -> dict:
"""Carga un workflow API format en el grafo de la UI de ComfyUI. """Carga un workflow API format en el grafo de la UI de ComfyUI.
@@ -33,12 +36,24 @@ def comfyui_load_workflow_ui(
"8188", el puerto del server). Identifica la pestana entre todas las "8188", el puerto del server). Identifica la pestana entre todas las
abiertas. abiertas.
filename: nombre que ComfyUI asocia al workflow cargado. filename: nombre que ComfyUI asocia al workflow cargado.
clear_outputs: si True (default), limpia los previews/outputs residuales
(app.nodeOutputs + node.imgs) ANTES de cargar, replicando lo que hace
la ruta del menu (app.clean()). Evita que un preview cacheado de un
workflow anterior se pegue a un nodo nuevo que reusa el mismo node_id
(bug de imagenes residuales). Ponlo en False solo si quieres conservar
a proposito los previews del grafo previo.
timeout_s: timeout de la conexion CDP en segundos. timeout_s: timeout de la conexion CDP en segundos.
Returns: Returns:
dict {ok: bool, loaded: bool, error: str}. ok/loaded True si dict {ok: bool, loaded: bool, error: str}. ok/loaded True si
app.loadApiJson termino sin excepcion en la pagina. app.loadApiJson termino sin excepcion en la pagina.
""" """
if clear_outputs:
comfyui_clear_node_outputs_ui(
port=port,
server_url_substr=server_url_substr,
timeout_s=timeout_s,
)
expr = ( expr = (
"(async function(){" "(async function(){"
" if(!window.app || typeof app.loadApiJson!=='function'){" " if(!window.app || typeof app.loadApiJson!=='function'){"