Files
1hew d1dbe3ddfa v3.15.0 chore(release): 发布 Load PS 与批处理节点更新
整合 Load PS、批处理取样节点重构与文本列表解析增强,统一这次用户可见功能调整的发布入口。
2026-05-09 00:19:31 +08:00

282 lines
10 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import os
import sys
import time
import logging
import threading
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
# ==================== 自动配置区 ====================
# 自动检测ComfyUI根目录(从utils/workflow目录向上查找)
current_dir = os.path.dirname(os.path.abspath(__file__))
UTILS_ROOT = os.path.dirname(current_dir) # utils目录
PLUGIN_ROOT = os.path.dirname(UTILS_ROOT) # ComfyUI-1hewNodes目录
COMFYUI_ROOT = os.path.dirname(os.path.dirname(PLUGIN_ROOT)) # 从ComfyUI-1hewNodes向上两级到ComfyUI根目录
# 自动检测可能的工作流目录
POSSIBLE_WORKFLOW_DIRS = [
os.path.join(COMFYUI_ROOT, "user", "default", "workflows"),
os.path.join(COMFYUI_ROOT, "workflows"),
os.path.join(COMFYUI_ROOT, "user", "workflows"),
os.path.join(os.path.expanduser("~"), "ComfyUI", "workflows"),
]
# 临时文件路径(保存在utils/workflow文件夹中)
TEMP_FILE_PATH = os.path.join(current_dir, "current_workflow.tmp")
# 监控日志级别(DEBUG/INFO/WARNING)
LOG_LEVEL = logging.INFO
# ================================================
# 配置日志
logging.basicConfig(
level=LOG_LEVEL,
format="%(asctime)s - %(levelname)s - %(message)s",
datefmt="%H:%M:%S"
)
def _safe_log_text(value):
"""Keep workflow paths intact, but make console logging encoding-safe."""
text = str(value)
encoding = getattr(getattr(sys, "stdout", None), "encoding", None) or "utf-8"
try:
text.encode(encoding)
return text
except UnicodeEncodeError:
return text.encode(encoding, errors="backslashreplace").decode(encoding, errors="replace")
class WorkflowFileHandler(FileSystemEventHandler):
def __init__(self):
self.last_updated = 0 # 用于过滤短时间内的重复触发
self.min_interval = 0.5 # 减少最小触发间隔(秒)
self.last_file = "" # 记录上次处理的文件,避免重复处理
self.workflow_root = None # 缓存工作流根目录
def _get_workflow_root(self):
"""获取当前使用的工作流根目录"""
if self.workflow_root is None:
for workflow_dir in POSSIBLE_WORKFLOW_DIRS:
if os.path.exists(workflow_dir) and os.path.isdir(workflow_dir):
self.workflow_root = workflow_dir
break
return self.workflow_root
def _get_relative_path(self, file_path):
"""获取相对于workflows目录的路径"""
workflow_root = self._get_workflow_root()
if workflow_root is None:
return os.path.basename(file_path)
# 标准化路径分隔符
file_path = os.path.abspath(file_path).replace("\\", "/")
workflow_root = os.path.abspath(workflow_root).replace("\\", "/")
# 检查文件是否在workflows目录下
if file_path.startswith(workflow_root):
# 获取相对路径
relative_path = file_path[len(workflow_root):].lstrip("/")
return relative_path
else:
# 如果不在workflows目录下,只返回文件名
return os.path.basename(file_path)
def _update_temp_file(self, file_path, event_type="unknown"):
"""更新临时文件的通用方法"""
current_time = time.time()
relative_path = self._get_relative_path(file_path)
# 避免重复处理同一文件
if (current_time - self.last_updated > self.min_interval
and relative_path != self.last_file):
self.last_updated = current_time
self.last_file = relative_path
# 写入临时文件
try:
with open(TEMP_FILE_PATH, "w", encoding="utf-8") as f:
f.write(relative_path)
logging.info(f"[{event_type}] 已更新工作流路径:{_safe_log_text(relative_path)}")
except Exception as e:
logging.error(f"写入临时文件失败:{str(e)}")
def on_modified(self, event):
"""处理文件修改事件(保存操作会触发)"""
if (not event.is_directory
and event.src_path.endswith(".json")
and os.path.abspath(event.src_path) != os.path.abspath(TEMP_FILE_PATH)):
self._update_temp_file(event.src_path, "修改")
def on_moved(self, event):
"""处理文件移动/重命名事件"""
if (not event.is_directory
and event.dest_path.endswith(".json")
and os.path.abspath(event.dest_path) != os.path.abspath(TEMP_FILE_PATH)):
self._update_temp_file(event.dest_path, "移动")
def on_created(self, event):
"""处理文件创建事件"""
if (not event.is_directory
and event.src_path.endswith(".json")
and os.path.abspath(event.src_path) != os.path.abspath(TEMP_FILE_PATH)):
self._update_temp_file(event.src_path, "创建")
def on_opened(self, event):
"""处理文件打开事件(如果支持)"""
if (hasattr(event, 'src_path') and not event.is_directory
and event.src_path.endswith(".json")
and os.path.abspath(event.src_path) != os.path.abspath(TEMP_FILE_PATH)):
self._update_temp_file(event.src_path, "打开")
def find_workflow_directory():
"""自动查找工作流目录"""
for workflow_dir in POSSIBLE_WORKFLOW_DIRS:
if os.path.exists(workflow_dir) and os.path.isdir(workflow_dir):
# logging.info(f"找到工作流目录:{workflow_dir}")
return workflow_dir
# 如果都不存在,创建默认目录
default_dir = POSSIBLE_WORKFLOW_DIRS[0]
try:
os.makedirs(default_dir, exist_ok=True)
logging.info(f"创建默认工作流目录:{default_dir}")
return default_dir
except Exception as e:
logging.error(f"无法创建工作流目录:{str(e)}")
return None
def validate_workflow_dir():
"""验证工作流目录是否存在"""
workflow_root = find_workflow_directory()
if workflow_root is None:
logging.error("无法找到或创建工作流目录")
logging.info("可能的目录位置:")
for dir_path in POSSIBLE_WORKFLOW_DIRS:
logging.info(f" - {dir_path}")
return False, None
return True, workflow_root
# 全局变量管理监控状态
_observer = None
_monitor_thread = None
_is_monitoring = False
def _monitor_worker():
"""监控工作线程"""
global _observer, _is_monitoring
# 验证并获取工作流目录
is_valid, workflow_root = validate_workflow_dir()
if not is_valid:
logging.error("工作流监控启动失败:无法找到工作流目录")
return
event_handler = WorkflowFileHandler()
_observer = Observer()
# 监控目标目录及所有子目录(recursive=True)
_observer.schedule(event_handler, workflow_root, recursive=True)
try:
_observer.start()
_is_monitoring = True
# 保持监控运行
while _is_monitoring:
time.sleep(1)
except Exception as e:
logging.error(f"[ComfyUI-1hewNodes] 工作流监控启动失败:{str(e)}")
finally:
if _observer:
_observer.stop()
_observer.join()
_is_monitoring = False
def start_workflow_monitor():
"""启动工作流监控(后台线程模式)"""
global _monitor_thread, _is_monitoring
if _is_monitoring:
return True
try:
_monitor_thread = threading.Thread(target=_monitor_worker, daemon=True)
_monitor_thread.start()
return True
except Exception as e:
logging.error(f"[ComfyUI-1hewNodes] 启动工作流监控线程失败:{str(e)}")
return False
def stop_workflow_monitor():
"""停止工作流监控"""
global _is_monitoring, _observer, _monitor_thread
if not _is_monitoring:
return
try:
# 设置停止标志
_is_monitoring = False
# 停止Observer
if _observer:
_observer.stop()
# 等待Observer线程结束
try:
_observer.join(timeout=5.0) # 最多等待5秒
except Exception as e:
logging.warning(f"[ComfyUI-1hewNodes] Observer停止时出现异常:{str(e)}")
_observer = None
# 等待监控线程结束
if _monitor_thread and _monitor_thread.is_alive():
try:
_monitor_thread.join(timeout=3.0) # 最多等待3秒
except Exception as e:
logging.warning(f"[ComfyUI-1hewNodes] 监控线程停止时出现异常:{str(e)}")
_monitor_thread = None
except Exception as e:
logging.error(f"[ComfyUI-1hewNodes] 停止工作流监控时出错:{str(e)}")
# 强制重置状态
_is_monitoring = False
_observer = None
_monitor_thread = None
def is_monitoring():
"""检查监控是否正在运行"""
return _is_monitoring
# 兼容性:保留原有的start_observer函数用于独立运行
def start_observer():
"""启动文件监控器(独立运行模式)"""
# 验证并获取工作流目录
is_valid, workflow_root = validate_workflow_dir()
if not is_valid:
return False
event_handler = WorkflowFileHandler()
observer = Observer()
# 监控目标目录及所有子目录(recursive=True)
observer.schedule(event_handler, workflow_root, recursive=True)
try:
observer.start()
logging.info(f"监控已启动 | 目标目录:{workflow_root}")
logging.info("按 Ctrl+C 停止监控...")
while True:
time.sleep(1)
except KeyboardInterrupt:
observer.stop()
logging.info("监控已停止")
except Exception as e:
logging.error(f"监控启动失败:{str(e)}")
observer.stop()
observer.join()
return True
if __name__ == "__main__":
if not start_observer():
# 目录无效时暂停,让用户看清错误
input("按 Enter 退出...")