config_util.py
3.42 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
"""
config.json 安全加载器。
公开接口:
- DEFAULT_CONFIG: 默认配置常量
- load_config_safe(path): 区分处理各类 IO / JSON 错误,返回 (config, error_message)
- get_config_dir(): 跨平台返回配置目录
- get_config_path(): 返回 config.json 绝对路径
"""
from __future__ import annotations
import json
import os
import shutil
import sys
import platform
from datetime import datetime
from pathlib import Path
from typing import Tuple
DEFAULT_CONFIG: dict = {
"api_key": "",
"saved_prompts": [],
"db_config": None,
"last_user": "",
"saved_password_hash": "",
"logging_config": {
"enabled": True,
"level": "INFO",
"log_to_console": True,
},
"history_config": {
"max_history_count": 100,
},
}
def get_config_dir() -> Path:
"""跨平台返回用户级配置目录(与 image_generator.py 中 get_config_dir 一致)。"""
if getattr(sys, "frozen", False):
system = platform.system()
if system == "Darwin":
d = Path.home() / "Library" / "Application Support" / "ZB100ImageGenerator"
elif system == "Windows":
d = Path(os.getenv("APPDATA", str(Path.home()))) / "ZB100ImageGenerator"
else:
d = Path.home() / ".config" / "zb100imagegenerator"
else:
d = Path(".").resolve()
d.mkdir(parents=True, exist_ok=True)
return d
def get_config_path() -> Path:
return get_config_dir() / "config.json"
def load_config_safe(config_path: Path) -> Tuple[dict, str]:
"""
安全加载 config.json。
返回 (config, error):
- 成功:(合并后的 config dict, "")
- 失败但可恢复:(DEFAULT_CONFIG 副本, 错误描述);preflight 会根据返回
的 error 和 config 内容共同决定是否拦截启动
- 不抛异常
行为:
- 文件不存在 → (DEFAULT_CONFIG 副本, "") # 让 preflight 判断是否允许
- 空文件 / JSON 损坏 → 备份为 .bak.<timestamp> + 返回默认值 + error
- PermissionError / OSError → 返回默认值 + error(不备份,读都读不到)
- 顶层不是 object → 返回默认值 + error
- 正常 → defaults.update(loaded) 合并后返回(保留未知字段以兼容)
"""
config_path = Path(config_path)
if not config_path.exists():
return dict(DEFAULT_CONFIG), ""
try:
content = config_path.read_text(encoding="utf-8")
except PermissionError as e:
return dict(DEFAULT_CONFIG), f"permission denied: {e}"
except OSError as e:
return dict(DEFAULT_CONFIG), f"IO error: {e}"
if not content.strip():
_backup(config_path, reason="empty")
return dict(DEFAULT_CONFIG), "config.json was empty, using defaults"
try:
loaded = json.loads(content)
except json.JSONDecodeError as e:
_backup(config_path, reason="parse-error")
return dict(DEFAULT_CONFIG), f"JSON parse error, backed up to .bak: {e}"
if not isinstance(loaded, dict):
return dict(DEFAULT_CONFIG), "config.json top-level is not an object"
merged = dict(DEFAULT_CONFIG)
merged.update(loaded)
return merged, ""
def _backup(src: Path, reason: str) -> None:
"""把损坏 / 空的 config 文件备份,不抛异常。"""
try:
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
dst = src.with_suffix(f".json.bak.{reason}.{ts}")
shutil.copy2(src, dst)
except Exception:
pass