Files
yawiii-ComfyUI-Prompt-Assis…/utils/migration_tool.py
T
2026-01-10 17:38:47 +08:00

1117 lines
45 KiB
Python
Raw 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
class MigrationTool:
"""数据迁移工具类"""
def __init__(self, plugin_dir, user_base_dir, logger=None):
"""
初始化迁移工具
参数:
plugin_dir: 插件目录路径
user_base_dir: 用户配置基础目录
logger: 日志函数(可选)
"""
self.plugin_dir = plugin_dir
self.user_base_dir = user_base_dir
if logger:
self._log_func = logger
else:
def default_logger(msg):
from .common import _ANSI_CLEAR_EOL
print(f"\r{_ANSI_CLEAR_EOL}{msg}", flush=True)
self._log_func = default_logger
# 定义路径
self.legacy_config_dir = os.path.join(plugin_dir, "config")
self.config_dir = os.path.join(user_base_dir, "config")
self.tags_dir = os.path.join(user_base_dir, "tags")
self.rules_dir = os.path.join(user_base_dir, "rules")
def _log(self, msg: str):
"""统一日志调用层"""
self._log_func(msg)
# ---版本比对工具---
def _compare_versions(self, v1: str, v2: str) -> int:
"""
比较两个版本号
返回:
1: v1 > v2
0: v1 == v2
-1: v1 < v2
"""
def parse(v):
return [int(x) for x in str(v).split('.')]
p1, p2 = parse(v1), parse(v2)
# 补齐长度
max_len = max(len(p1), len(p2))
p1.extend([0] * (max_len - len(p1)))
p2.extend([0] * (max_len - len(p2)))
for a, b in zip(p1, p2):
if a > b: return 1
if a < b: return -1
return 0
# ---config.json 专用迁移---
def ensure_config_json_exists(self, file_path: str, default_data: dict, legacy_path: str = None) -> bool:
"""
确保 config.json 存在(专用迁移逻辑)
逻辑:
1. 文件存在 → 跳过
2. 文件不存在 + 旧文件存在 → 提取 API Key 和模型信息,映射到新服务商
3. 文件不存在 + 无旧文件 → 创建默认配置
"""
if os.path.exists(file_path):
return False
os.makedirs(os.path.dirname(file_path), exist_ok=True)
# 检查旧版本文件
if legacy_path and os.path.exists(legacy_path):
try:
with open(legacy_path, 'r', encoding='utf-8') as f:
legacy_data = json.load(f)
# 执行专用迁移:提取 API Key 和模型信息
migrated_data = self._migrate_config_api_keys_to_services(legacy_data, default_data)
self._save_with_version(file_path, migrated_data, default_data)
self._log("[config.json] 从插件目录迁移旧文件完成(已提取 API Key 和模型信息)")
return True
except Exception as e:
self._log(f"[config.json] 迁移旧文件失败: {str(e)},使用默认配置")
# 创建默认配置
self._log("[config.json] 文件不存在,创建默认配置...")
self._save_with_version(file_path, default_data, default_data)
return True
def _migrate_config_api_keys_to_services(self, legacy_data: dict, default_data: dict) -> dict:
"""
从旧配置提取 API Key 和模型信息,映射到新配置的服务商
逻辑:
1. 遍历旧配置中有 API Key 的服务
2. 在新配置中查找对应的服务商 ID
3. 如果找到,填入 API Key 和模型信息
4. 如果找不到,创建新服务商
"""
import copy
result = copy.deepcopy(default_data)
# 提取旧配置中的服务信息
legacy_services = legacy_data.get('model_services', [])
if not legacy_services:
# 兼容更旧的格式(llm/vlm providers)
legacy_services = self._extract_legacy_providers(legacy_data)
# 构建新配置服务商的 ID 映射
new_services = result.get('model_services', [])
service_id_map = {s.get('id'): i for i, s in enumerate(new_services)}
for legacy_service in legacy_services:
api_key = legacy_service.get('api_key', '').strip()
if not api_key:
continue
service_id = legacy_service.get('id', '')
if service_id in service_id_map:
# 服务商存在,更新 API Key 和模型信息
idx = service_id_map[service_id]
new_services[idx]['api_key'] = api_key
# 迁移模型信息
for model_type in ['llm_models', 'vlm_models']:
legacy_models = legacy_service.get(model_type, [])
if legacy_models:
new_services[idx][model_type] = legacy_models
# 迁移其他配置
for key in ['base_url', 'auto_unload', 'disable_thinking', 'enable_advanced_params', 'filter_thinking_output']:
if key in legacy_service:
new_services[idx][key] = legacy_service[key]
self._log(f"[config.json] 迁移服务商: {service_id}")
else:
# 服务商不存在,创建新服务商
new_service = {
'id': service_id,
'type': legacy_service.get('type', 'openai_compatible'),
'name': legacy_service.get('name', service_id),
'description': legacy_service.get('description', f'{service_id}(从旧版迁移)'),
'base_url': legacy_service.get('base_url', ''),
'api_key': api_key,
'disable_thinking': legacy_service.get('disable_thinking', True),
'enable_advanced_params': legacy_service.get('enable_advanced_params', True),
'filter_thinking_output': legacy_service.get('filter_thinking_output', True),
'llm_models': legacy_service.get('llm_models', []),
'vlm_models': legacy_service.get('vlm_models', [])
}
new_services.append(new_service)
self._log(f"[config.json] 创建新服务商: {service_id}")
# 迁移 current_services
if 'current_services' in legacy_data:
result['current_services'] = legacy_data['current_services']
# 迁移百度翻译配置
if 'baidu_translate' in legacy_data:
result['baidu_translate'] = legacy_data['baidu_translate']
result['model_services'] = new_services
return result
def _extract_legacy_providers(self, legacy_data: dict) -> list:
"""
从更旧的配置格式(llm/vlm providers)提取服务信息
兼容 v1.0 格式:
{
"llm": {"providers": {"zhipu": {...}, "custom": {...}}},
"vlm": {"providers": {"zhipu": {...}}}
}
"""
services = []
provider_map = {} # 用于合并同一个 provider 的 llm 和 vlm 配置
for service_type in ['llm', 'vlm']:
if service_type not in legacy_data:
continue
providers = legacy_data[service_type].get('providers', {})
for provider_name, provider_config in providers.items():
api_key = provider_config.get('api_key', '').strip()
if not api_key:
continue
if provider_name not in provider_map:
provider_map[provider_name] = {
'id': provider_name,
'type': 'openai_compatible',
'name': provider_config.get('name', provider_name),
'base_url': provider_config.get('base_url', ''),
'api_key': api_key,
'llm_models': [],
'vlm_models': []
}
# 添加模型信息
model_name = provider_config.get('model', '')
if model_name:
models_key = f'{service_type}_models'
provider_map[provider_name][models_key].append({
'name': model_name,
'display_name': '',
'is_default': True,
'temperature': provider_config.get('temperature', 0.7),
'max_tokens': provider_config.get('max_tokens', 1000),
'top_p': provider_config.get('top_p', 0.9)
})
services = list(provider_map.values())
return services
# ---统一保存方法---
def _save_with_version(self, file_path: str, data: dict, default_data: dict) -> bool:
"""
保存配置文件,自动处理版本号
参数:
file_path: 目标文件路径
data: 要保存的数据
default_data: 默认配置(用于获取版本号)
返回:
bool: 保存成功返回 True
"""
try:
# 确保版本号存在且在开头
version = data.get('__config_version') or default_data.get('__config_version', '2.0')
data = {'__config_version': version, **{k: v for k, v in data.items() if k != '__config_version'}}
with open(file_path, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=2)
return True
except Exception as e:
self._log(f"保存文件失败 [{os.path.basename(file_path)}]: {str(e)}")
return False
def _ensure_simple_config(self, file_path: str, default_data: dict, file_desc: str = "config") -> bool:
"""
简单确保配置文件存在(不进行版本管理和迁移)
逻辑:
- 文件存在 → 跳过
- 文件不存在 → 创建默认配置(不含版本号)
适用于: active_prompts.json, tags_user.json 等简单配置文件
"""
if os.path.exists(file_path):
return False
os.makedirs(os.path.dirname(file_path), exist_ok=True)
try:
# 移除版本号(这些文件不需要版本管理)
data_to_save = {k: v for k, v in default_data.items() if not k.startswith('__')}
with open(file_path, 'w', encoding='utf-8') as f:
json.dump(data_to_save, f, ensure_ascii=False, indent=2)
self._log(f"[{file_desc}] 文件不存在,创建默认配置...")
return True
except Exception as e:
self._log(f"[{file_desc}] 创建默认配置失败: {str(e)}")
return False
# ---配置文件确保存在---
def ensure_config_exists(self, file_path: str, default_data: dict, legacy_path: str = None, file_desc: str = "config") -> bool:
"""
确保单个配置文件存在
逻辑:
1. 文件存在 → 跳过(增量更新由 migrate_incremental_updates 处理)
2. 文件不存在 + 旧文件存在 → 迁移旧文件,与默认配置合并,添加版本号
3. 文件不存在 + 无旧文件 → 创建默认配置
参数:
file_path: 目标文件路径
default_data: 默认配置数据
legacy_path: 旧版本文件路径(可选)
file_desc: 文件描述(用于日志)
返回:
bool: True 表示创建了新文件,False 表示文件已存在
"""
if os.path.exists(file_path):
return False
# 确保目录存在
os.makedirs(os.path.dirname(file_path), exist_ok=True)
# 检查是否有旧版本文件
if legacy_path and os.path.exists(legacy_path):
try:
with open(legacy_path, 'r', encoding='utf-8') as f:
legacy_data = json.load(f)
# 与默认配置合并,补全缺失的字段
merged_data = self._merge_with_defaults(legacy_data, default_data, file_desc)
self._save_with_version(file_path, merged_data, default_data)
self._log(f"[{file_desc}] 从插件目录迁移旧文件完成")
return True
except Exception as e:
self._log(f"[{file_desc}] 迁移旧文件失败: {str(e)},使用默认配置")
# 创建默认配置
self._log(f"[{file_desc}] 文件不存在,创建默认配置...")
self._save_with_version(file_path, default_data, default_data)
return True
def _merge_with_defaults(self, user_data: dict, default_data: dict, file_desc: str = "") -> dict:
"""
将用户数据与默认配置合并,补全缺失的字段
参数:
user_data: 用户数据(旧版本)
default_data: 默认配置
file_desc: 文件描述(用于日志)
返回:
合并后的数据
"""
import copy
result = copy.deepcopy(default_data)
# 递归合并用户数据到结果中(用户数据优先)
self._recursive_merge(result, user_data, file_desc)
return result
def _recursive_merge(self, base: dict, overlay: dict, file_desc: str = "", path: str = ""):
"""
递归合并:将 overlay 的值覆盖到 base 中(保留 base 的结构)
策略:
- overlay 中存在的键覆盖 base 中的值
- 如果都是 dict,递归合并
- 跳过版本字段(由 _save_with_version 处理)
"""
for key, value in overlay.items():
# 跳过版本字段
if key.startswith("__"):
continue
current_path = f"{path}.{key}" if path else key
if key in base:
if isinstance(base[key], dict) and isinstance(value, dict):
# 递归合并嵌套字典
self._recursive_merge(base[key], value, file_desc, current_path)
else:
# 直接覆盖(用户值优先)
base[key] = value
else:
# overlay 中有但 base 中没有的键,直接添加(用户自定义内容)
base[key] = value
def ensure_all_configs_exist(self, default_configs: dict, legacy_dir: str):
"""
确保所有配置文件存在
参数:
default_configs: 默认配置字典
legacy_dir: 旧版本文件目录
"""
# config.json(使用专用迁移方法)
if 'config' in default_configs:
self.ensure_config_json_exists(
os.path.join(self.config_dir, "config.json"),
default_configs['config'],
os.path.join(legacy_dir, "config.json")
)
# system_prompts.json
if 'system_prompts' in default_configs:
self.ensure_config_exists(
os.path.join(self.rules_dir, "system_prompts.json"),
default_configs['system_prompts'],
os.path.join(legacy_dir, "system_prompts.json"),
"system_prompts.json"
)
# active_prompts.json 和 tags_user.json 不需要版本管理和迁移,
# 直接在文件不存在时创建默认配置
self._ensure_simple_config(
os.path.join(self.config_dir, "active_prompts.json"),
default_configs.get('active_prompts', {}),
"active_prompts.json"
)
self._ensure_simple_config(
os.path.join(self.config_dir, "tags_user.json"),
default_configs.get('tags_user', {"favorites": []}),
"tags_user.json"
)
# kontext_presets.json
if 'kontext_presets' in default_configs:
self.ensure_config_exists(
os.path.join(self.rules_dir, "kontext_presets.json"),
default_configs['kontext_presets'],
os.path.join(legacy_dir, "kontext_presets.json"),
"kontext_presets.json"
)
def migrate_incremental_updates(self, default_configs):
"""
执行增量更新:将默认配置中的新字段添加到用户配置中
参数:
default_configs: 包含各类默认配置的字典
{
'config': ...,
'system_prompts': ...,
'kontext_presets': ...
}
"""
try:
results = {}
# 1. 更新 config.json
if 'config' in default_configs:
results['config_update'] = self._update_config_json(default_configs['config'])
# 2. 更新 system_prompts.json
if 'system_prompts' in default_configs:
results['system_prompts_update'] = self._update_json_file(
os.path.join(self.rules_dir, "system_prompts.json"),
default_configs['system_prompts'],
"system_prompts"
)
# active_prompts.json 和 tags_user.json 不需要增量更新
# (结构简单,没有版本管理需求)
# 5. 更新 kontext_presets.json
if 'kontext_presets' in default_configs:
results['kontext_presets_update'] = self._update_json_file(
os.path.join(self.rules_dir, "kontext_presets.json"),
default_configs['kontext_presets'],
"kontext_presets"
)
return results
except Exception as e:
self._log(f"增量更新失败: {str(e)}")
return {}
def _update_config_json(self, default_config):
"""
处理 config.json 的增量更新 (带版本检查)
合并策略(与其他配置文件统一):
1. 版本比对
2. 根级字段: 使用通用的 _deep_merge_defaults 补全
3. model_services: 按 id 匹配,只补全用户已有服务的缺失字段(不追加新服务)
4. 完成后同步版本号
"""
user_config_path = os.path.join(self.config_dir, "config.json")
if not os.path.exists(user_config_path):
return False
try:
with open(user_config_path, 'r', encoding='utf-8') as f:
user_config = json.load(f)
# 获取版本号
template_version = default_config.get('__config_version', '2.0')
user_version = user_config.get('__config_version') # 不设默认值
# 如果用户文件没有版本号,只补上版本号(静默跳过)
if user_version is None:
user_config = {'__config_version': template_version, **user_config}
with open(user_config_path, 'w', encoding='utf-8') as f:
json.dump(user_config, f, ensure_ascii=False, indent=2)
return True
# 版本比对:模板版本 <= 用户版本时跳过
cmp_result = self._compare_versions(template_version, user_version)
if cmp_result <= 0:
return False
import copy
# 1. 根级字段补全(排除 model_services,单独处理)
for key, value in default_config.items():
if key == "model_services":
continue # model_services 单独处理
if key not in user_config:
user_config[key] = copy.deepcopy(value)
self._log(f"[config.json] 补全根字段: {key}")
elif isinstance(value, dict) and isinstance(user_config[key], dict):
# 递归合并嵌套字典(如 baidu_translate、current_services)
self._deep_merge_defaults(user_config[key], value)
# 2. model_services 按 id 匹配合并
self._merge_model_services(user_config, default_config)
# 更新版本号(重构字典确保版本号在开头)
user_config = {'__config_version': template_version, **{k: v for k, v in user_config.items() if k != '__config_version'}}
with open(user_config_path, 'w', encoding='utf-8') as f:
json.dump(user_config, f, ensure_ascii=False, indent=2)
self._log(f"[config.json] 增量更新已完成 (v{user_version} -> v{template_version})")
return True
except Exception as e:
self._log(f"[config.json] 更新检查出错: {str(e)}")
return False
def _merge_model_services(self, user_config, default_config):
"""
按 id 匹配合并 model_services(完整策略)
策略:
- 补全用户已有服务的缺失字段
- 追加模板中用户不存在的服务商(版本更新时的新服务商)
- 不覆盖 llm_models/vlm_models(用户自定义的模型列表)
"""
if 'model_services' not in default_config:
return
if 'model_services' not in user_config:
user_config['model_services'] = []
# 构建用户服务的 id 集合
user_service_ids = {s.get('id') for s in user_config['model_services'] if s.get('id')}
import copy
# 1. 补全用户已有服务的缺失字段
template_services_map = {
s.get('id'): s for s in default_config['model_services'] if s.get('id')
}
for user_service in user_config['model_services']:
service_id = user_service.get('id')
if not service_id or service_id not in template_services_map:
continue
template_service = template_services_map[service_id]
service_name = user_service.get('name', service_id)
# 补全服务级别的缺失字段
for key, value in template_service.items():
if key in ['llm_models', 'vlm_models']:
# 模型列表不补全(用户自定义)
continue
if key not in user_service:
user_service[key] = copy.deepcopy(value)
self._log(f"[config.json] 补全服务 '{service_name}' 字段: {key}")
# 2. 追加模板中用户不存在的服务商
for template_service in default_config['model_services']:
service_id = template_service.get('id')
if not service_id or service_id in user_service_ids:
continue
# 追加新服务商
new_service = copy.deepcopy(template_service)
user_config['model_services'].append(new_service)
self._log(f"[config.json] 追加新服务商: {new_service.get('name', service_id)}")
def _update_json_file(self, file_path, default_data, file_desc):
"""
通用的 JSON 文件增量更新 (带版本检查)
逻辑:
1. 检查用户文件是否有版本号
2. 如果没有版本号,只补上当前模板版本号(跳过增量更新)
3. 如果有版本号,比对模板版本和用户版本
4. 仅当模板版本 > 用户版本时执行增量
5. 增量完成后同步用户版本号
"""
if not os.path.exists(file_path):
return False
try:
with open(file_path, 'r', encoding='utf-8') as f:
user_data = json.load(f)
# 获取版本号
template_version = default_data.get('__config_version', '2.0')
user_version = user_data.get('__config_version') # 不设默认值
# 如果用户文件没有版本号,只补上版本号(静默处理)
if user_version is None:
user_data = {'__config_version': template_version, **user_data}
self._save_with_version(file_path, user_data, default_data)
return True
# 版本比对:模板版本 <= 用户版本时跳过
cmp_result = self._compare_versions(template_version, user_version)
if cmp_result <= 0:
return False
# 执行深度合并
modified = self._deep_merge_defaults(user_data, default_data)
# ---特殊处理:system_prompts 规则内容覆盖---
if file_desc == "system_prompts":
modified = self._overwrite_prompts_from_template(user_data, default_data) or modified
# ---特殊处理:为 system_prompts 中的所有规则补全 category 和 showIn 字段---
if file_desc == "system_prompts":
modified = self._ensure_prompts_have_category(user_data) or modified
modified = self._ensure_prompts_have_show_in(user_data) or modified
# 无论是否有字段变更,都需要更新版本号(重构字典确保版本号在开头)
user_data = {'__config_version': template_version, **{k: v for k, v in user_data.items() if k != '__config_version'}}
with open(file_path, 'w', encoding='utf-8') as f:
json.dump(user_data, f, ensure_ascii=False, indent=2)
self._log(f"[{file_desc}.json] 增量更新已完成 (v{user_version} -> v{template_version})")
return True
except Exception as e:
self._log(f"[{file_desc}.json] 更新检查出错: {str(e)}")
return False
def _ensure_prompts_have_category(self, system_prompts_data):
"""
确保 system_prompts 中的所有规则都有 category 字段
为所有规则类型(expand_prompts、vision_prompts、video_prompts)中的每个规则
补全 category 字段(默认值为空字符串)
返回是否发生了修改
"""
modified = False
# 需要处理的规则类型
prompt_types = ['expand_prompts', 'vision_prompts', 'video_prompts']
for prompt_type in prompt_types:
if prompt_type not in system_prompts_data:
continue
prompts = system_prompts_data[prompt_type]
if not isinstance(prompts, dict):
continue
for prompt_id, prompt_data in prompts.items():
if not isinstance(prompt_data, dict):
continue
# 为规则补全 category 字段
if 'category' not in prompt_data:
prompt_data['category'] = ''
modified = True
return modified
def _ensure_prompts_have_show_in(self, system_prompts_data):
"""
确保 system_prompts 中的所有规则都有 showIn 字段
为所有规则类型(expand_prompts、vision_prompts、video_prompts)中的每个规则
补全 showIn 字段(默认值为 ["frontend", "node"])
返回是否发生了修改
"""
modified = False
# 需要处理的规则类型
prompt_types = ['expand_prompts', 'vision_prompts', 'video_prompts']
for prompt_type in prompt_types:
if prompt_type not in system_prompts_data:
continue
prompts = system_prompts_data[prompt_type]
if not isinstance(prompts, dict):
continue
for prompt_id, prompt_data in prompts.items():
if not isinstance(prompt_data, dict):
continue
# 为规则补全 showIn 字段
if 'showIn' not in prompt_data:
prompt_data['showIn'] = ["frontend", "node"]
modified = True
return modified
def _overwrite_prompts_from_template(self, user_data, template_data):
"""
用模板中的规则内容覆盖用户配置中的同名规则
当版本更新时(模板版本 > 用户版本),将模板中的规则内容
完整覆盖到用户配置中的对应规则,确保内置规则保持最新。
覆盖策略:
- 仅覆盖模板和用户配置中都存在的同名规则
- 用户自定义的规则(模板中不存在)保持不变
- 模板中新增的规则由 _deep_merge_defaults 处理
返回是否发生了修改
"""
import copy
modified = False
# 需要处理的规则类型
prompt_types = ['expand_prompts', 'vision_prompts', 'video_prompts', 'translate_prompts']
for prompt_type in prompt_types:
# 检查模板中是否有该类型
if prompt_type not in template_data:
continue
template_prompts = template_data[prompt_type]
if not isinstance(template_prompts, dict):
continue
# 检查用户配置中是否有该类型
if prompt_type not in user_data:
continue
user_prompts = user_data[prompt_type]
if not isinstance(user_prompts, dict):
continue
# 遍历模板中的每个规则
for prompt_id, template_prompt in template_prompts.items():
if not isinstance(template_prompt, dict):
continue
# 检查用户配置中是否存在同名规则
if prompt_id in user_prompts:
# 用模板内容完整覆盖用户规则
user_prompts[prompt_id] = copy.deepcopy(template_prompt)
modified = True
self._log(f"[system_prompts] 覆盖规则: {template_prompt.get('name', prompt_id)}")
return modified
def _deep_merge_defaults(self, user_data, default_data):
"""
递归将 default_data 中的缺失字段合并到 user_data
合并策略:
- dict: 递归合并,补全缺失的键
- list: 将模板中不存在于用户列表的新元素追加到末尾
返回是否发生了修改
"""
modified = False
import copy
# ---处理字典类型---
if isinstance(user_data, dict) and isinstance(default_data, dict):
for key, value in default_data.items():
if key not in user_data:
# 字段不存在,直接添加
user_data[key] = copy.deepcopy(value)
modified = True
else:
# 字段存在,递归检查
if self._deep_merge_defaults(user_data[key], value):
modified = True
# ---处理数组类型---
elif isinstance(user_data, list) and isinstance(default_data, list):
# 将模板数组中不存在于用户数组的元素追加到末尾
for item in default_data:
if item not in user_data:
user_data.append(copy.deepcopy(item))
modified = True
return modified
def migrate_tags_json_to_csv(self):
"""
迁移旧版 JSON 标签到 CSV 格式
迁移逻辑:
1. 检查 tags 目录是否为空
2. 如果为空,读取插件 config 目录下的 tags.json 和 tags_user.json
3. tags.json → 转换为 "默认标签.csv"
4. tags_user.json → 转换为 "用户标签.csv"
CSV 格式: 标签名\t标签值\t一级分类\t二级分类\t三级分类\t四级分类
"""
try:
# 1. 检查是否需要迁移
if not self._should_migrate_tags():
return False
# 2. 读取 JSON 文件
tags_data, user_tags_data = self._load_legacy_tags_json()
migrated_count = 0
# ---处理 tags.json → 默认标签.csv---
if tags_data:
csv_rows = []
self._extract_tags_recursive(tags_data, [], csv_rows)
if csv_rows:
csv_filename = "默认标签.csv"
self._write_tags_csv(csv_rows, csv_filename)
self._log(f"[tags.json] ✅ 成功迁移 {len(csv_rows)} 个标签到 {csv_filename}")
migrated_count += len(csv_rows)
# ---处理 tags_user.json → 用户标签.csv---
if user_tags_data:
csv_rows = []
# tags_user.json 是2层结构: {分类: {标签名: 标签值}}
for category, tags in user_tags_data.items():
if not isinstance(tags, dict):
continue
for tag_name, tag_value in tags.items():
# CSV 行: [标签名, 标签值, 一级分类, 二级分类, 三级分类, 四级分类]
row = [
tag_name,
tag_value,
category,
"", # 二级分类(空)
"", # 三级分类(空)
"" # 四级分类(空)
]
csv_rows.append(row)
if csv_rows:
csv_filename = "用户标签.csv"
self._write_tags_csv(csv_rows, csv_filename)
self._log(f"[tags_user.json] ✅ 成功迁移 {len(csv_rows)} 个标签到 {csv_filename}")
migrated_count += len(csv_rows)
# ---如果两个文件都不存在,尝试从模板创建默认标签---
if not tags_data and not user_tags_data:
template_path = os.path.join(self.plugin_dir, "config", "tags_template.json")
if os.path.exists(template_path):
try:
with open(template_path, 'r', encoding='utf-8') as f:
template_data = json.load(f)
csv_rows = []
self._extract_tags_recursive(template_data, [], csv_rows)
if csv_rows:
csv_filename = "默认标签.csv"
self._write_tags_csv(csv_rows, csv_filename)
self._log(f"✨ 检测到全新环境,已基于模板创建初始标签文件: {csv_filename}")
return True
except Exception as e:
self._log(f"❗ 从模板创建初始标签失败: {str(e)}")
# 模板读取失败或不存在时的最后保底
csv_rows = [["欢迎使用提示词小助手", "", "指南", "开始", "", ""]]
csv_filename = "默认标签.csv"
self._write_tags_csv(csv_rows, csv_filename)
self._log(f"✨ 已创建简易初始标签文件: {csv_filename}")
return True
return migrated_count > 0
except Exception as e:
self._log(f"[tags] ❗ 标签迁移失败: {str(e)}")
return False
def _should_migrate_tags(self):
"""检查是否需要迁移标签"""
# 检查 tags 目录是否存在
if not os.path.exists(self.tags_dir):
return True
# 检查是否有 CSV 文件
try:
csv_files = [f for f in os.listdir(self.tags_dir) if f.endswith('.csv')]
return len(csv_files) == 0
except Exception:
return True
def _load_legacy_tags_json(self):
"""
加载旧版 JSON 标签文件
返回:
(tags_data, user_tags_data) 元组
"""
tags_data = None
user_tags_data = None
# 读取 tags.json
legacy_tags_path = os.path.join(self.legacy_config_dir, "tags.json")
if os.path.exists(legacy_tags_path):
try:
with open(legacy_tags_path, 'r', encoding='utf-8') as f:
tags_data = json.load(f)
except Exception as e:
self._log(f"❗ 读取 tags.json 失败: {str(e)}")
# 读取 tags_user.json
legacy_user_tags_path = os.path.join(self.legacy_config_dir, "tags_user.json")
if os.path.exists(legacy_user_tags_path):
try:
with open(legacy_user_tags_path, 'r', encoding='utf-8') as f:
user_tags_data = json.load(f)
except Exception as e:
self._log(f"❗ 读取 tags_user.json 失败: {str(e)}")
return tags_data, user_tags_data
def _extract_tags_recursive(self, data, categories, csv_rows):
"""
递归提取标签数据
根据嵌套深度判断是分类还是标签:
- 如果值是字符串,则为标签(键=标签名,值=标签值)
- 如果值是字典,则为分类,继续递归
参数:
data: 当前层级的数据字典
categories: 当前路径上的分类列表(最多4级)
csv_rows: 结果列表,用于收集CSV行
"""
for key, value in data.items():
if isinstance(value, str):
# 值是字符串,说明当前键是标签名,值是标签值
# 构建CSV行:[标签名, 标签值, 一级分类, 二级分类, 三级分类, 四级分类]
row = [key, value]
# 填充分类(共4级,不足的补空字符串)
for i in range(4):
if i < len(categories):
row.append(categories[i])
else:
row.append("")
csv_rows.append(row)
elif isinstance(value, dict):
# 值是字典,说明当前键是分类名,继续递归
# 限制最多4级分类,超过则忽略更深层级
if len(categories) < 4:
new_categories = categories + [key]
self._extract_tags_recursive(value, new_categories, csv_rows)
else:
# 超过4级分类,记录警告并跳过
self._log(f"⚠️ 分类层级超过4级,已忽略: {' → '.join(categories)} → {key}")
def _write_tags_csv(self, csv_rows, filename):
"""
写入 CSV 文件
参数:
csv_rows: CSV 行数据
filename: 文件名
"""
csv_path = os.path.join(self.tags_dir, filename)
# 确保目录存在
os.makedirs(self.tags_dir, exist_ok=True)
# 写入 CSV 文件(使用 utf-8-sig 编码,兼容 Excel)
with open(csv_path, 'w', newline='', encoding='utf-8-sig') as f:
writer = csv.writer(f)
# 写入表头
writer.writerow(['标签名', '标签值', '一级分类', '二级分类', '三级分类', '四级分类'])
# 写入数据
writer.writerows(csv_rows)
# --- Config.json 迁移 ---
def migrate_config_api_keys(self):
"""
迁移旧版 config.json 中的 API Key 到新版配置
迁移逻辑:
1. 检查用户配置目录的 config.json 是否存在
2. 如果不存在,读取插件 config 目录下的 config.json
3. 提取 API Key 并迁移到新版配置(v2.0 model_services 格式)
提取的 API Key:
- baidu_translate: app_id, secret_key
- llm.providers: zhipu, siliconflow, custom 的 api_key
- vlm.providers: zhipu, siliconflow, custom 的 api_key
"""
try:
# 1. 检查是否需要迁移
user_config_path = os.path.join(self.config_dir, "config.json")
if os.path.exists(user_config_path):
return False
# 2. 读取旧版 config.json
legacy_config_path = os.path.join(self.legacy_config_dir, "config.json")
if not os.path.exists(legacy_config_path):
return False
# 3. 加载旧版配置
with open(legacy_config_path, 'r', encoding='utf-8') as f:
legacy_config = json.load(f)
self._log(f"[config.json] 找到旧版配置,准备迁移到 v2.0 格式")
# 4. 将完整的旧版配置保存到临时文件,供 config_manager 转换
migration_data_path = os.path.join(self.config_dir, ".migration_legacy_config.json")
os.makedirs(self.config_dir, exist_ok=True)
with open(migration_data_path, 'w', encoding='utf-8') as f:
json.dump(legacy_config, f, ensure_ascii=False, indent=2)
return True
except Exception as e:
self._log(f"❗ config.json 迁移失败: {str(e)}")
return False
def _extract_api_keys_from_legacy_config(self, legacy_config):
"""
从旧版 config.json 提取 API Key
参数:
legacy_config: 旧版配置字典
返回:
提取的 API Key 字典
"""
api_keys = {}
# 提取百度翻译配置
if 'baidu_translate' in legacy_config:
baidu = legacy_config['baidu_translate']
if baidu.get('app_id') or baidu.get('secret_key'):
api_keys['baidu_translate'] = {
'app_id': baidu.get('app_id', ''),
'secret_key': baidu.get('secret_key', '')
}
self._log("提取百度翻译配置")
# 提取 LLM API Key
if 'llm' in legacy_config and 'providers' in legacy_config['llm']:
llm_providers = legacy_config['llm']['providers']
api_keys['llm'] = {}
for provider_name in ['zhipu', 'siliconflow', 'custom']:
if provider_name in llm_providers:
api_key = llm_providers[provider_name].get('api_key', '')
if api_key:
api_keys['llm'][provider_name] = api_key
self._log(f"提取 LLM {provider_name} API Key")
# 提取 VLM API Key
if 'vlm' in legacy_config and 'providers' in legacy_config['vlm']:
vlm_providers = legacy_config['vlm']['providers']
api_keys['vlm'] = {}
for provider_name in ['zhipu', 'siliconflow', 'custom']:
if provider_name in vlm_providers:
api_key = vlm_providers[provider_name].get('api_key', '')
if api_key:
api_keys['vlm'][provider_name] = api_key
self._log(f"提取 VLM {provider_name} API Key")
return api_keys
def run_migrations(plugin_dir, user_base_dir, logger=None, default_configs=None):
"""
运行所有迁移任务
执行顺序:
1. 确保所有配置文件存在(缺则创建/迁移)
2. 执行旧版 API Key 迁移
3. 执行增量更新(版本比对后按需执行)
参数:
plugin_dir: 插件目录路径
user_base_dir: 用户配置基础目录
logger: 日志函数(可选)
default_configs: 默认配置字典(用于创建默认文件和增量更新)
返回:
迁移结果字典
"""
tool = MigrationTool(plugin_dir, user_base_dir, logger)
legacy_dir = os.path.join(plugin_dir, "config")
results = {
'configs_created': False,
'tags_migration': False,
'config_migration': False,
'incremental_updates': {}
}
# 1. 确保所有配置文件存在
if default_configs:
tool.ensure_all_configs_exist(default_configs, legacy_dir)
results['configs_created'] = True
# 2. 执行旧版 API Key 迁移
results['config_migration'] = tool.migrate_config_api_keys()
results['tags_migration'] = tool.migrate_tags_json_to_csv()
# 3. 执行增量更新(版本比对后按需执行)
if default_configs:
results['incremental_updates'] = tool.migrate_incremental_updates(default_configs)
return results