Files

1790 lines
70 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 json
import csv
import tempfile
import shutil
import folder_paths
class ConfigManager:
def __init__(self):
# 插件目录
self.dir_path = os.path.dirname(os.path.abspath(__file__))
# 获取 ComfyUI 用户目录
try:
user_dir = folder_paths.get_user_directory()
if user_dir and os.path.isdir(user_dir):
# 使用 user/default/prompt-assistant 作为基础目录
self.base_dir = os.path.join(user_dir, "default", "prompt-assistant")
# self._log(f"使用用户配置目录: {self.base_dir}")
else:
# 回退到插件目录
self.base_dir = self.dir_path
self._log(f"回退到插件配置目录: {self.base_dir}")
except Exception as e:
# 异常处理,回退到插件目录
self.base_dir = self.dir_path
self._log(f"无法获取用户目录({str(e)}),使用插件配置目录")
# 定义各个子目录
self.config_dir = os.path.join(self.base_dir, "config")
self.rules_dir = os.path.join(self.base_dir, "rules")
self.tags_dir = os.path.join(self.base_dir, "tags")
# 确保目录存在
os.makedirs(self.config_dir, exist_ok=True)
os.makedirs(self.rules_dir, exist_ok=True)
os.makedirs(self.tags_dir, exist_ok=True)
# 配置文件路径(用户配置和选择)
self.config_path = os.path.join(self.config_dir, "config.json")
self.active_prompts_path = os.path.join(self.config_dir, "active_prompts.json")
self.tags_user_path = os.path.join(self.config_dir, "tags_user.json")
self.tags_selection_path = os.path.join(self.config_dir, "tags_selection.json")
# 规则文件路径(规则定义和模板)
self.system_prompts_path = os.path.join(self.rules_dir, "system_prompts.json")
self.kontext_presets_path = os.path.join(self.rules_dir, "kontext_presets.json")
# ---模板目录(插件内置)---
self.templates_dir = os.path.join(self.dir_path, "config")
# 存储模板版本号(用于版本比对)
self._template_versions = {}
# ---加载默认配置(从模板文件)---
self.default_config = self._load_template("config", {"version": "2.0", "model_services": []})
self.default_system_prompts = self._load_template("system_prompts", {})
self.default_kontext_presets = self._load_template("kontext_presets", {})
# ---简单默认配置(无需模板,直接定义)---
self.default_active_prompts = {
"expand": "expand_扩写-通用",
"vision_zh": "vision_zh_图像描述-Tag风格",
"vision_en": "vision_en_Detail_Caption"
}
self.default_user_tags = {"favorites": []}
# 默认标签选择
self.default_tags_selection = {"selected_file": "用户标签.csv"}
# 执行数据迁移和配置文件初始化
# migration_tool 统一处理:确保文件存在 -> CSV标签迁移 -> 旧版迁移 -> 增量更新
self._run_migrations()
# 验证并修复激活提示词(静默模式,仅异常时修复)
self.validate_and_fix_active_prompts()
# 验证并修复模型参数配置
self.validate_and_fix_model_params()
# --- 数据迁移 ---
def _run_migrations(self):
"""
执行数据迁移(按需调用,不影响性能)
仅在需要时才导入和运行迁移工具
"""
try:
from .utils.migration_tool import run_migrations
# 准备默认配置数据用于增量更新
default_configs = {
'config': self.default_config,
'system_prompts': self.default_system_prompts,
'active_prompts': self.default_active_prompts,
'tags_user': self.default_user_tags,
'kontext_presets': self.default_kontext_presets
}
# 运行迁移
results = run_migrations(
plugin_dir=self.dir_path,
user_base_dir=self.base_dir,
logger=self._log,
default_configs=default_configs
)
# 记录迁移结果
if results.get('tags_migration'):
self._log("[用户标签.csv] 数据迁移完成")
except Exception as e:
self._log(f"数据迁移失败: {str(e)}")
# 迁移失败不影响正常运行,仅记录日志
# --- 统一日志输出 ---
def _log(self, msg: str):
"""统一控制台日志前缀"""
from .utils.common import _ANSI_CLEAR_EOL
print(f"\r{_ANSI_CLEAR_EOL}✨ {msg}", flush=True)
# ---模板加载---
def _load_template(self, template_name: str, fallback: dict = None) -> dict:
"""
从模板文件加载默认配置
参数:
template_name: 模板名称(不含扩展名和_template后缀)
fallback: 加载失败时的回退默认值
返回:
配置字典(包含 __config_version 用于版本管理)
"""
template_path = os.path.join(self.templates_dir, f"{template_name}_template.json")
try:
with open(template_path, "r", encoding="utf-8") as f:
data = json.load(f)
# 获取版本号并保存,用于后续比对
template_version = data.get("__config_version", "2.0")
self._template_versions[template_name] = template_version
return data
except Exception as e:
self._log(f"加载模板 {template_name} 失败: {str(e)},使用回退值")
# 确保 fallback 也包含版本号
if fallback is None:
fallback = {}
# 如果 fallback 没有版本号,添加默认版本号
if "__config_version" not in fallback:
fallback = {"__config_version": "2.0", **fallback}
self._template_versions[template_name] = "2.0"
return fallback
def _get_config_version(self, config: dict) -> str:
"""
获取配置版本号(兼容新旧两种版本字段)
版本字段优先级:
1. __config_version (新版本字段,如 "2.0.0")
2. version (旧版本字段,如 "2.0" 或 "1.0")
3. 默认返回 "1.0"(无版本字段视为最旧版本)
返回:
版本字符串,如 "2.0.0"、"2.0" 或 "1.0"
"""
# 优先使用新版本字段
if "__config_version" in config:
return config["__config_version"]
# 兼容旧版本字段
return config.get("version", "1.0")
def _is_v2_config(self, config: dict) -> bool:
"""
检查配置是否为 v2.0 或更高版本
返回:
True 表示 v2.0 或更高版本 (1.9 也视为 v2 格式,用于增量测试)
"""
version = self._get_config_version(config)
try:
v_float = float(version)
return v_float >= 1.9
except ValueError:
# 如果不是数字(如 "2.0.0"),取主版本号比较
major_version = version.split(".")[0]
try:
return int(major_version) >= 2
except ValueError:
return False
# --- 注意:以下方法已迁移到 migration_tool.py ---
# - _apply_migrated_api_keys
# - _migrate_provider_to_service
# - _create_or_update_custom_service
# - _match_service_by_provider
# - _check_and_add_missing_services
# 配置文件的创建、迁移和增量更新统一由 migration_tool 处理
def _atomic_write_json(self, file_path: str, data: dict) -> bool:
"""
原子性写入 JSON 文件
采用"写临时文件 + 原子性重命名"的策略,确保文件写入的原子性:
- 如果写入成功,新文件会替换旧文件
- 如果写入失败或被中断,旧文件保持不变
参数:
file_path: 目标文件路径
data: 要保存的数据字典
返回:
bool: 保存成功返回 True,失败返回 False
"""
temp_fd = None
temp_path = None
try:
# ---步骤1:写入临时文件---
# 在同一目录下创建临时文件(确保在同一文件系统,rename 才是原子的)
temp_fd, temp_path = tempfile.mkstemp(
dir=os.path.dirname(file_path),
suffix='.tmp',
prefix='.tmp_'
)
# 完整写入新配置到临时文件
with os.fdopen(temp_fd, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=2)
temp_fd = None # 文件已关闭,避免重复关闭
# ---步骤2:原子性替换---
# rename 操作是原子的,要么成功替换,要么失败不变
shutil.move(temp_path, file_path)
temp_path = None # 已移动,避免清理时删除
return True
except Exception as e:
self._log(f"原子性写入 JSON 文件失败 [{os.path.basename(file_path)}]: {str(e)}")
return False
finally:
# 清理临时文件(如果写入失败)
if temp_fd is not None:
try:
os.close(temp_fd)
except:
pass
if temp_path is not None and os.path.exists(temp_path):
try:
os.unlink(temp_path)
except:
pass
def load_config(self):
"""加载配置文件"""
try:
with open(self.config_path, "r", encoding="utf-8") as f:
return json.load(f)
except Exception as e:
self._log(f"加载配置文件失败: {str(e)}")
return self.default_config
def save_config(self, config):
"""保存配置文件"""
return self._atomic_write_json(self.config_path, config)
def load_system_prompts(self):
"""加载系统提示词配置"""
try:
with open(self.system_prompts_path, "r", encoding="utf-8") as f:
return json.load(f)
except Exception as e:
self._log(f"加载系统提示词配置失败: {str(e)}")
return self.default_system_prompts
def save_system_prompts(self, system_prompts):
"""保存系统提示词配置"""
return self._atomic_write_json(self.system_prompts_path, system_prompts)
def load_active_prompts(self):
"""加载激活的提示词配置"""
try:
with open(self.active_prompts_path, "r", encoding="utf-8") as f:
return json.load(f)
except Exception as e:
self._log(f"加载激活的提示词配置失败: {str(e)}")
return self.default_active_prompts
def save_active_prompts(self, active_prompts):
"""保存激活的提示词配置"""
return self._atomic_write_json(self.active_prompts_path, active_prompts)
def load_user_tags(self):
"""加载用户标签配置"""
try:
with open(self.tags_user_path, "r", encoding="utf-8") as f:
return json.load(f)
except Exception as e:
self._log(f"加载用户标签配置失败: {str(e)}")
return self.default_user_tags
def save_user_tags(self, user_tags):
"""保存用户标签配置"""
return self._atomic_write_json(self.tags_user_path, user_tags)
def load_kontext_presets(self):
"""加载Kontext预设配置"""
try:
with open(self.kontext_presets_path, "r", encoding="utf-8") as f:
return json.load(f)
except Exception as e:
self._log(f"加载Kontext预设配置失败: {str(e)}")
return {}
def save_kontext_presets(self, kontext_presets):
"""保存Kontext预设配置"""
return self._atomic_write_json(self.kontext_presets_path, kontext_presets)
# --- 注意:ensure_tags_csv_exists 和 CSV 标签迁移已迁移到 migration_tool.py ---
def list_tags_files(self) -> list:
"""列出tags目录下所有CSV文件"""
try:
files = []
for filename in os.listdir(self.tags_dir):
if filename.endswith(".csv"):
files.append(filename)
return sorted(files)
except Exception as e:
self._log(f"列出标签文件失败: {str(e)}")
return []
def load_tags_csv(self, filename: str) -> dict:
"""加载CSV标签文件,返回嵌套字典结构"""
csv_path = os.path.join(self.tags_dir, filename)
if not os.path.exists(csv_path):
self._log(f"CSV文件不存在: {filename}")
return {}
# 尝试多种编码,优先尝试 utf-8-sig (Excel默认UTF-8),然后是 gbk (Excel默认ANSI),最后是 utf-8
encodings = ['utf-8-sig', 'gbk', 'gb18030', 'utf-8']
for encoding in encodings:
try:
result = {}
with open(csv_path, "r", encoding=encoding, newline="") as f:
reader = csv.reader(f)
try:
header = next(reader, None) # 跳过表头
except StopIteration:
return {} # 空文件
for row in reader:
# 过滤无效行
if not row or not any(cell.strip() for cell in row):
continue
# 至少需要两列:标签名, 标签值
if len(row) < 2:
continue
tag_name = row[0].strip()
tag_value = row[1].strip()
if not tag_name:
continue
# 分类路径:从第3列开始,过滤空值
categories = [c.strip() for c in row[2:] if c.strip()]
# 构建嵌套结构
current = result
for cat in categories:
if cat not in current or not isinstance(current[cat], dict):
current[cat] = {}
current = current[cat]
# 处理空分类占位符:只创建分类结构,不添加标签
if tag_name == "__empty__" or tag_name == "__placeholder__":
continue
# 添加标签
current[tag_name] = tag_value
return result
except UnicodeDecodeError:
continue
except Exception as e:
self._log(f"加载CSV标签失败 ({encoding}): {str(e)}")
continue
self._log(f"无法加载CSV文件: {filename},尝试了所有编码均失败")
return {}
def save_tags_csv(self, filename: str, tags: dict) -> bool:
"""保存标签数据到CSV文件"""
csv_path = os.path.join(self.tags_dir, filename)
try:
rows = []
max_depth = 0
def extract_tags(obj, path: list):
nonlocal max_depth
# 确保 obj 是字典类型
if not isinstance(obj, dict):
return
# 如果是空分类(空字典),添加占位行
if len(obj) == 0 and path:
# 使用 __empty__ 作为占位符标记空分类
rows.append(["__empty__", ""] + path)
max_depth = max(max_depth, len(path))
return
for key, value in obj.items():
if isinstance(value, str):
rows.append([key, value] + path)
max_depth = max(max_depth, len(path))
elif isinstance(value, dict):
extract_tags(value, path + [key])
# 提取所有标签
extract_tags(tags, [])
if not rows:
self._log(f"保存CSV标签: 数据为空")
# 如果数据为空,写入只含表头的文件或保持现状?
# 通常为了防止误删,如果 tags 为空暂不操作或清空文件。
# 这里选择写入表头:
with open(csv_path, "w", encoding="utf-8-sig", newline="") as f:
writer = csv.writer(f)
writer.writerow(["标签名", "标签值"])
return True
# 动态构建表头
header = ["标签名", "标签值"]
for i in range(max_depth):
num_zh = ["一", "二", "三", "四", "五", "六", "七", "八", "九", "十"]
suffix = num_zh[i] if i < len(num_zh) else str(i + 1)
header.append(f"{suffix}级分类")
with open(csv_path, "w", encoding="utf-8-sig", newline="") as f:
writer = csv.writer(f)
writer.writerow(header)
for row in rows:
# 补齐长度以匹配表头
while len(row) < len(header):
row.append("")
# 确保 row 长度不超过表头(防御性)
writer.writerow(row[:len(header)])
return True
except Exception as e:
self._log(f"保存CSV标签失败: {str(e)}")
return False
def get_tags_selection(self) -> dict:
"""获取用户选择的标签文件"""
try:
if os.path.exists(self.tags_selection_path):
with open(self.tags_selection_path, "r", encoding="utf-8") as f:
return json.load(f)
return self.default_tags_selection
except Exception as e:
self._log(f"读取标签选择失败: {str(e)}")
return self.default_tags_selection
def save_tags_selection(self, selection: dict) -> bool:
"""保存用户选择的标签文件"""
try:
with open(self.tags_selection_path, "w", encoding="utf-8") as f:
json.dump(selection, f, ensure_ascii=False, indent=2)
return True
except Exception as e:
self._log(f"保存标签选择失败: {str(e)}")
return False
def get_favorites(self) -> dict:
"""获取收藏列表"""
user_tags = self.load_user_tags()
favorites = user_tags.get("favorites", {})
# 兼容性处理:如果是列表,转换为字典
if isinstance(favorites, list):
new_favorites = {}
for item in favorites:
if isinstance(item, str):
new_favorites[item] = item
elif isinstance(item, dict):
name = item.get("name", item.get("value"))
value = item.get("value")
if name and value:
new_favorites[name] = value
return new_favorites
return favorites
def add_favorite(self, tag_value: str, tag_name: str = None, category: str = "默认") -> bool:
"""添加收藏"""
try:
user_tags = self.load_user_tags()
favorites = user_tags.get("favorites", {})
# 兼容性迁移:如果是一维字典 {name: value},无需强制迁移,但新添加的会放入 category
# 如果是列表,先迁移为字典
if isinstance(favorites, list):
favorites = self.get_favorites()
name = tag_name if tag_name else tag_value
# 使用嵌套结构 {分类: {名称: 值}}
if category not in favorites:
# 检查是否存在旧的平铺结构,如果有,且category是默认,可能混杂
# 这里简单处理:如果 favorites 只有键值对且都不是字典,说明是旧版平铺
# 但为了不破坏旧数据,我们在顶层只存储分类字典
# 如果 favorites 中已有非字典的值,说明是旧版平铺结构 {name: value}
# 我们将它们移动到 "默认" 分类
has_legacy = any(not isinstance(v, dict) for v in favorites.values())
if has_legacy:
legacy_items = {k: v for k, v in favorites.items() if not isinstance(v, dict)}
# 清除旧项
for k in legacy_items:
del favorites[k]
# 初始化默认分类
if "默认" not in favorites:
favorites["默认"] = {}
favorites["默认"].update(legacy_items)
if category not in favorites:
favorites[category] = {}
# 如果 favorites[category] 不是字典(防御性编程),初始化为字典
if not isinstance(favorites.get(category), dict):
favorites[category] = {}
favorites[category][name] = tag_value
user_tags["favorites"] = favorites
return self.save_user_tags(user_tags)
except Exception as e:
self._log(f"添加收藏失败: {str(e)}")
return False
def remove_favorite(self, tag_value: str, category: str = None) -> bool:
"""移除收藏"""
try:
user_tags = self.load_user_tags()
favorites = user_tags.get("favorites", {})
# 兼容性迁移
if isinstance(favorites, list):
favorites = self.get_favorites()
removed = False
# 如果指定了分类,只在指定分类中删除
if category:
# 尝试直接匹配分类(完全匹配)
target_categories = [category]
# 如果没找到,尝试模糊匹配(处理文件名后缀差异)
if category not in favorites:
# 比如 category 是 "foo",favorites里有 "foo.csv" 或相反
# 但通常 favorites 里的 key 已经是去后缀的
pass
for cat in target_categories:
if cat in favorites and isinstance(favorites[cat], dict):
# 根据值删除
keys_to_remove = [k for k, v in favorites[cat].items() if v == tag_value]
for k in keys_to_remove:
del favorites[cat][k]
removed = True
# 如果该分类空了,是否删除分类键?暂时保留
else:
# 未指定分类,递归全部删除(旧逻辑)
# 如果是旧版平铺结构
if any(not isinstance(v, dict) for v in favorites.values()):
keys_to_remove = [k for k, v in favorites.items() if not isinstance(v, dict) and v == tag_value]
for k in keys_to_remove:
del favorites[k]
removed = True
# 如果是新版嵌套结构
for cat, items in favorites.items():
if isinstance(items, dict):
keys_to_remove = [k for k, v in items.items() if v == tag_value]
for k in keys_to_remove:
del items[k]
removed = True
if removed:
user_tags["favorites"] = favorites
return self.save_user_tags(user_tags)
return True
except Exception as e:
self._log(f"移除收藏失败: {str(e)}")
return False
def get_system_prompts(self):
"""获取系统提示词配置 (合并提示词定义和激活状态)"""
system_prompts = self.load_system_prompts()
active_prompts = self.load_active_prompts()
system_prompts['active_prompts'] = active_prompts
return system_prompts
def update_system_prompts(self, system_prompts):
"""更新系统提示词配置 (仅更新提示词定义)"""
prompts_to_save = system_prompts.copy()
if 'active_prompts' in prompts_to_save:
del prompts_to_save['active_prompts']
return self.save_system_prompts(prompts_to_save)
def update_active_prompts(self, active_prompts):
"""更新所有激活的提示词"""
return self.save_active_prompts(active_prompts)
def update_active_prompt(self, prompt_type, prompt_id):
"""更新单个激活的提示词"""
active_prompts = self.load_active_prompts()
active_prompts[prompt_type] = prompt_id
return self.save_active_prompts(active_prompts)
def get_baidu_translate_config(self):
"""获取百度翻译配置"""
config = self.load_config()
return config.get("baidu_translate", self.default_config["baidu_translate"])
def get_llm_config(self):
"""获取LLM配置"""
config = self.load_config()
current_service_info = config.get('current_services', {}).get('llm')
# 适配新旧格式:支持字符串(旧)和字典(新)
if isinstance(current_service_info, str):
# 旧格式: "service_id"
current_service_id = current_service_info
current_model_name = None
elif isinstance(current_service_info, dict):
# 新格式: {"service": "service_id", "model": "model_name"}
current_service_id = current_service_info.get('service')
current_model_name = current_service_info.get('model')
else:
# 未设置
current_service_id = None
current_model_name = None
if not current_service_id:
# 没有选中的服务,返回默认结构
return self._get_empty_llm_config()
# 查找对应的服务
service = self._get_service_by_id(current_service_id)
if not service:
return self._get_empty_llm_config()
# 获取LLM模型列表
llm_models = service.get('llm_models', [])
# 如果指定了模型名称,尝试查找
target_model = None
if current_model_name:
target_model = next((m for m in llm_models if m.get('name') == current_model_name), None)
# 如果未找到指定模型,使用默认模型或第一个模型
if not target_model:
target_model = next((m for m in llm_models if m.get('is_default')),
llm_models[0] if llm_models else None)
if not target_model:
return self._get_empty_llm_config()
# 直接获取API Key(明文存储)
api_key = service.get('api_key', '')
# 返回配置
return {
"provider": service.get('id', ''), # 使用service_id作为provider
"model": target_model.get('name', ''),
"base_url": service.get('base_url', ''),
"api_key": api_key,
"temperature": target_model.get('temperature', 0.7),
"max_tokens": target_model.get('max_tokens', 1000),
"top_p": target_model.get('top_p', 0.9),
"auto_unload": service.get('auto_unload', True) if service.get('type') == 'ollama' else None,
"providers": {} # v2.0中不再使用此字段
}
def _get_empty_llm_config(self):
"""返回空的LLM配置"""
return {
"provider": "",
"model": "",
"base_url": "",
"api_key": "",
"temperature": 0.7,
"max_tokens": 1000,
"top_p": 0.9,
"providers": {}
}
def _get_service_by_id(self, service_id: str) -> dict:
"""根据ID获取服务配置"""
config = self.load_config()
services = config.get('model_services', [])
for service in services:
if service.get('id') == service_id:
return service
return None
def get_vision_config(self):
"""获取视觉模型配置"""
config = self.load_config()
current_service_info = config.get('current_services', {}).get('vlm')
# 适配新旧格式:支持字符串(旧)和字典(新)
if isinstance(current_service_info, str):
# 旧格式: "service_id"
current_service_id = current_service_info
current_model_name = None
elif isinstance(current_service_info, dict):
# 新格式: {"service": "service_id", "model": "model_name"}
current_service_id = current_service_info.get('service')
current_model_name = current_service_info.get('model')
else:
# 未设置
current_service_id = None
current_model_name = None
if not current_service_id:
# 没有选中的服务,返回默认结构
return self._get_empty_vision_config()
# 查找对应的服务
service = self._get_service_by_id(current_service_id)
if not service:
return self._get_empty_vision_config()
# 获取VLM模型列表
vlm_models = service.get('vlm_models', [])
# 如果指定了模型名称,尝试查找
target_model = None
if current_model_name:
target_model = next((m for m in vlm_models if m.get('name') == current_model_name), None)
# 如果未找到指定模型,使用默认模型或第一个模型
if not target_model:
target_model = next((m for m in vlm_models if m.get('is_default')),
vlm_models[0] if vlm_models else None)
if not target_model:
return self._get_empty_vision_config()
# 直接获取API Key(明文存储)
api_key = service.get('api_key', '')
# 返回配置
return {
"provider": service.get('id', ''), # 使用service_id作为provider
"model": target_model.get('name', ''),
"base_url": service.get('base_url', ''),
"api_key": api_key,
"temperature": target_model.get('temperature', 0.7),
"max_tokens": target_model.get('max_tokens', 4096),
"top_p": target_model.get('top_p', 0.9),
"auto_unload": service.get('auto_unload', True) if service.get('type') == 'ollama' else None,
"providers": {} # v2.0中不再使用此字段
}
def _get_empty_vision_config(self):
"""返回空的视觉模型配置"""
return {
"provider": "",
"model": "",
"base_url": "",
"api_key": "",
"temperature": 0.7,
"max_tokens": 4096,
"top_p": 0.9,
"providers": {}
}
def get_translate_config(self):
"""获取翻译服务配置(支持百度翻译和LLM翻译)"""
config = self.load_config()
current_service_info = config.get('current_services', {}).get('translate')
# 适配新旧格式:支持字符串(旧)和字典(新)
if isinstance(current_service_info, str):
# 旧格式: "service_id"
current_service_id = current_service_info
current_model_name = None
elif isinstance(current_service_info, dict):
# 新格式: {"service": "service_id", "model": "model_name"}
current_service_id = current_service_info.get('service')
current_model_name = current_service_info.get('model')
else:
# 未设置,默认使用百度翻译
current_service_id = 'baidu'
current_model_name = None
# 百度翻译特殊处理(使用独立的baidu_translate配置)
if current_service_id == 'baidu':
baidu_config = self.get_baidu_translate_config()
return {
"provider": "baidu",
"model": "",
"base_url": "",
"api_key": baidu_config.get('app_id', ''),
"secret_key": baidu_config.get('secret_key', ''),
"temperature": 0.7,
"max_tokens": 1000,
"top_p": 0.9,
"providers": {}
}
# 查找对应的LLM服务
service = self._get_service_by_id(current_service_id)
if not service:
# 服务不存在,回退到百度翻译
baidu_config = self.get_baidu_translate_config()
return {
"provider": "baidu",
"model": "",
"base_url": "",
"api_key": baidu_config.get('app_id', ''),
"secret_key": baidu_config.get('secret_key', ''),
"temperature": 0.7,
"max_tokens": 1000,
"top_p": 0.9,
"providers": {}
}
# 获取LLM模型列表
llm_models = service.get('llm_models', [])
# 如果指定了模型名称,尝试查找
target_model = None
if current_model_name:
target_model = next((m for m in llm_models if m.get('name') == current_model_name), None)
# 如果未找到指定模型,使用默认模型或第一个模型
if not target_model:
target_model = next((m for m in llm_models if m.get('is_default')),
llm_models[0] if llm_models else None)
if not target_model:
# 没有可用模型,回退到百度翻译
baidu_config = self.get_baidu_translate_config()
return {
"provider": "baidu",
"model": "",
"base_url": "",
"api_key": baidu_config.get('app_id', ''),
"secret_key": baidu_config.get('secret_key', ''),
"temperature": 0.7,
"max_tokens": 1000,
"top_p": 0.9,
"providers": {}
}
# 返回LLM翻译配置
api_key = service.get('api_key', '')
return {
"provider": service.get('id', ''),
"model": target_model.get('name', ''),
"base_url": service.get('base_url', ''),
"api_key": api_key,
"temperature": target_model.get('temperature', 0.7),
"max_tokens": target_model.get('max_tokens', 1000),
"top_p": target_model.get('top_p', 0.9),
"auto_unload": service.get('auto_unload', True) if service.get('type') == 'ollama' else None,
"providers": {}
}
def get_settings(self):
"""获取ComfyUI用户设置(从设置文件读取)"""
try:
# ComfyUI的设置文件通常位于 user/default/comfy.settings.json
# 需要找到ComfyUI的根目录
import sys
# 尝试从多个可能的路径查找设置文件
possible_paths = []
# 方法1: 通过当前文件路径向上查找
current_dir = os.path.dirname(os.path.abspath(__file__))
# custom_nodes/comfyui_prompt_assistant -> custom_nodes -> ComfyUI
comfyui_root = os.path.dirname(os.path.dirname(current_dir))
possible_paths.append(os.path.join(comfyui_root, "user", "default", "comfy.settings.json"))
# 方法2: 通过sys.path查找
for path in sys.path:
if 'ComfyUI' in path:
possible_paths.append(os.path.join(path, "user", "default", "comfy.settings.json"))
# 尝试读取设置文件
for settings_path in possible_paths:
if os.path.exists(settings_path):
try:
with open(settings_path, 'r', encoding='utf-8') as f:
settings_data = json.load(f)
# 返回设置数据
return settings_data
except Exception as e:
self._log(f"读取设置文件失败: {settings_path}, 错误: {str(e)}")
continue
# 如果都找不到,返回空字典
return {}
except Exception as e:
# 如果无法获取,返回空字典
self._log(f"获取用户设置失败: {str(e)}")
return {}
def update_baidu_translate_config(self, app_id=None, secret_key=None):
"""更新百度翻译配置"""
config = self.load_config()
if "baidu_translate" not in config:
config["baidu_translate"] = {}
# 仅更新提供的参数
if app_id is not None:
config["baidu_translate"]["app_id"] = app_id
if secret_key is not None:
config["baidu_translate"]["secret_key"] = secret_key
return self.save_config(config)
# --- 注意:validate_and_fix_system_prompts 已迁移到 migration_tool.py ---
# 系统提示词的验证和补全由 migration_tool 的增量更新逻辑统一处理
def validate_and_fix_active_prompts(self):
"""
验证激活提示词是否存在,如果不存在则修复
注意:此方法只修复 active_prompts.json(切换到存在的提示词)
不会恢复 system_prompts.json 中被删除的内容(尊重用户的删除操作)
"""
try:
system_prompts = self.load_system_prompts()
active_prompts = self.load_active_prompts()
# 标记是否需要更新激活提示词
modified = False
# 检查并修复扩写提示词
if "expand" in active_prompts:
expand_id = active_prompts["expand"]
expand_prompts = system_prompts.get("expand_prompts", {})
if expand_id not in expand_prompts:
# 激活的提示词不存在,切换到第一个可用的
if expand_prompts:
first_expand_id = next(iter(expand_prompts))
active_prompts["expand"] = first_expand_id
self._log(f"激活的扩写提示词 '{expand_id}' 不存在,已切换到 '{first_expand_id}'")
modified = True
else:
# 没有可用的扩写提示词,清空激活
active_prompts["expand"] = ""
self._log(f"警告:没有可用的扩写提示词")
modified = True
# 检查并修复中文反推提示词
if "vision_zh" in active_prompts:
vision_zh_id = active_prompts["vision_zh"]
vision_prompts = system_prompts.get("vision_prompts", {})
zh_prompts = {k: v for k, v in vision_prompts.items() if k.startswith("vision_zh_")}
if vision_zh_id not in vision_prompts:
if zh_prompts:
first_id = next(iter(zh_prompts))
active_prompts["vision_zh"] = first_id
self._log(f"激活的中文反推提示词 '{vision_zh_id}' 不存在,已切换到 '{first_id}'")
modified = True
else:
active_prompts["vision_zh"] = ""
self._log(f"警告:没有可用的中文反推提示词")
modified = True
# 检查并修复英文反推提示词
if "vision_en" in active_prompts:
vision_en_id = active_prompts["vision_en"]
vision_prompts = system_prompts.get("vision_prompts", {})
en_prompts = {k: v for k, v in vision_prompts.items() if k.startswith("vision_en_")}
if vision_en_id not in vision_prompts:
if en_prompts:
first_id = next(iter(en_prompts))
active_prompts["vision_en"] = first_id
self._log(f"激活的英文反推提示词 '{vision_en_id}' 不存在,已切换到 '{first_id}'")
modified = True
else:
active_prompts["vision_en"] = ""
self._log(f"警告:没有可用的英文反推提示词")
modified = True
# 如果需要更新,保存修复后的激活提示词
if modified:
self.save_active_prompts(active_prompts)
self._log("已完成激活提示词的验证和修复")
except Exception as e:
self._log(f"验证激活提示词异常: {str(e)}")
def validate_and_fix_model_params(self):
"""
验证并修复模型参数配置
注意: v2.0版本中,模型参数直接存储在 model_services 数组的模型对象中,
这个方法主要用于确保配置文件存在和格式正确
"""
try:
config = self.load_config()
# 确保是 v2.0 格式
if not self._is_v2_config(config):
self._log("[config.json] 警告: 检测到旧版本配置,请手动创建新的配置文件或使用默认配置")
return
# v2.0 格式中,参数已经在各个服务的模型列表中,无需额外验证
# 如果需要补全缺失的服务或模型参数,应该在服务商管理API中处理
except Exception as e:
self._log(f"[config.json] 验证模型参数配置时出错: {str(e)}")
# --- API Key 安全相关方法(方案A)---
@staticmethod
def mask_api_key(api_key: str) -> str:
"""
掩码API Key,只显示首尾部分
用于前端安全显示,防止API Key在Network中明文可见
参数:
api_key: 明文API Key
返回:
str: 掩码后的API Key
示例:
- sk-abc123xyz789 -> sk-abc***xyz789
- 短Key (< 8字符) -> ***
- 空字符串 -> ""
"""
if not api_key:
return ""
if len(api_key) < 8:
return "***"
# 显示前6个字符和后4个字符
return f"{api_key[:6]}***{api_key[-4:]}"
def get_llm_config_masked(self):
"""
获取LLM配置(API Key掩码版本)
用于前端显示,不暴露完整API Key
返回:
Dict: LLM配置,api_key字段被掩码
"""
config = self.get_llm_config()
if 'api_key' in config:
# 掩码API Key
config['api_key_masked'] = self.mask_api_key(config['api_key'])
config['api_key_exists'] = bool(config['api_key'])
# 移除明文API Key
del config['api_key']
# 处理所有providers的API Key
if 'providers' in config:
for provider_name, provider_config in config['providers'].items():
if 'api_key' in provider_config:
provider_config['api_key_masked'] = self.mask_api_key(provider_config['api_key'])
provider_config['api_key_exists'] = bool(provider_config['api_key'])
del provider_config['api_key']
return config
def get_vision_config_masked(self):
"""
获取视觉模型配置(API Key掩码版本)
用于前端显示,不暴露完整API Key
返回:
Dict: 视觉模型配置,api_key字段被掩码
"""
config = self.get_vision_config()
if 'api_key' in config:
# 掩码API Key
config['api_key_masked'] = self.mask_api_key(config['api_key'])
config['api_key_exists'] = bool(config['api_key'])
# 移除明文API Key
del config['api_key']
# 处理所有providers的API Key
if 'providers' in config:
for provider_name, provider_config in config['providers'].items():
if 'api_key' in provider_config:
provider_config['api_key_masked'] = self.mask_api_key(provider_config['api_key'])
provider_config['api_key_exists'] = bool(provider_config['api_key'])
del provider_config['api_key']
return config
# --- 服务商管理方法(CRUD)---
def get_all_services(self):
"""
获取所有服务商列表
返回:
List[Dict]: 服务商列表
"""
config = self.load_config()
if self._is_v2_config(config):
return config.get('model_services', [])
else:
# v1.0不支持此功能
return []
def get_service(self, service_id: str):
"""
获取指定服务商的完整配置
参数:
service_id: 服务商ID
返回:
Dict: 服务商配置,不存在返回None
"""
return self._get_service_by_id(service_id)
def create_service(self, service_type: str, name: str = "", base_url: str = "",
api_key: str = "", description: str = ""):
"""
创建新的服务商
参数:
service_type: 服务类型 ('openai_compatible' 或 'ollama')
name: 服务商名称(如果为空,自动生成)
base_url: Base URL
api_key: API Key(明文存储)
description: 描述
返回:
str: 新创建的service_id,失败返回None
"""
try:
config = self.load_config()
if not self._is_v2_config(config):
self._log("创建服务商失败: 配置版本过低,请先迁移到v2.0")
return None
# 获取现有服务商列表
current_services = config.get('model_services', [])
# 生成服务商ID和名称
service_id, auto_name = self._generate_service_id_and_name(service_type, current_services)
# 如果用户没有提供名称,使用自动生成的名称
if not name:
name = auto_name
# 创建服务配置
new_service = {
"id": service_id,
"type": service_type,
"name": name,
"description": description,
"base_url": base_url,
"api_key": api_key or "",
"disable_thinking": True,
"enable_advanced_params": True,
"filter_thinking_output": True,
"llm_models": [],
"vlm_models": []
}
# Ollama特有配置
if service_type == "ollama":
new_service["auto_unload"] = True
# 添加到配置
if 'model_services' not in config:
config['model_services'] = []
config['model_services'].append(new_service)
# 保存配置
if self.save_config(config):
self._log(f"成功创建服务商: {name} (ID: {service_id})")
return service_id
else:
self._log(f"保存服务商配置失败: {name}")
return None
except Exception as e:
self._log(f"创建服务商异常: {str(e)}")
import traceback
traceback.print_exc()
return None
def _generate_service_id_and_name(self, service_type: str, current_services: list) -> tuple:
"""
生成服务商ID和默认名称
参数:
service_type: 服务类型
current_services: 现有服务商列表
返回:
tuple: (service_id, default_name)
"""
import random
# 类型映射
type_map = {
"ollama": {
"name_prefix": "Ollama服务",
"id_prefix": "ollama"
},
"openai_compatible": {
"name_prefix": "通用服务",
"id_prefix": "service"
}
}
# 获取类型配置
type_config = type_map.get(service_type, {
"name_prefix": "新服务",
"id_prefix": service_type
})
name_prefix = type_config["name_prefix"]
id_prefix = type_config["id_prefix"]
# 收集已使用的编号
existing_numbers = set()
for service in current_services:
sid = service.get('id', '')
# 匹配格式:{id_prefix}_{数字}
if sid.startswith(f"{id_prefix}_"):
try:
num_str = sid.split('_')[-1]
if num_str.isdigit():
existing_numbers.add(int(num_str))
except:
pass
# 生成随机三位数(100-999),最多尝试100次
max_attempts = 100
for _ in range(max_attempts):
random_number = random.randint(100, 999)
if random_number not in existing_numbers:
break
else:
# 如果100次都重复,使用更大的随机数(4位数)
random_number = random.randint(1000, 9999)
while random_number in existing_numbers:
random_number = random.randint(1000, 9999)
# 生成ID和名称
service_id = f"{id_prefix}_{random_number}"
default_name = f"{name_prefix}-{random_number}"
return service_id, default_name
def delete_service(self, service_id: str):
"""
删除服务商
参数:
service_id: 服务商ID
返回:
bool: 成功返回True
"""
try:
config = self.load_config()
if not self._is_v2_config(config):
self._log("删除服务商失败: 配置版本过低")
return False
services = config.get('model_services', [])
# 查找并删除服务
original_length = len(services)
config['model_services'] = [s for s in services if s.get('id') != service_id]
if len(config['model_services']) == original_length:
self._log(f"删除服务商失败: 服务商不存在 (ID: {service_id})")
return False
# 如果删除的是当前服务,清除current_services引用
current_services = config.get('current_services', {})
if current_services.get('llm') == service_id:
current_services['llm'] = None
if current_services.get('vlm') == service_id:
current_services['vlm'] = None
if current_services.get('translate') == service_id:
current_services['translate'] = None
# 保存配置
if self.save_config(config):
self._log(f"成功删除服务商: {service_id}")
return True
else:
self._log(f"保存配置失败")
return False
except Exception as e:
self._log(f"删除服务商异常: {str(e)}")
import traceback
traceback.print_exc()
return False
def update_services_order(self, service_ids: list) -> bool:
"""
更新服务商顺序
参数:
service_ids: 服务商ID列表,按新顺序排列
返回:
bool: 成功返回True
"""
try:
config = self.load_config()
if not self._is_v2_config(config):
self._log("更新服务商顺序失败: 配置版本过低")
return False
services = config.get('model_services', [])
# 创建ID到服务的映射
service_map = {s.get('id'): s for s in services}
# 验证所有service_id都存在
for service_id in service_ids:
if service_id not in service_map:
self._log(f"更新服务商顺序失败: 服务商不存在 (ID: {service_id})")
return False
# 按新顺序重建services数组
new_services = []
for service_id in service_ids:
new_services.append(service_map[service_id])
# 添加未在service_ids中的服务(防止遗漏)
for service_id, service in service_map.items():
if service_id not in service_ids:
new_services.append(service)
self._log(f"警告: 服务商 {service_id} 不在新顺序中,已追加到末尾")
config['model_services'] = new_services
# 保存配置
if self.save_config(config):
self._log(f"成功更新服务商顺序: {', '.join(service_ids)}")
return True
else:
self._log("保存配置失败")
return False
except Exception as e:
self._log(f"更新服务商顺序异常: {str(e)}")
import traceback
traceback.print_exc()
return False
def update_service(self, service_id: str, **kwargs):
"""
更新服务商配置
参数:
service_id: 服务商ID
**kwargs: 要更新的字段(name, description, base_url, api_key, auto_unload等)
返回:
bool: 成功返回True
"""
try:
config = self.load_config()
if not self._is_v2_config(config):
self._log("更新服务商失败: 配置版本过低")
return False
# 查找服务
services = config.get('model_services', [])
service = None
service_index = -1
for i, s in enumerate(services):
if s.get('id') == service_id:
service = s
service_index = i
break
if not service:
self._log(f"更新服务商失败: 服务商不存在 (ID: {service_id})")
return False
# 更新字段
if 'name' in kwargs:
service['name'] = kwargs['name']
if 'description' in kwargs:
service['description'] = kwargs['description']
if 'base_url' in kwargs:
service['base_url'] = kwargs['base_url']
if 'api_key' in kwargs:
# 直接使用明文API Key
service['api_key'] = kwargs['api_key'] or ""
if 'auto_unload' in kwargs and service.get('type') == 'ollama':
service['auto_unload'] = kwargs['auto_unload']
if 'disable_thinking' in kwargs:
service['disable_thinking'] = kwargs['disable_thinking']
if 'enable_advanced_params' in kwargs:
service['enable_advanced_params'] = kwargs['enable_advanced_params']
if 'filter_thinking_output' in kwargs:
service['filter_thinking_output'] = kwargs['filter_thinking_output']
# 更新services数组
config['model_services'][service_index] = service
# 保存配置
if self.save_config(config):
self._log(f"成功更新服务商: {service_id}")
return True
else:
self._log(f"保存配置失败")
return False
except Exception as e:
self._log(f"更新服务商异常: {str(e)}")
import traceback
traceback.print_exc()
return False
def set_current_service(self, service_type: str, service_id: str, model_name: str = None):
"""
设置当前使用的服务商和模型
参数:
service_type: 服务类型 ('llm', 'vlm', 或 'translate')
service_id: 服务商ID
model_name: 模型名称(可选,如果不提供则使用该服务的默认模型或第一个模型)
返回:
bool: 成功返回True
"""
try:
config = self.load_config()
if not self._is_v2_config(config):
self._log("设置当前服务商失败: 配置版本过低")
return False
# ---百度翻译特殊处理---
# 百度翻译使用独立的baidu_translate配置,不在model_services中
if service_id == 'baidu':
# 百度翻译支持LLM服务类型(旧兼容)和translate服务类型
if service_type not in ['llm', 'translate']:
self._log(f"设置当前服务商失败: 百度翻译不支持{service_type}服务类型")
return False
# 确保baidu_translate配置存在
if 'baidu_translate' not in config:
config['baidu_translate'] = {"app_id": "", "secret_key": ""}
# 确保current_services结构存在
if 'current_services' not in config:
config['current_services'] = {}
# 设置百度为当前服务(无模型概念)
config['current_services'][service_type] = {
"service": "baidu",
"model": ""
}
# 保存配置
if self.save_config(config):
self._log(f"当前服务商已切换: 百度翻译 ({service_type})")
return True
else:
self._log("设置当前服务商失败: 保存配置失败")
return False
# ---其他服务:验证服务存在---
service = self._get_service_by_id(service_id)
if not service:
self._log(f"设置当前服务商失败: 服务商不存在 (ID: {service_id})")
return False
# 根据service_type确定模型列表字段
model_list_key = f'{service_type}_models'
if service_type == 'translate':
model_list_key = 'llm_models'
# 如果提供了model_name,验证模型是否存在
if model_name:
model_list = service.get(model_list_key, [])
model_exists = any(m.get('name') == model_name for m in model_list)
if not model_exists:
self._log(f"设置当前服务商失败: 模型不存在 (模型: {model_name}, 服务: {service_id})")
return False
# 确保current_services结构存在
if 'current_services' not in config:
config['current_services'] = {}
# 获取当前服务信息(兼容旧格式)
current_info = config['current_services'].get(service_type)
# 设置新格式的current_services
if model_name:
# 明确指定了模型
config['current_services'][service_type] = {
"service": service_id,
"model": model_name
}
else:
# 未指定模型,使用默认模型或第一个模型
model_list = service.get(model_list_key, [])
# 如果是百度服务,没有模型
if service.get('id') == 'baidu' or service.get('type') == 'baidu':
config['current_services'][service_type] = {
"service": service_id,
"model": ""
}
else:
# 查找默认模型或第一个模型
default_model = next((m for m in model_list if m.get('is_default')),
model_list[0] if model_list else None)
if default_model:
config['current_services'][service_type] = {
"service": service_id,
"model": default_model.get('name', '')
}
else:
# 没有模型,只设置服务
config['current_services'][service_type] = {
"service": service_id,
"model": ""
}
# 保存配置
if self.save_config(config):
service_name = service.get('name', service_id)
log_model = f" | 模型:{model_name}" if model_name else ""
self._log(f"成功设置当前{service_type}服务: {service_name}{log_model}")
return True
else:
self._log(f"保存配置失败")
return False
except Exception as e:
import traceback
traceback.print_exc()
return False
# --- 模型管理方法 ---
def add_model_to_service(self, service_id: str, model_type: str, model_name: str,
temperature: float = 0.7, top_p: float = 0.9, max_tokens: int = 4096):
"""添加模型到服务商"""
try:
config = self.load_config()
services = config.get('model_services', [])
for i, service in enumerate(services):
if service.get('id') == service_id:
model_list_key = 'llm_models' if model_type == 'llm' else 'vlm_models'
if model_list_key not in service:
service[model_list_key] = []
# 检查是否已存在
if any(m.get('name') == model_name for m in service[model_list_key]):
self._log(f"模型已存在: {model_name}")
return False
# 添加新模型
new_model = {
"name": model_name,
"is_default": len(service[model_list_key]) == 0,
"temperature": temperature,
"top_p": top_p,
"max_tokens": max_tokens
}
service[model_list_key].append(new_model)
config['model_services'][i] = service
if self.save_config(config):
self._log(f"成功添加模型: {model_name}")
return True
return False
self._log(f"服务商不存在: {service_id}")
return False
except Exception as e:
self._log(f"添加模型异常: {str(e)}")
return False
def delete_model_from_service(self, service_id: str, model_type: str, model_name: str):
"""从服务商删除模型"""
try:
config = self.load_config()
services = config.get('model_services', [])
for i, service in enumerate(services):
if service.get('id') == service_id:
model_list_key = 'llm_models' if model_type == 'llm' else 'vlm_models'
if model_list_key not in service:
return False
original_length = len(service[model_list_key])
service[model_list_key] = [m for m in service[model_list_key] if m.get('name') != model_name]
if len(service[model_list_key]) == original_length:
self._log(f"模型不存在: {model_name}")
return False
# 如果删除的是默认模型,设置第一个为默认
if len(service[model_list_key]) > 0:
if not any(m.get('is_default') for m in service[model_list_key]):
service[model_list_key][0]['is_default'] = True
config['model_services'][i] = service
if self.save_config(config):
self._log(f"成功删除模型: {model_name}")
return True
return False
self._log(f"服务商不存在: {service_id}")
return False
except Exception as e:
self._log(f"删除模型异常: {str(e)}")
return False
def set_default_model(self, service_id: str, model_type: str, model_name: str):
"""设置默认模型"""
try:
config = self.load_config()
services = config.get('model_services', [])
for i, service in enumerate(services):
if service.get('id') == service_id:
model_list_key = 'llm_models' if model_type == 'llm' else 'vlm_models'
if model_list_key not in service:
return False
found = False
for model in service[model_list_key]:
if model.get('name') == model_name:
model['is_default'] = True
found = True
else:
model['is_default'] = False
if not found:
self._log(f"模型不存在: {model_name}")
return False
config['model_services'][i] = service
if self.save_config(config):
self._log(f"成功设置默认模型: {model_name}")
return True
return False
self._log(f"服务商不存在: {service_id}")
return False
except Exception as e:
self._log(f"设置默认模型异常: {str(e)}")
return False
def update_model_order(self, service_id: str, model_type: str, model_names: list):
"""更新模型顺序"""
try:
config = self.load_config()
services = config.get('model_services', [])
for i, service in enumerate(services):
if service.get('id') == service_id:
model_list_key = 'llm_models' if model_type == 'llm' else 'vlm_models'
if model_list_key not in service:
return False
# 创建模型字典
model_dict = {m.get('name'): m for m in service[model_list_key]}
# 按新顺序重新排列
new_model_list = []
for name in model_names:
if name in model_dict:
new_model_list.append(model_dict[name])
service[model_list_key] = new_model_list
config['model_services'][i] = service
if self.save_config(config):
self._log(f"成功更新模型顺序")
return True
return False
self._log(f"服务商不存在: {service_id}")
return False
except Exception as e:
self._log(f"更新模型顺序异常: {str(e)}")
return False
def update_model_parameter(self, service_id: str, model_type: str, model_name: str,
parameter_name: str, parameter_value):
"""更新模型参数"""
try:
config = self.load_config()
services = config.get('model_services', [])
for i, service in enumerate(services):
if service.get('id') == service_id:
model_list_key = 'llm_models' if model_type == 'llm' else 'vlm_models'
if model_list_key not in service:
return False
# 查找模型并更新参数
for model in service[model_list_key]:
if model.get('name') == model_name:
model[parameter_name] = parameter_value
config['model_services'][i] = service
if self.save_config(config):
self._log(f"成功更新模型参数: {model_name}.{parameter_name} = {parameter_value}")
return True
return False
self._log(f"模型不存在: {model_name}")
return False
self._log(f"服务商不存在: {service_id}")
return False
except Exception as e:
self._log(f"更新模型参数异常: {str(e)}")
return False
# 创建全局配置管理器实例
config_manager = ConfigManager()