Files
CC-Switch/src/main.tsx
T
SaladDay edeee25fae feat(db): in-app recovery screen with upgrade button when DB version is too new (#4575)
* feat(db): in-app recovery screen with upgrade button when DB version is too new

When the SQLite user_version is newer than the app supports (SCHEMA_VERSION),
Database::init() fails and previously dead-ended in a native Retry/Exit dialog
(Retry just fails again). The app now boots a dedicated recovery screen instead.

- Detect the recoverable "version too new" case and surface it via init_status
  (kind="db_version_too_new" + db_version/supported_version); force-show the main
  window and skip normal AppState boot (recovery commands need only AppHandle).
- The recovery screen first checks for an available update:
  - update available  -> "Upgrade app" runs the updater (download + install +
    restart) with a download progress bar.
  - no update (already latest) -> warns that the DB is too new even for the latest
    build (likely a third-party client), so upgrading cannot help.
- install_update_and_restart now emits `update-download-progress` events; new
  `check_app_update_available` command.
- i18n: en / ja / zh / zh-TW.

Verified: pnpm typecheck + format:check + test:unit (351) green; cross-model
review of Rust correctness and recovery-mode safety (no AppState panic).

* fix(db): pre-check too-new DB before schema writes; exit on close in recovery mode

Addresses review feedback on the too-new-DB recovery flow.

- P1: stored_user_version_exceeds_supported() is now checked BEFORE
  Database::init(), so create_tables()'s DDL (incl. the unconditional
  DROP INDEX/DROP TABLE IF EXISTS failover_queue) never runs against a
  database whose user_version we cannot understand. The earlier
  post-init-failure recovery branch is removed; the pre-check is the
  single authoritative guard, so "don't write to a DB we can't read"
  now holds from the very first DB access.

- P2: the window CloseRequested handler now detects recovery mode
  (init_error kind = db_version_too_new) and exits the app instead of
  honoring minimize_to_tray_on_close. Recovery mode returns before the
  tray is created, so hiding the window would leave the app running with
  no tray to bring it back; native-close now quits cleanly.

Verified: cargo fmt --check clean; cross-model Rust review of both fixes
(compile-correctness + no normal-startup/close regression).
2026-06-24 15:07:42 +08:00

119 lines
3.9 KiB
TypeScript

import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import { DatabaseUpgrade } from "./components/DatabaseUpgrade";
import { UpdateProvider } from "./contexts/UpdateContext";
import "./index.css";
// 导入国际化配置
import i18n from "./i18n";
import { QueryClientProvider } from "@tanstack/react-query";
import { ThemeProvider } from "@/components/theme-provider";
import { queryClient } from "@/lib/query";
import { Toaster } from "@/components/ui/sonner";
import { listen } from "@tauri-apps/api/event";
import { invoke } from "@tauri-apps/api/core";
import { message } from "@tauri-apps/plugin-dialog";
import { exit } from "@tauri-apps/plugin-process";
// 根据平台添加 body class,便于平台特定样式
try {
const ua = navigator.userAgent || "";
const plat = (navigator.platform || "").toLowerCase();
const isMac = /mac/i.test(ua) || plat.includes("mac");
if (isMac) {
document.body.classList.add("is-mac");
}
} catch {
// 忽略平台检测失败
}
// 配置加载错误payload类型
interface ConfigLoadErrorPayload {
path?: string;
error?: string;
/** "db_version_too_new" 表示数据库版本过新,渲染应用内升级恢复界面 */
kind?: string;
}
/**
* 处理配置加载失败:显示错误消息并强制退出应用
* 不给用户"取消"选项,因为配置损坏时应用无法正常运行
*/
async function handleConfigLoadError(
payload: ConfigLoadErrorPayload | null,
): Promise<void> {
const path = payload?.path ?? "~/.cc-switch/config.json";
const detail = payload?.error ?? "Unknown error";
await message(
i18n.t("errors.configLoadFailedMessage", {
path,
detail,
defaultValue:
"无法读取配置文件:\n{{path}}\n\n错误详情:\n{{detail}}\n\n请手动检查 JSON 是否有效,或从同目录的备份文件(如 config.json.bak)恢复。\n\n应用将退出以便您进行修复。",
}),
{
title: i18n.t("errors.configLoadFailedTitle", {
defaultValue: "配置加载失败",
}),
kind: "error",
},
);
await exit(1);
}
// 监听后端的配置加载错误事件:仅提醒用户并强制退出,不修改任何配置文件
try {
void listen("configLoadError", async (evt) => {
await handleConfigLoadError(evt.payload as ConfigLoadErrorPayload | null);
});
} catch (e) {
// 忽略事件订阅异常(例如在非 Tauri 环境下)
console.error("订阅 configLoadError 事件失败", e);
}
async function bootstrap() {
// 启动早期主动查询后端初始化错误,避免事件竞态
try {
const initError = (await invoke(
"get_init_error",
)) as ConfigLoadErrorPayload | null;
if (initError && initError.kind === "db_version_too_new") {
// 数据库版本过新:渲染应用内「升级应用」恢复界面,不进入正常 App
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<ThemeProvider defaultTheme="system" storageKey="cc-switch-theme">
<DatabaseUpgrade payload={initError} />
<Toaster />
</ThemeProvider>
</React.StrictMode>,
);
return;
}
if (initError && (initError.path || initError.error)) {
await handleConfigLoadError(initError);
// 注意:不会执行到这里,因为 exit(1) 会终止进程
return;
}
} catch (e) {
// 忽略拉取错误,继续渲染
console.error("拉取初始化错误失败", e);
}
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<ThemeProvider defaultTheme="system" storageKey="cc-switch-theme">
<UpdateProvider>
<App />
<Toaster />
</UpdateProvider>
</ThemeProvider>
</QueryClientProvider>
</React.StrictMode>,
);
}
void bootstrap();