添加国际化,修复安全问题,使用英文提示

This commit is contained in:
2025-11-17 07:31:33 +08:00
parent e5d55e6e8d
commit a2b773bb49
10 changed files with 240 additions and 51 deletions
+1 -1
View File
@@ -118,7 +118,7 @@ pip install -r requirements.txt
### 文件命名选项 ### 文件命名选项
- **文件名前缀**:自定义前缀 - **文件名前缀**:自定义前缀
- **添加时间戳**:在文件名中包含时间 - **添加时间戳**:在文件名中包含时间
- **添加计数器**批量保存时的序号 - **添加计数器**多张图片时的序号
- **计数器设置**:起始值和位数 - **计数器设置**:起始值和位数
## 🔧 高级功能 ## 🔧 高级功能
+4
View File
@@ -0,0 +1,4 @@
{
"SmartFolderManager": "Smart Folder Manager",
"SmartImageSaver": "Smart Image Saver"
}
+59
View File
@@ -0,0 +1,59 @@
{
"SmartFolderManager": {
"title": "Smart Folder Manager",
"inputs": {
"images": "Input images for extracting size info and passing to save node",
"base_folder": "Base folder path, can be relative or absolute path",
"create_subfolders": "Whether to automatically create subfolders",
"enable_date_folder": "Whether to create date folder",
"enable_model_folder": "Whether to create model folder",
"enable_seed_folder": "Whether to create seed folder",
"enable_prompt_folder": "Whether to create prompt folder",
"enable_custom_folder": "Whether to use custom folder",
"date_format": "Date format: yyyy=year MM=month dd=day hh=hour mm=minute ss=second",
"include_time": "Whether to include time in date",
"model_source": "Model info source: auto=from metadata/external input, manual=manual selection",
"manual_model_name": "Manual model selection (only effective when model_source=manual)",
"model_input": "Input from model loader node (only effective when model_source=auto)",
"seed": "Seed value (manual input or connect external node)",
"positive_prompt": "Positive prompt (manual input or connect external node)",
"negative_prompt": "Negative prompt (manual input or connect external node)",
"custom_subfolder": "Custom subfolder name",
"model_short_name": "Use model short name (remove extension)",
"prompt_max_length": "Maximum length for prompt folder name",
"sanitize_names": "Clean illegal characters in filenames"
},
"outputs": {
"images": "Passed image data",
"folder_path": "Generated folder path",
"metadata_json": "Extracted metadata JSON"
}
},
"SmartImageSaver": {
"title": "Smart Image Saver",
"inputs": {
"images": "Images to save (usually from SmartFolderManager)",
"folder_path": "Save path (from SmartFolderManager)",
"metadata_json": "Metadata JSON (from SmartFolderManager)",
"filename_prefix": "Filename prefix",
"file_format": "Image format",
"preview_mode": "Save mode",
"add_timestamp": "Add timestamp to filename",
"add_counter": "Add counter (for multiple images)",
"counter_start": "Counter start value",
"counter_padding": "Counter padding (zero-fill)",
"jpeg_quality": "JPEG quality (1-100)",
"webp_quality": "WebP quality (1-100)",
"webp_lossless": "WebP lossless compression",
"png_compression": "PNG compression level (0-9)",
"embed_metadata": "Embed metadata into image",
"embed_workflow": "Embed workflow information",
"overwrite_existing": "Overwrite existing files",
"create_backup": "Create backup for overwritten files",
"optimize_size": "Optimize file size"
},
"outputs": {
"images": "Saved images"
}
}
}
+4
View File
@@ -0,0 +1,4 @@
{
"SmartFolderManager": "智能文件夹管理",
"SmartImageSaver": "智能图片保存"
}
+59
View File
@@ -0,0 +1,59 @@
{
"SmartFolderManager": {
"title": "智能文件夹管理",
"inputs": {
"images": "输入图片,用于提取尺寸信息并传递给保存节点",
"base_folder": "基础文件夹路径,可以是相对路径或绝对路径",
"create_subfolders": "是否自动创建子文件夹",
"enable_date_folder": "是否创建日期文件夹",
"enable_model_folder": "是否创建模型文件夹",
"enable_seed_folder": "是否创建种子文件夹",
"enable_prompt_folder": "是否创建提示词文件夹",
"enable_custom_folder": "是否使用自定义文件夹",
"date_format": "日期格式: yyyy=年 MM=月 dd=日 hh=时 mm=分 ss=秒",
"include_time": "是否在日期中包含时间",
"model_source": "模型信息来源:auto=从元数据/外部输入获取,manual=手动选择",
"manual_model_name": "手动选择模型(仅在model_source=manual时生效)",
"model_input": "从模型加载器节点输入(仅在model_source=auto时生效)",
"seed": "种子值(手动输入或连接外部节点)",
"positive_prompt": "正向提示词(手动输入或连接外部节点)",
"negative_prompt": "负向提示词(手动输入或连接外部节点)",
"custom_subfolder": "自定义子文件夹名称",
"model_short_name": "使用模型短名称(去除扩展名)",
"prompt_max_length": "提示词文件夹名最大长度",
"sanitize_names": "清理文件名中的非法字符"
},
"outputs": {
"images": "传递的图片数据",
"folder_path": "生成的文件夹路径",
"metadata_json": "提取的元数据JSON"
}
},
"SmartImageSaver": {
"title": "智能图片保存",
"inputs": {
"images": "要保存的图片(通常来自SmartFolderManager",
"folder_path": "保存路径(来自SmartFolderManager",
"metadata_json": "元数据JSON(来自SmartFolderManager",
"filename_prefix": "文件名前缀",
"file_format": "图片格式",
"preview_mode": "保存模式",
"add_timestamp": "在文件名中添加时间戳",
"add_counter": "添加计数器(多张图片时)",
"counter_start": "计数器起始值",
"counter_padding": "计数器位数(补零)",
"jpeg_quality": "JPEG质量(1-100",
"webp_quality": "WebP质量(1-100",
"webp_lossless": "WebP无损压缩",
"png_compression": "PNG压缩级别(0-9",
"embed_metadata": "嵌入元数据到图片",
"embed_workflow": "嵌入工作流信息",
"overwrite_existing": "覆盖已存在的文件",
"create_backup": "为覆盖的文件创建备份",
"optimize_size": "优化文件大小"
},
"outputs": {
"images": "保存的图片"
}
}
}
+24 -6
View File
@@ -24,12 +24,19 @@ class PathManager:
} }
def sanitize_filename(self, name: str, max_length: int = 100) -> str: def sanitize_filename(self, name: str, max_length: int = 100) -> str:
"""清理文件名,移除非法字符""" """安全地清理文件名,移除非法字符和路径遍历攻击"""
if not name: if not name:
return "untitled" return "untitled"
name = str(name) name = str(name)
# 使用os.path.basename确保只获取文件名部分,防止路径遍历
name = os.path.basename(name)
# 如果basename后为空(比如输入是路径分隔符),使用默认名称
if not name or name in ('.', '..'):
return "untitled"
# 移除或替换非法字符 # 移除或替换非法字符
name = re.sub(self.illegal_chars, '_', name) name = re.sub(self.illegal_chars, '_', name)
@@ -41,6 +48,16 @@ class PathManager:
# 移除首尾的下划线和点 # 移除首尾的下划线和点
name = name.strip('_.') name = name.strip('_.')
# 防止Windows保留名称
reserved_names = [
'CON', 'PRN', 'AUX', 'NUL',
'COM1', 'COM2', 'COM3', 'COM4', 'COM5', 'COM6', 'COM7', 'COM8', 'COM9',
'LPT1', 'LPT2', 'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9'
]
name_upper = name.upper()
if name_upper in reserved_names or name_upper.split('.')[0] in reserved_names:
name = f"_{name}"
# 限制长度 # 限制长度
if len(name) > max_length: if len(name) > max_length:
name = name[:max_length].rstrip('_.') name = name[:max_length].rstrip('_.')
@@ -143,13 +160,14 @@ class PathManager:
return [seg for seg in path_segments if seg] return [seg for seg in path_segments if seg]
def resolve_base_path(self, base_folder: str, default_output_dir: str) -> str: def resolve_base_path(self, base_folder: str, default_output_dir: str) -> str:
"""解析基础路径""" """安全地解析基础路径"""
from ..utils.validators import InputValidator
if not base_folder or base_folder.lower() in ["", "output", "."]: if not base_folder or base_folder.lower() in ["", "output", "."]:
return default_output_dir return default_output_dir
elif os.path.isabs(base_folder):
return base_folder # 使用安全的路径连接方法
else: return InputValidator.secure_path_join(base_folder, "")
return os.path.join(default_output_dir, base_folder)
def build_full_path(self, base_folder: str, structure_mode: str, def build_full_path(self, base_folder: str, structure_mode: str,
metadata: Dict[str, Any], user_inputs: Dict[str, Any], metadata: Dict[str, Any], user_inputs: Dict[str, Any],
+2 -2
View File
@@ -10,8 +10,8 @@ NODE_CLASS_MAPPINGS = {
} }
NODE_DISPLAY_NAME_MAPPINGS = { NODE_DISPLAY_NAME_MAPPINGS = {
"SmartFolderManager": "智能文件夹管理", "SmartFolderManager": "SmartFolderManager",
"SmartImageSaver": "智能图片保存", "SmartImageSaver": "SmartImageSaver",
} }
__all__ = [ __all__ = [
+20 -20
View File
@@ -24,103 +24,103 @@ class SmartFolderManager:
def INPUT_TYPES(cls): def INPUT_TYPES(cls):
return { return {
"required": { "required": {
"images": ("IMAGE", {"tooltip": "输入图片,用于提取尺寸信息并传递给保存节点"}), "images": ("IMAGE", {"tooltip": "Input images for extracting size info and passing to save node"}),
"base_folder": ("STRING", { "base_folder": ("STRING", {
"default": "output", "default": "output",
"multiline": False, "multiline": False,
"tooltip": "基础文件夹路径,可以是相对路径或绝对路径" "tooltip": "Base folder path, can be relative or absolute path"
}), }),
"create_subfolders": ("BOOLEAN", { "create_subfolders": ("BOOLEAN", {
"default": True, "default": True,
"tooltip": "是否自动创建子文件夹" "tooltip": "Whether to automatically create subfolders"
}), }),
}, },
"optional": { "optional": {
# 文件夹层级开关 # 文件夹层级开关
"enable_date_folder": ("BOOLEAN", { "enable_date_folder": ("BOOLEAN", {
"default": True, "default": True,
"tooltip": "是否创建日期文件夹" "tooltip": "Whether to create date folder"
}), }),
"enable_model_folder": ("BOOLEAN", { "enable_model_folder": ("BOOLEAN", {
"default": True, "default": True,
"tooltip": "是否创建模型文件夹" "tooltip": "Whether to create model folder"
}), }),
"enable_seed_folder": ("BOOLEAN", { "enable_seed_folder": ("BOOLEAN", {
"default": True, "default": True,
"tooltip": "是否创建种子文件夹" "tooltip": "Whether to create seed folder"
}), }),
"enable_prompt_folder": ("BOOLEAN", { "enable_prompt_folder": ("BOOLEAN", {
"default": False, "default": False,
"tooltip": "是否创建提示词文件夹" "tooltip": "Whether to create prompt folder"
}), }),
"enable_custom_folder": ("BOOLEAN", { "enable_custom_folder": ("BOOLEAN", {
"default": False, "default": False,
"tooltip": "是否使用自定义文件夹" "tooltip": "Whether to use custom folder"
}), }),
# 日期相关 # 日期相关
"date_format": ("STRING", { "date_format": ("STRING", {
"default": "yyyy-MM-dd", "default": "yyyy-MM-dd",
"multiline": False, "multiline": False,
"tooltip": "日期格式: yyyy年 MM月 dd日 hh时 mm分 ss秒" "tooltip": "Date format: yyyy=year MM=month dd=day hh=hour mm=minute ss=second"
}), }),
"include_time": ("BOOLEAN", { "include_time": ("BOOLEAN", {
"default": False, "default": False,
"tooltip": "是否在日期中包含时间" "tooltip": "Whether to include time in date"
}), }),
# 模型信息来源选择 # 模型信息来源选择
"model_source": (["auto", "manual"], { "model_source": (["auto", "manual"], {
"default": "auto", "default": "auto",
"tooltip": "模型信息来源:auto=从元数据/外部输入获取,manual=手动选择" "tooltip": "Model info source: auto=from metadata/external input, manual=manual selection"
}), }),
"manual_model_name": (folder_paths.get_filename_list("checkpoints"), { "manual_model_name": (folder_paths.get_filename_list("checkpoints"), {
"tooltip": "手动选择模型(仅在model_source=manual时生效)" "tooltip": "Manual model selection (only effective when model_source=manual)"
}), }),
"model_input": ("MODEL", { "model_input": ("MODEL", {
"tooltip": "从模型加载器节点输入(仅在model_source=auto时生效)" "tooltip": "Input from model loader node (only effective when model_source=auto)"
}), }),
# 种子信息 # 种子信息
"seed": ("STRING", { "seed": ("STRING", {
"default": "0", "default": "0",
"multiline": False, "multiline": False,
"tooltip": "种子值(手动输入或连接外部节点)" "tooltip": "Seed value (manual input or connect external node)"
}), }),
# 提示词信息 # 提示词信息
"positive_prompt": ("STRING", { "positive_prompt": ("STRING", {
"default": "", "default": "",
"multiline": True, "multiline": True,
"tooltip": "正向提示词(手动输入或连接外部节点)" "tooltip": "Positive prompt (manual input or connect external node)"
}), }),
"negative_prompt": ("STRING", { "negative_prompt": ("STRING", {
"default": "", "default": "",
"multiline": True, "multiline": True,
"tooltip": "负向提示词(手动输入或连接外部节点)" "tooltip": "Negative prompt (manual input or connect external node)"
}), }),
# 自定义路径 # 自定义路径
"custom_subfolder": ("STRING", { "custom_subfolder": ("STRING", {
"default": "", "default": "",
"multiline": False, "multiline": False,
"tooltip": "自定义子文件夹名称" "tooltip": "Custom subfolder name"
}), }),
# 显示选项 # 显示选项
"model_short_name": ("BOOLEAN", { "model_short_name": ("BOOLEAN", {
"default": True, "default": True,
"tooltip": "使用模型短名称(去除扩展名)" "tooltip": "Use model short name (remove extension)"
}), }),
"prompt_max_length": ("INT", { "prompt_max_length": ("INT", {
"default": 50, "default": 50,
"min": 10, "min": 10,
"max": 200, "max": 200,
"tooltip": "提示词文件夹名最大长度" "tooltip": "Maximum length for prompt folder name"
}), }),
"sanitize_names": ("BOOLEAN", { "sanitize_names": ("BOOLEAN", {
"default": True, "default": True,
"tooltip": "清理文件名中的非法字符" "tooltip": "Clean illegal characters in filenames"
}), }),
}, },
"hidden": { "hidden": {
+20 -20
View File
@@ -28,52 +28,52 @@ class SmartImageSaver:
def INPUT_TYPES(cls): def INPUT_TYPES(cls):
return { return {
"required": { "required": {
"images": ("IMAGE", {"tooltip": "要保存的图片(通常来自SmartFolderManager"}), "images": ("IMAGE", {"tooltip": "Images to save (usually from SmartFolderManager)"}),
"folder_path": ("STRING", { "folder_path": ("STRING", {
"default": "", "default": "",
"multiline": False, "multiline": False,
"tooltip": "保存路径(来自SmartFolderManager" "tooltip": "Save path (from SmartFolderManager)"
}), }),
"metadata_json": ("STRING", { "metadata_json": ("STRING", {
"default": "", "default": "",
"multiline": True, "multiline": True,
"tooltip": "元数据JSON(来自SmartFolderManager" "tooltip": "Metadata JSON (from SmartFolderManager)"
}), }),
"filename_prefix": ("STRING", { "filename_prefix": ("STRING", {
"default": "image", "default": "image",
"multiline": False, "multiline": False,
"tooltip": "文件名前缀" "tooltip": "Filename prefix"
}), }),
"file_format": (["png", "jpeg", "webp", "bmp", "tiff"], { "file_format": (["png", "jpeg", "webp", "bmp", "tiff"], {
"default": "png", "default": "png",
"tooltip": "图片格式" "tooltip": "Image format"
}), }),
"preview_mode": (["save_and_preview", "preview_only", "save_only"], { "preview_mode": (["save_and_preview", "preview_only", "save_only"], {
"default": "save_and_preview", "default": "save_and_preview",
"tooltip": "保存模式" "tooltip": "Save mode"
}), }),
}, },
"optional": { "optional": {
# 文件名选项 # 文件名选项
"add_timestamp": ("BOOLEAN", { "add_timestamp": ("BOOLEAN", {
"default": False, "default": False,
"tooltip": "在文件名中添加时间戳" "tooltip": "Add timestamp to filename"
}), }),
"add_counter": ("BOOLEAN", { "add_counter": ("BOOLEAN", {
"default": True, "default": True,
"tooltip": "添加计数器(批量保存时)" "tooltip": "Add counter (for multiple images)"
}), }),
"counter_start": ("INT", { "counter_start": ("INT", {
"default": 1, "default": 1,
"min": 0, "min": 0,
"max": 99999, "max": 99999,
"tooltip": "计数器起始值" "tooltip": "Counter start value"
}), }),
"counter_padding": ("INT", { "counter_padding": ("INT", {
"default": 4, "default": 4,
"min": 1, "min": 1,
"max": 10, "max": 10,
"tooltip": "计数器位数(补零)" "tooltip": "Counter padding (zero-fill)"
}), }),
# 图片质量选项 # 图片质量选项
@@ -81,47 +81,47 @@ class SmartImageSaver:
"default": 95, "default": 95,
"min": 1, "min": 1,
"max": 100, "max": 100,
"tooltip": "JPEG质量(1-100" "tooltip": "JPEG quality (1-100)"
}), }),
"webp_quality": ("INT", { "webp_quality": ("INT", {
"default": 90, "default": 90,
"min": 1, "min": 1,
"max": 100, "max": 100,
"tooltip": "WebP质量(1-100" "tooltip": "WebP quality (1-100)"
}), }),
"webp_lossless": ("BOOLEAN", { "webp_lossless": ("BOOLEAN", {
"default": False, "default": False,
"tooltip": "WebP无损压缩" "tooltip": "WebP lossless compression"
}), }),
"png_compression": ("INT", { "png_compression": ("INT", {
"default": 6, "default": 6,
"min": 0, "min": 0,
"max": 9, "max": 9,
"tooltip": "PNG压缩级别(0-9" "tooltip": "PNG compression level (0-9)"
}), }),
# 元数据选项 # 元数据选项
"embed_metadata": ("BOOLEAN", { "embed_metadata": ("BOOLEAN", {
"default": True, "default": True,
"tooltip": "嵌入元数据到图片" "tooltip": "Embed metadata into image"
}), }),
"embed_workflow": ("BOOLEAN", { "embed_workflow": ("BOOLEAN", {
"default": False, "default": True,
"tooltip": "嵌入工作流信息" "tooltip": "Embed workflow information"
}), }),
# 高级选项 # 高级选项
"overwrite_existing": ("BOOLEAN", { "overwrite_existing": ("BOOLEAN", {
"default": False, "default": False,
"tooltip": "覆盖已存在的文件" "tooltip": "Overwrite existing files"
}), }),
"create_backup": ("BOOLEAN", { "create_backup": ("BOOLEAN", {
"default": False, "default": False,
"tooltip": "为覆盖的文件创建备份" "tooltip": "Create backup for overwritten files"
}), }),
"optimize_size": ("BOOLEAN", { "optimize_size": ("BOOLEAN", {
"default": False, "default": False,
"tooltip": "优化文件大小" "tooltip": "Optimize file size"
}), }),
}, },
"hidden": { "hidden": {
+47 -2
View File
@@ -9,8 +9,8 @@ class InputValidator:
"""输入验证器""" """输入验证器"""
@staticmethod @staticmethod
def validate_folder_path(path: str) -> bool: def validate_folder_path(path: str, allowed_base_paths: list = None) -> bool:
"""验证文件夹路径是否有效""" """验证文件夹路径是否有效且安全"""
if not path: if not path:
return False return False
@@ -23,8 +23,53 @@ class InputValidator:
if re.search(illegal_chars, path): if re.search(illegal_chars, path):
return False return False
# 检查路径遍历攻击
if '..' in path or path.startswith('/') or '~' in path:
return False
return True return True
@staticmethod
def secure_path_join(base_path: str, user_path: str = "") -> str:
"""安全地连接路径,防止路径遍历攻击 - 使用GitHub建议的方法"""
import folder_paths
# 获取ComfyUI的输出目录作为安全基础路径
safe_base = folder_paths.get_output_directory()
try:
# 使用realpath解析安全基础路径
resolved_safe_base = os.path.realpath(safe_base)
# 处理base_path
if os.path.isabs(base_path):
# 绝对路径:直接解析
candidate_path = os.path.realpath(base_path)
else:
# 相对路径:相对于安全基础路径
candidate_path = os.path.realpath(os.path.join(safe_base, base_path))
# 如果有用户路径,继续连接
if user_path:
candidate_path = os.path.realpath(os.path.join(candidate_path, user_path))
# 使用commonpath确保路径在安全范围内
try:
common = os.path.commonpath([resolved_safe_base, candidate_path])
# 检查公共路径是否就是安全基础路径
if os.path.realpath(common) == resolved_safe_base:
return candidate_path
else:
# 路径逃逸,返回安全基础路径
return resolved_safe_base
except ValueError:
# commonpath失败(通常是不同驱动器),返回安全基础路径
return resolved_safe_base
except (OSError, ValueError) as e:
# 任何路径操作失败,返回安全基础路径
return safe_base
@staticmethod @staticmethod
def validate_filename(filename: str) -> bool: def validate_filename(filename: str) -> bool:
"""验证文件名是否有效""" """验证文件名是否有效"""