Files
cmbot/src/services/config_service.py
T

222 lines
8.6 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 json
import logging
logger = logging.getLogger(__name__)
DEFAULT_CONFIG = {
"output_format": "PNG", # PNG or JPG
"output_quality": 95, # JPG quality 1-95
"output_dir": "", # empty = use app output/ dir
"last_garment_dir": "",
"last_print_dir": "",
"last_template": "", # name of the last-selected template
"last_batch_mode": "full_combo", # BatchMode value of the last-used mode
"last_tab": 0, # last-selected workflow tab (0=添加印花, 1=AI 穿搭)
"update_source": "", # HTTP(S) manifest source (empty = no update check)
"update_user": "", # HTTP Basic Auth user (empty = anonymous)
"update_pass": "", # HTTP Basic Auth password (empty = anonymous)
# AI 穿搭(docs/11 §11):上次路径与批量设置并入 app_config(密钥不在此,见 ai_models.json)
"outfit_excel": "",
"outfit_output_dir": "",
"outfit_model": "", # last-selected model name
"outfit_concurrency": 1,
"outfit_request_interval": 2.0,
"outfit_task_cooldown": 1.0,
"outfit_retry_count": 2,
"outfit_resolution": "1K",
"outfit_quality": "均衡",
"outfit_retry_failed": False,
"outfit_prompt_name": "默认", # last-selected 话术模板 name (docs/11 §7.2)
}
_CONFIG_FILENAME = "app_config.json"
_AI_MODELS_FILENAME = "ai_models.json"
_OUTFIT_PROMPT_FILENAME = "outfit_prompt.txt" # legacy single prompt (migrated)
_OUTFIT_PROMPTS_FILENAME = "outfit_prompts.json" # multi named templates (§7.2)
DEFAULT_OUTFIT_PROMPT_NAME = "默认"
# Default outfit prompt (docs/11 §7). Seeded into outfit_prompts.json on first run.
DEFAULT_OUTFIT_PROMPT = (
"为商品「{title}」生成人物上身实穿效果图:真人模特正面穿着这件衣服,"
"完整保留款式、版型、颜色与印花图案,自然光、纯色棚拍背景,"
"电商主图风格,不加文字与促销标签。"
)
def load_config():
"""
从 JSON 文件加载应用配置。
返回默认配置与文件配置的合并字典。如果配置文件不存在、损坏或无法读取,
函数不会抛出异常,而是记录日志并返回默认配置。
Returns:
dict: 合并后的配置字典
"""
from services.file_service import get_config_path
config_file = get_config_path(_CONFIG_FILENAME)
if not config_file.exists():
logger.info("Config file not found, using defaults: %s", config_file)
return dict(DEFAULT_CONFIG)
try:
with open(str(config_file), encoding="utf-8") as f:
data = json.load(f)
if not isinstance(data, dict):
raise ValueError("Config root is not a JSON object")
merged = dict(DEFAULT_CONFIG)
merged.update(data)
logger.info("Config loaded from %s", config_file)
return merged
except (json.JSONDecodeError, ValueError) as exc:
logger.warning("Config file damaged (%s), using defaults: %s", exc, config_file)
return dict(DEFAULT_CONFIG)
except OSError as exc:
logger.warning("Config file unreadable (%s), using defaults: %s", exc, config_file)
return dict(DEFAULT_CONFIG)
def save_config(data):
"""Save config dict to JSON. Logs error on failure, does not raise."""
from services.file_service import get_config_path
config_file = get_config_path(_CONFIG_FILENAME)
try:
config_file.parent.mkdir(parents=True, exist_ok=True)
with open(str(config_file), "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
logger.info("Config saved to %s", config_file)
except OSError as exc:
logger.error("Failed to save config to %s: %s", config_file, exc)
def _seed_factory_config_if_missing(filename):
"""Copy a packaged config template to user config only when missing."""
from shutil import copy2
from services.file_service import get_app_dir, get_config_path
target = get_config_path(filename)
if target.exists():
return False
source = get_app_dir() / "config" / filename
if not source.exists():
return False
try:
target.parent.mkdir(parents=True, exist_ok=True)
copy2(str(source), str(target))
logger.info("Seeded factory config from %s to %s", source, target)
return True
except OSError as exc:
logger.warning(
"Failed to seed factory config from %s to %s: %s",
source,
target,
exc,
)
return False
def load_ai_models():
"""Load AI model configs from ai_models.json.
Returns a list of dicts (each with a ``name`` plus AiModelConfig fields).
Missing/corrupt file → empty list (the UI then prompts the admin to add one).
Keys live only in ~/.cmbot and are never committed (docs/11 §11).
"""
from services.file_service import get_config_path
_seed_factory_config_if_missing(_AI_MODELS_FILENAME)
models_file = get_config_path(_AI_MODELS_FILENAME)
if not models_file.exists():
logger.info("AI models file not found: %s", models_file)
return []
try:
with open(str(models_file), encoding="utf-8-sig") as f:
data = json.load(f)
except (json.JSONDecodeError, ValueError, OSError) as exc:
logger.warning("AI models file unreadable (%s): %s", exc, models_file)
return []
models = data.get("models") if isinstance(data, dict) else data
if not isinstance(models, list):
logger.warning("AI models file has no model list: %s", models_file)
return []
return [m for m in models if isinstance(m, dict)]
def load_outfit_prompt():
"""Return the saved outfit prompt template, or the built-in default."""
from services.file_service import get_config_path
prompt_file = get_config_path(_OUTFIT_PROMPT_FILENAME)
if not prompt_file.exists():
return DEFAULT_OUTFIT_PROMPT
try:
with open(str(prompt_file), encoding="utf-8-sig") as f:
return f.read()
except OSError as exc:
logger.warning("Outfit prompt unreadable (%s): %s", exc, prompt_file)
return DEFAULT_OUTFIT_PROMPT
def save_outfit_prompt(text):
"""Persist the outfit prompt template (utf-8, no BOM). Does not raise."""
from services.file_service import get_config_path
prompt_file = get_config_path(_OUTFIT_PROMPT_FILENAME)
try:
prompt_file.parent.mkdir(parents=True, exist_ok=True)
with open(str(prompt_file), "w", encoding="utf-8") as f:
f.write(text)
logger.info("Outfit prompt saved to %s", prompt_file)
except OSError as exc:
logger.error("Failed to save outfit prompt to %s: %s", prompt_file, exc)
def _normalize_prompts(data):
"""Keep only valid {name, text} entries (non-empty name, string text)."""
if not isinstance(data, list):
return []
out = []
for item in data:
if isinstance(item, dict):
name = str(item.get("name", "")).strip()
text = item.get("text", "")
if name and isinstance(text, str):
out.append({"name": name, "text": text})
return out
def load_outfit_prompts():
"""Load named 话术 templates (docs/11 §7.2); always returns >= 1.
Missing/corrupt → migrate the legacy outfit_prompt.txt into a single
「默认」template, or seed it from DEFAULT_OUTFIT_PROMPT.
"""
from services.file_service import get_config_path
prompts_file = get_config_path(_OUTFIT_PROMPTS_FILENAME)
if prompts_file.exists():
try:
with open(str(prompts_file), encoding="utf-8-sig") as f:
prompts = _normalize_prompts(json.load(f))
if prompts:
return prompts
except (json.JSONDecodeError, ValueError, OSError) as exc:
logger.warning("Outfit prompts unreadable (%s): %s", exc, prompts_file)
# Seed / migrate (load_outfit_prompt reads the legacy txt or the default).
return [{"name": DEFAULT_OUTFIT_PROMPT_NAME, "text": load_outfit_prompt()}]
def save_outfit_prompts(prompts):
"""Persist named 话术 templates as JSON (utf-8, no BOM). Does not raise."""
from services.file_service import get_config_path
prompts_file = get_config_path(_OUTFIT_PROMPTS_FILENAME)
try:
prompts_file.parent.mkdir(parents=True, exist_ok=True)
with open(str(prompts_file), "w", encoding="utf-8") as f:
json.dump(list(prompts), f, ensure_ascii=False, indent=2)
logger.info("Outfit prompts saved to %s", prompts_file)
except OSError as exc:
logger.error("Failed to save outfit prompts to %s: %s", prompts_file, exc)