diff --git a/nodes.py b/nodes.py index 231d4d4de..4fe7062ba 100644 --- a/nodes.py +++ b/nodes.py @@ -2078,6 +2078,7 @@ EXTENSION_WEB_DIRS = {} # Dictionary of successfully loaded module names and associated directories. LOADED_MODULE_DIRS = {} +LOADED_CUSTOM_NODES = {} def get_module_name(module_path: str) -> str: """ @@ -2103,6 +2104,8 @@ def get_module_name(module_path: str) -> str: def load_custom_node(module_path: str, ignore=set(), module_parent="custom_nodes") -> bool: module_name = get_module_name(module_path) + project_name = module_name + if os.path.isfile(module_path): sp = os.path.splitext(module_path) module_name = sp[0] @@ -2110,6 +2113,15 @@ def load_custom_node(module_path: str, ignore=set(), module_parent="custom_nodes elif os.path.isdir(module_path): sys_module_name = module_path.replace(".", "_x_") + is_custom_node = module_parent == "custom_nodes" + + if is_custom_node: + LOADED_CUSTOM_NODES[project_name] = { + "project_name": project_name, + "status": "loading", + "error": None + } + try: logging.debug("Trying to load custom node {}".format(module_path)) if os.path.isfile(module_path): @@ -2130,14 +2142,20 @@ def load_custom_node(module_path: str, ignore=set(), module_parent="custom_nodes project_config = config_parser.extract_node_configuration(module_path) + project_name_from_config = project_config.project.name + + if is_custom_node and project_name_from_config != project_name: + LOADED_CUSTOM_NODES[project_name_from_config] = LOADED_CUSTOM_NODES.pop(project_name) + LOADED_CUSTOM_NODES[project_name_from_config]["project_name"] = project_name_from_config + + project_name = project_name_from_config + web_dir_name = project_config.tool_comfy.web if web_dir_name: web_dir_path = os.path.join(module_path, web_dir_name) if os.path.isdir(web_dir_path): - project_name = project_config.project.name - EXTENSION_WEB_DIRS[project_name] = web_dir_path logging.info("Automatically register web folder {} for {}".format(web_dir_name, project_name)) @@ -2150,17 +2168,39 @@ def load_custom_node(module_path: str, ignore=set(), module_parent="custom_nodes EXTENSION_WEB_DIRS[module_name] = web_dir if hasattr(module, "NODE_CLASS_MAPPINGS") and getattr(module, "NODE_CLASS_MAPPINGS") is not None: + node_count = 0 for name, node_cls in module.NODE_CLASS_MAPPINGS.items(): if name not in ignore: NODE_CLASS_MAPPINGS[name] = node_cls node_cls.RELATIVE_PYTHON_MODULE = "{}.{}".format(module_parent, get_module_name(module_path)) + node_count += 1 + if hasattr(module, "NODE_DISPLAY_NAME_MAPPINGS") and getattr(module, "NODE_DISPLAY_NAME_MAPPINGS") is not None: NODE_DISPLAY_NAME_MAPPINGS.update(module.NODE_DISPLAY_NAME_MAPPINGS) + + if is_custom_node: + LOADED_CUSTOM_NODES[project_name].update({ + "status": "loaded", + "node_count": node_count, + "nodes": list(module.NODE_CLASS_MAPPINGS.keys()) + }) + logging.info(f"Successfully loaded custom node: {project_name} with {node_count} nodes") + return True else: + if is_custom_node: + LOADED_CUSTOM_NODES[project_name].update({ + "status": "skipped", + "error": "No NODE_CLASS_MAPPINGS found" + }) logging.warning(f"Skip {module_path} module for custom nodes due to the lack of NODE_CLASS_MAPPINGS.") return False except Exception as e: + if is_custom_node: + LOADED_CUSTOM_NODES[project_name].update({ + "status": "failed", + "error": str(e) + }) logging.warning(traceback.format_exc()) logging.warning(f"Cannot import {module_path} module for custom nodes: {e}") return False diff --git a/server.py b/server.py index f4de0079b..43c770f2b 100644 --- a/server.py +++ b/server.py @@ -748,6 +748,127 @@ class PromptServer(): return web.Response(status=200) + @routes.get("/custom_nodes") + async def get_custom_nodes(request): + """ + { + "custom_nodes": [ + { + "project_name": "node_1", + "status": "loaded|failed|skipped|loading", + "node_count": 5, + "nodes": ["NodeClass1", "NodeClass2"], + "error": "error message if failed" + } + ], + "total_count": 10, + "loaded_count": 8, + "failed_count": 1, + "skipped_count": 1 + } + """ + try: + custom_nodes_list = [] + loaded_count = 0 + failed_count = 0 + skipped_count = 0 + + for project_name, node_info in nodes.LOADED_CUSTOM_NODES.items(): + node_data = { + "project_name": node_info.get("project_name", project_name), + "status": node_info.get("status", "unknown") + } + + if node_info.get("status") == "loaded": + node_data.update({ + "node_count": node_info.get("node_count", 0), + "nodes": node_info.get("nodes", []) + }) + loaded_count += 1 + elif node_info.get("status") == "failed": + node_data["error"] = node_info.get("error", "Unknown error") + failed_count += 1 + elif node_info.get("status") == "skipped": + node_data["error"] = node_info.get("error", "No NODE_CLASS_MAPPINGS found") + skipped_count += 1 + + custom_nodes_list.append(node_data) + + response_data = { + "custom_nodes": custom_nodes_list, + "total_count": len(custom_nodes_list), + "loaded_count": loaded_count, + "failed_count": failed_count, + "skipped_count": skipped_count + } + + return web.json_response(response_data) + + except Exception as e: + logging.error(f"Error in get_custom_nodes: {e}") + logging.error(traceback.format_exc()) + return web.json_response({ + "error": f"Failed to get custom nodes: {str(e)}", + "custom_nodes": [], + "total_count": 0, + "loaded_count": 0, + "failed_count": 0, + "skipped_count": 0 + }, status=500) + + @routes.get("/custom_node/{project_name}") + async def get_custom_node_by_name(request): + try: + project_name = request.match_info["project_name"] + + if not project_name: + return web.json_response({ + "error": "Project name is required", + "message": "Please provide a valid project name in the URL path" + }, status=400) + + import urllib.parse + project_name = urllib.parse.unquote(project_name) + + if project_name not in nodes.LOADED_CUSTOM_NODES: + return web.json_response({ + "error": f"Custom node '{project_name}' not found", + "message": f"The custom node '{project_name}' is not loaded or does not exist" + }, status=404) + + node_info = nodes.LOADED_CUSTOM_NODES[project_name] + + response_data = { + "project_name": node_info.get("project_name", project_name), + "status": node_info.get("status", "unknown") + } + + if node_info.get("status") == "loaded": + response_data.update({ + "node_count": node_info.get("node_count", 0), + "nodes": node_info.get("nodes", []) + }) + elif node_info.get("status") in ["failed", "skipped"]: + response_data["error"] = node_info.get("error", "Unknown error") + + return web.json_response(response_data) + + except KeyError as e: + logging.error(f"Missing parameter in get_custom_node_by_name: {e}") + return web.json_response({ + "error": "Invalid request", + "message": "Required parameter is missing from the request" + }, status=400) + + except Exception as e: + logging.error(f"Error in get_custom_node_by_name: {e}") + logging.error(traceback.format_exc()) + return web.json_response({ + "error": f"Failed to get custom node information: {str(e)}", + "message": "An internal error occurred while retrieving the custom node information" + }, status=500) + + async def setup(self): timeout = aiohttp.ClientTimeout(total=None) # no timeout self.client_session = aiohttp.ClientSession(timeout=timeout)