From 73ab8a2a9a1707a1ee463b1d62593bdf8b50d636 Mon Sep 17 00:00:00 2001 From: Estrella Pan Date: Thu, 8 Oct 2026 17:44:49 +0200 Subject: [PATCH] =?UTF-8?q?fix(4.0):=204.x=20=E5=8F=A6=E5=86=99=E7=89=88?= =?UTF-8?q?=E6=9C=AC=E8=AE=B0=E5=BD=95=E5=B9=B6=E5=A4=87=E4=BB=BD=203.x=20?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=BA=93=EF=BC=8C=E5=9B=9E=E9=80=80=203.3=20?= =?UTF-8?q?=E4=B8=8D=E5=86=8D=E8=AF=AF=E8=B7=91=203.0=20=E8=BF=81=E7=A7=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 1 + backend/src/module/conf/__init__.py | 5 ++- backend/src/module/update/version_check.py | 52 +++++++++++++++++++--- backend/src/test/test_version_check.py | 45 ++++++++++++++----- 4 files changed, 85 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b1e10b02..9161a788 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ - **修复(在线更新)**:在线更新包带上 `ab_sdk`,4.0 镜像启动时与 `module` 一起替换;更新包要求镜像不低于 4.0.0-beta.1,已安装 3.3 的用户在 beta 通道不会应用 4.x 更新包(缺少 `ab_sdk` 与新依赖,应用后无法启动),需要拉取 4.0 镜像 - **修复(多下载器)**:删除规则并删除文件时,若某个下载器不可用,规则与种子记录会保留并提示稍后重试;此前规则和种子记录已先删除,该下载器上的种子之后无法再删 - **修复(多下载器)**:aria2 任务与番剧的关联按下载器实例区分,两个 aria2 实例分配到相同 gid 时不再互相覆盖,同一种子也可以分别加入两个实例;插件下载器的工厂参数 `DownloaderConnection` 新增 `instance_id` +- **修复(回退 3.3)**:4.x 改把运行版本写入 `config/version_v4.info`,`config/version.info` 保留 3.x 的最后记录。此前 3.3 读到 `4.0.0` 只比较 minor,会当作 3.0 数据运行 3.0→3.1 迁移并重建数据库。首次以 4.x 启动 3.x 数据时还会把数据库备份为 `data/data.db.v3.bak`,回退时与 `config.json.v3.bak` 一起还原 - **新增(插件目录界面)**:设置 → 插件 新增「插件目录」:浏览签名目录中的插件并安装、更新;目录安装的插件可在卡片上卸载(内置、本地、pip 与 LLM 提供商插件不提供卸载)。插件卡片标出已签名或未签名。目录不可达时在区内显示原因,不弹全局提示 - **修复(插件设置)**:经签名目录安装的插件在插件卡片上的来源显示为未翻译的 `config.plugins_set.source_catalog` - **修复(插件安装)**:`DELETE /api/v1/plugins/{id}` 不再能卸载 LLM 提供商插件;安装通用插件后,LLM 插件列举不再反复输出 `Skipping broken plugin` 警告 diff --git a/backend/src/module/conf/__init__.py b/backend/src/module/conf/__init__.py index 79707c82..ddbb66a3 100644 --- a/backend/src/module/conf/__init__.py +++ b/backend/src/module/conf/__init__.py @@ -7,7 +7,10 @@ from .log import LOG_PATH, setup_logger TMDB_API = "32b19d6a05b512190a056fa4e747cbbc" DATA_PATH = "sqlite:///data/data.db" LEGACY_DATA_PATH = Path("data/data.json") -VERSION_PATH = Path("config/version.info") +# 3.x 只比较 version.info 末行的 minor,读到 4.x 会当成 3.0 并重建数据库, +# 所以 4.x 另写一份记录,version.info 保持 3.x 的最后状态,回退时仍安全。 +VERSION_PATH = Path("config/version_v4.info") +V3_VERSION_PATH = Path("config/version.info") POSTERS_PATH = Path("data/posters") PLATFORM = "Windows" if sys.platform == "win32" else "Unix" diff --git a/backend/src/module/update/version_check.py b/backend/src/module/update/version_check.py index 7eaaeee5..79182164 100644 --- a/backend/src/module/update/version_check.py +++ b/backend/src/module/update/version_check.py @@ -5,7 +5,13 @@ from pathlib import Path import semver -from module.conf import DATA_PATH, LEGACY_DATA_PATH, VERSION, VERSION_PATH +from module.conf import ( + DATA_PATH, + LEGACY_DATA_PATH, + V3_VERSION_PATH, + VERSION, + VERSION_PATH, +) from module.conf.config import CONFIG_PATH logger = logging.getLogger(__name__) @@ -67,6 +73,30 @@ def _read_last_version() -> semver.Version | None: return None +def _read_v3_version() -> semver.Version | None: + """3.x 的记录只读不写:回退到 3.3 时它仍要看到自己的版本。""" + try: + lines = V3_VERSION_PATH.read_text().splitlines() + return semver.Version.parse(lines[-1].strip()) + except (IndexError, ValueError) as e: + logger.warning(f"{V3_VERSION_PATH} is empty or malformed ({e}).") + return None + + +def _backup_v3_database() -> None: + """首次以 4.x 启动 3.x 数据时备份数据库,供回退 3.3 时还原;已有备份不覆盖。""" + backup = DATABASE_PATH.with_name(DATABASE_PATH.name + ".v3.bak") + if backup.exists(): + return + # 用 SQLite 备份接口而不是复制文件:WAL 中未合并的数据也要带上 + with ( + closing(sqlite3.connect(DATABASE_PATH)) as src, + closing(sqlite3.connect(backup)) as dst, + ): + src.backup(dst) + logger.info(f"Backed up the 3.x database to {backup}.") + + def version_check() -> semver.Version | None: """校验并记录版本,返回上一次运行的版本(未知时为 None)。 @@ -75,17 +105,25 @@ def version_check() -> semver.Version | None: """ if VERSION in ("DEV_VERSION", "local"): return None - if not VERSION_PATH.exists(): + if VERSION_PATH.exists(): + last_ver = _read_last_version() + if last_ver is None: + return None + elif V3_VERSION_PATH.exists(): + last_ver = _read_v3_version() + if last_ver is not None and (last_ver.major, last_ver.minor) < ( + MIN_UPGRADE_FROM + ): + raise UnsupportedUpgradeError(_upgrade_hint(str(last_ver))) + _backup_v3_database() + VERSION_PATH.write_text(VERSION + "\n") + return last_ver + else: # 3.3 也不补跑 3.0 → 3.1 的数据迁移(缺 version.info 时跳过),只有 3.1.x 会 if _is_v30_database(): raise UnsupportedUpgradeError(_upgrade_hint("3.0", via="3.1.x")) VERSION_PATH.write_text(VERSION + "\n") return None - last_ver = _read_last_version() - if last_ver is None: - return None - if (last_ver.major, last_ver.minor) < MIN_UPGRADE_FROM: - raise UnsupportedUpgradeError(_upgrade_hint(str(last_ver))) if semver.Version.parse(VERSION) > last_ver: with VERSION_PATH.open("a") as f: f.write(VERSION + "\n") diff --git a/backend/src/test/test_version_check.py b/backend/src/test/test_version_check.py index 4d592a9b..00f03166 100644 --- a/backend/src/test/test_version_check.py +++ b/backend/src/test/test_version_check.py @@ -18,11 +18,12 @@ def _make_db(path, *tables: str) -> None: @pytest.fixture def paths(tmp_path, monkeypatch): - version_path = tmp_path / "version.info" + version_path = tmp_path / "version_v4.info" legacy_path = tmp_path / "data.json" db_path = tmp_path / "data.db" _make_db(db_path, "bangumi", "rssitem") monkeypatch.setattr(vc, "VERSION_PATH", version_path) + monkeypatch.setattr(vc, "V3_VERSION_PATH", tmp_path / "version.info") monkeypatch.setattr(vc, "LEGACY_DATA_PATH", legacy_path) monkeypatch.setattr(vc, "DATABASE_PATH", db_path) monkeypatch.setattr(vc, "VERSION", "4.0.0") @@ -49,20 +50,42 @@ def test_missing_version_file_with_30_database_is_refused(paths, tmp_path, monke assert not version_path.exists() -def test_upgrade_from_33_is_recorded(paths): +def test_upgrade_from_33_keeps_v3_record_and_backs_up_database(paths, tmp_path): + """3.3 只比较 version.info 末行的 minor:写入 4.x 会被读成 3.0 并重建数据库。""" version_path, _ = paths - version_path.write_text("3.2.7\n3.3.6\n") + v3_path = tmp_path / "version.info" + v3_path.write_text("3.2.7\n3.3.6\n") last = vc.version_check() assert str(last) == "3.3.6" - assert version_path.read_text().splitlines()[-1] == "4.0.0" + assert v3_path.read_text() == "3.2.7\n3.3.6\n" + assert version_path.read_text() == "4.0.0\n" + with closing(sqlite3.connect(tmp_path / "data.db.v3.bak")) as conn: + tables = {r[0] for r in conn.execute("SELECT name FROM sqlite_master")} + assert tables == {"bangumi", "rssitem"} -def test_upgrade_from_33_prerelease_is_allowed(paths): - version_path, _ = paths - version_path.write_text("3.3.0-beta.2\n") +def test_upgrade_does_not_replace_existing_database_backup(paths, tmp_path): + (tmp_path / "version.info").write_text("3.3.6\n") + backup = tmp_path / "data.db.v3.bak" + backup.write_text("older") + vc.version_check() + assert backup.read_text() == "older" + + +def test_upgrade_from_33_prerelease_is_allowed(paths, tmp_path): + (tmp_path / "version.info").write_text("3.3.0-beta.2\n") assert str(vc.version_check()) == "3.3.0-beta.2" +def test_later_4x_boot_makes_no_database_backup(paths, tmp_path): + version_path, _ = paths + (tmp_path / "version.info").write_text("3.3.6\n") + version_path.write_text("4.0.0-beta.1\n") + assert str(vc.version_check()) == "4.0.0-beta.1" + assert version_path.read_text() == "4.0.0-beta.1\n4.0.0\n" + assert not (tmp_path / "data.db.v3.bak").exists() + + def test_same_version_is_not_duplicated(paths): version_path, _ = paths version_path.write_text("4.0.0\n") @@ -78,15 +101,17 @@ def test_downgrade_is_not_recorded(paths): @pytest.mark.parametrize("previous", ["3.2.7", "3.1.0", "2.6.4"]) -def test_upgrade_from_pre_33_is_refused(paths, previous): +def test_upgrade_from_pre_33_is_refused(paths, tmp_path, previous): version_path, _ = paths - version_path.write_text(previous + "\n") + v3_path = tmp_path / "version.info" + v3_path.write_text(previous + "\n") with pytest.raises(vc.UnsupportedUpgradeError, match=previous) as exc: vc.version_check() # 本次启动已把 config.json 改写为 4.0 格式,提示要先还原备份再回到旧版本 assert "config.json.v3.bak" in str(exc.value) # 拒绝时不改写记录,用户回退到 3.3.x 后仍能正常迁移 - assert version_path.read_text() == previous + "\n" + assert v3_path.read_text() == previous + "\n" + assert not version_path.exists() def test_legacy_data_json_is_refused(paths):