fix(desktop): 迁移 skip 分支补捞 preferences.json

_migrate_legacy_data_dir 的 has_new_data skip 分支此前只捞 secrets.json
(0669204c), preferences.json 不捞 — 偏好丢失会让 onboarding_completed
回落默认 False, 升级后全屏引导重弹, 表现为「昨天同步过数据, 升级后
又被要求获取数据」(用户反馈链路)。函数泛化为 _rescue_legacy_user_data:
secrets.json + preferences.json 逐项捞回, 旧有新无才拷, 异常只告警。

验证: 新增 preferences 捞回/不覆盖 2 条用例; 原有 secrets 捞回 3 条
语义不变全绿。

Signed-off-by: shy3130 <shy3130@qq.com>
This commit is contained in:
shy3130
2026-10-29 18:39:40 +08:00
parent 3c0ad0179c
commit 5ff6e15995
2 changed files with 63 additions and 20 deletions
+25 -20
View File
@@ -107,30 +107,35 @@ def _last_available_rows(df: pl.DataFrame, cutoff: date) -> pl.DataFrame:
)
def _rescue_legacy_secrets(legacy_dir: Path, data_dir: Path) -> None:
"""整体迁移被跳过时, 单独捞回新目录缺失的 user_data/secrets.json。
def _rescue_legacy_user_data(legacy_dir: Path, data_dir: Path) -> None:
"""整体迁移被跳过时, 单独捞回新目录缺失的关键 user_data 文件。
场景: macOS ≤0.3.6 用户升级后先跑过新版 (新 data 已有行情), 再从废纸篓
把旧包数据恢复进新包 — has_new_data 守卫会跳过整目录搬迁, 但 secrets.json
(AI/数据源密钥) 是用户重新配置成本最高的文件, 旧有新无时应单独找回。
把旧包数据恢复进新包 — has_new_data 守卫会跳过整目录搬迁, 但两类文件
是用户重新配置成本最高的, 旧有新无时应单独找回:
- secrets.json (AI/数据源密钥)
- preferences.json (onboarding_completed/界面偏好 — 丢失会让
onboarding_completed 回落默认 False, 升级后全屏引导重弹, 表现为
「明明同步过数据, 升级后又被要求获取数据」)
任何异常只记警告, 绝不影响启动。
"""
import shutil
source = legacy_dir / "user_data" / "secrets.json"
dest = data_dir / "user_data" / "secrets.json"
if not source.is_file() or dest.exists():
return
try:
data = json.loads(source.read_text(encoding="utf-8"))
if not isinstance(data, dict) or not data:
return
dest.parent.mkdir(parents=True, exist_ok=True)
# copy2 保留源文件权限位 (secrets.json 为 0600)
shutil.copy2(source, dest)
logger.info("rescued legacy secrets.json %s -> %s", source, dest)
except (OSError, ValueError) as e:
logger.warning("legacy secrets.json rescue skipped: %s", e)
for name, note in (("secrets.json", "密钥"), ("preferences.json", "偏好")):
source = legacy_dir / "user_data" / name
dest = data_dir / "user_data" / name
if not source.is_file() or dest.exists():
continue
try:
data = json.loads(source.read_text(encoding="utf-8"))
if not isinstance(data, dict) or not data:
continue
dest.parent.mkdir(parents=True, exist_ok=True)
# copy2 保留源文件权限位 (secrets.json 为 0600)
shutil.copy2(source, dest)
logger.info("rescued legacy %s (%s) %s -> %s", name, note, source, dest)
except (OSError, ValueError) as e:
logger.warning("legacy %s rescue skipped: %s", name, e)
class DataStore:
@@ -221,8 +226,8 @@ class DataStore:
self.data_dir.rglob("*.jsonl")
)
if has_new_data:
# 整体搬迁会被守卫跳过, 但密钥文件单独捞回 (见函数 docstring)
_rescue_legacy_secrets(legacy_dir, self.data_dir)
# 整体搬迁会被守卫跳过, 但密钥/偏好等关键 user_data 文件单独捞回 (见函数 docstring)
_rescue_legacy_user_data(legacy_dir, self.data_dir)
logger.info(
"legacy data dir %s exists but new %s already has data, skip migration",
legacy_dir, self.data_dir,
+38
View File
@@ -205,3 +205,41 @@ def test_migration_rescue_skips_invalid_secrets(tmp_path, monkeypatch):
_repo_with_data_dir(new_dir)._migrate_legacy_data_dir()
assert not (new_dir / "user_data" / "secrets.json").exists()
def test_migration_rescues_preferences_when_new_has_data(tmp_path, monkeypatch):
"""skip 分支也要捞回 preferences.json: 偏好丢失会让 onboarding_completed
回落默认 False, 升级后全屏引导重弹 (「同步过数据却被要求获取数据」)。"""
new_dir, bundle_data = _setup_skip_migration(tmp_path)
(bundle_data / "user_data" / "preferences.json").write_text(
'{"onboarding_completed": true, "nav_hidden": ["a"]}',
encoding="utf-8",
)
monkeypatch.setattr(sys, "frozen", True, raising=False)
monkeypatch.setattr(sys, "platform", "darwin")
monkeypatch.setattr(sys, "executable", str(bundle_data.parent / "TSP"))
_repo_with_data_dir(new_dir)._migrate_legacy_data_dir()
rescued = new_dir / "user_data" / "preferences.json"
assert rescued.exists()
prefs = json.loads(rescued.read_text(encoding="utf-8"))
assert prefs["onboarding_completed"] is True
# 守卫语义不变: 旧目录整体保留
assert (bundle_data / "user_data" / "preferences.json").exists()
def test_migration_no_rescue_when_dest_preferences_exists(tmp_path, monkeypatch):
"""新目录已有 preferences.json (新版已在使用) → 绝不覆盖。"""
new_dir, bundle_data = _setup_skip_migration(tmp_path)
(bundle_data / "user_data" / "preferences.json").write_text('{"legacy": true}', encoding="utf-8")
(new_dir / "user_data" / "preferences.json").write_text('{"mine": true}', encoding="utf-8")
monkeypatch.setattr(sys, "frozen", True, raising=False)
monkeypatch.setattr(sys, "platform", "darwin")
monkeypatch.setattr(sys, "executable", str(bundle_data.parent / "TSP"))
_repo_with_data_dir(new_dir)._migrate_legacy_data_dir()
assert (new_dir / "user_data" / "preferences.json").read_text(encoding="utf-8") == '{"mine": true}'