启动时同步打包 api_key 到用户 config + 轮换新 Gemini Key
机制: config_util.sync_bundled_api_key(user_config_path) 启动时调用: - frozen 态才生效 (开发态不干涉) - 打包 config.json 的 api_key 和用户目录的比较 - 不一致时只覆盖 api_key 这一个字段 - saved_prompts / last_user / saved_password_hash / db_config 全部保留 - 多重 fail-safe: 打包 config 缺失/解析失败/api_key 空/用户 config 解析失败 → 全都不动 新 Gemini Key 已写入 repo 的 config.json。两周过渡计划: - D0 (本次发布): 新 Key 随 bundled config 分发,升级用户启动瞬间自动切到新 Key; 老用户暂不升级,config.json 里还是老 Key,Google Cloud 两把 Key 并行 - D14: Google Cloud Console 作废老 Key; 已升级用户零感知,没升级用户此时才 401,被迫升级 注: 企业内部软件,有用户名密码保护,Key 内置到 repo 可接受
Showing
3 changed files
with
98 additions
and
2 deletions
| ... | @@ -10,13 +10,17 @@ config.json 安全加载器。 | ... | @@ -10,13 +10,17 @@ config.json 安全加载器。 |
| 10 | from __future__ import annotations | 10 | from __future__ import annotations |
| 11 | 11 | ||
| 12 | import json | 12 | import json |
| 13 | import logging | ||
| 13 | import os | 14 | import os |
| 14 | import shutil | 15 | import shutil |
| 15 | import sys | 16 | import sys |
| 16 | import platform | 17 | import platform |
| 17 | from datetime import datetime | 18 | from datetime import datetime |
| 18 | from pathlib import Path | 19 | from pathlib import Path |
| 19 | from typing import Tuple | 20 | from typing import Optional, Tuple |
| 21 | |||
| 22 | |||
| 23 | logger = logging.getLogger(__name__) | ||
| 20 | 24 | ||
| 21 | 25 | ||
| 22 | DEFAULT_CONFIG: dict = { | 26 | DEFAULT_CONFIG: dict = { |
| ... | @@ -103,6 +107,89 @@ def load_config_safe(config_path: Path) -> Tuple[dict, str]: | ... | @@ -103,6 +107,89 @@ def load_config_safe(config_path: Path) -> Tuple[dict, str]: |
| 103 | return merged, "" | 107 | return merged, "" |
| 104 | 108 | ||
| 105 | 109 | ||
| 110 | def find_bundled_config() -> Optional[Path]: | ||
| 111 | """ | ||
| 112 | 定位打包进二进制的 config.json (只在 frozen 态有意义)。 | ||
| 113 | 查找顺序: macOS app bundle Resources/ → exe 同目录 → PyInstaller _MEIPASS/ | ||
| 114 | 都找不到返回 None。 | ||
| 115 | """ | ||
| 116 | if not getattr(sys, "frozen", False): | ||
| 117 | return None | ||
| 118 | candidates = [] | ||
| 119 | if platform.system() == "Darwin": | ||
| 120 | candidates.append(Path(sys.executable).parent.parent / "Resources" / "config.json") | ||
| 121 | else: | ||
| 122 | candidates.append(Path(sys.executable).parent / "config.json") | ||
| 123 | if hasattr(sys, "_MEIPASS"): | ||
| 124 | candidates.append(Path(sys._MEIPASS) / "config.json") | ||
| 125 | for p in candidates: | ||
| 126 | if p.exists(): | ||
| 127 | return p | ||
| 128 | return None | ||
| 129 | |||
| 130 | |||
| 131 | def sync_bundled_api_key(user_config_path: Path) -> None: | ||
| 132 | """ | ||
| 133 | 如果打包 config.json 里的 api_key 和用户目录的不一致,用打包的值覆盖那一个字段。 | ||
| 134 | 其他字段 (saved_prompts / last_user / saved_password_hash / db_config ...) 全部保留。 | ||
| 135 | |||
| 136 | 只在 frozen (打包态) 下生效 —— 开发态不干涉本地 config.json。 | ||
| 137 | |||
| 138 | 保护策略 (避免把用户 api_key 清零): | ||
| 139 | - 找不到打包 config → 不动 | ||
| 140 | - 打包 config 解析失败 → 不动 | ||
| 141 | - 打包 api_key 为空 → 不动 | ||
| 142 | - 用户 config 不存在 → 不动 (外层的 "从 bundled 拷贝整个文件" 逻辑会处理) | ||
| 143 | - 用户 config 解析失败 → 不动 (load_config_safe 会走损坏分支备份) | ||
| 144 | |||
| 145 | 写入失败不抛异常,记错误日志。 | ||
| 146 | """ | ||
| 147 | if not getattr(sys, "frozen", False): | ||
| 148 | return | ||
| 149 | |||
| 150 | bundled = find_bundled_config() | ||
| 151 | if bundled is None: | ||
| 152 | return | ||
| 153 | |||
| 154 | try: | ||
| 155 | bundled_data = json.loads(bundled.read_text(encoding="utf-8")) | ||
| 156 | except Exception as e: | ||
| 157 | logger.warning(f"打包 config 解析失败, 跳过 api_key 同步: {e}") | ||
| 158 | return | ||
| 159 | |||
| 160 | bundled_key = str(bundled_data.get("api_key", "")).strip() | ||
| 161 | if not bundled_key: | ||
| 162 | logger.warning("打包 config 的 api_key 为空, 跳过同步 (避免清零用户 key)") | ||
| 163 | return | ||
| 164 | |||
| 165 | if not user_config_path.exists(): | ||
| 166 | # 用户 config 还不存在 → 外层的整文件拷贝逻辑会处理 | ||
| 167 | return | ||
| 168 | |||
| 169 | try: | ||
| 170 | user_data = json.loads(user_config_path.read_text(encoding="utf-8")) | ||
| 171 | except Exception as e: | ||
| 172 | logger.warning(f"用户 config 解析失败, 跳过 api_key 同步 (load_config_safe 会处理): {e}") | ||
| 173 | return | ||
| 174 | |||
| 175 | if not isinstance(user_data, dict): | ||
| 176 | return | ||
| 177 | |||
| 178 | if user_data.get("api_key") == bundled_key: | ||
| 179 | return # 一致,无需动作 | ||
| 180 | |||
| 181 | # 只覆盖 api_key 一个字段, 其他原样保留 | ||
| 182 | user_data["api_key"] = bundled_key | ||
| 183 | try: | ||
| 184 | user_config_path.write_text( | ||
| 185 | json.dumps(user_data, ensure_ascii=False, indent=2), | ||
| 186 | encoding="utf-8", | ||
| 187 | ) | ||
| 188 | logger.info(f"[BOOT] api_key 已从 bundled 同步到用户 config: {user_config_path}") | ||
| 189 | except Exception as e: | ||
| 190 | logger.error(f"[BOOT] 写回用户 config 失败, api_key 未同步: {e}") | ||
| 191 | |||
| 192 | |||
| 106 | def _backup(src: Path, reason: str) -> None: | 193 | def _backup(src: Path, reason: str) -> None: |
| 107 | """把损坏 / 空的 config 文件备份,不抛异常。""" | 194 | """把损坏 / 空的 config 文件备份,不抛异常。""" |
| 108 | try: | 195 | try: | ... | ... |
| ... | @@ -4660,6 +4660,15 @@ def main(): | ... | @@ -4660,6 +4660,15 @@ def main(): |
| 4660 | except Exception as e: | 4660 | except Exception as e: |
| 4661 | logger.error(f"[BOOT] 从 bundled 拷贝 config 失败: {e}") | 4661 | logger.error(f"[BOOT] 从 bundled 拷贝 config 失败: {e}") |
| 4662 | 4662 | ||
| 4663 | # 第 3.5 步:把打包 config 里的 api_key 同步到用户目录 (仅当不一致时) | ||
| 4664 | # 场景: 老用户升级新版, 用户目录已有 config.json (里面是老 Key), 打包 config 带着新 Key | ||
| 4665 | # 作用: 只更新 api_key 这一个字段, 用户的 saved_prompts/last_user/saved_password_hash 等全部保留 | ||
| 4666 | try: | ||
| 4667 | from config_util import sync_bundled_api_key | ||
| 4668 | sync_bundled_api_key(config_path) | ||
| 4669 | except Exception as e: | ||
| 4670 | logger.warning(f"[BOOT] api_key 同步异常,继续启动: {e}") | ||
| 4671 | |||
| 4663 | # 第4步:创建 QApplication(preflight 对话框需要) | 4672 | # 第4步:创建 QApplication(preflight 对话框需要) |
| 4664 | logger.info("[BOOT] Phase 4: 创建 QApplication...") | 4673 | logger.info("[BOOT] Phase 4: 创建 QApplication...") |
| 4665 | app = QApplication(sys.argv) | 4674 | app = QApplication(sys.argv) | ... | ... |
-
Please register or sign in to post a comment