🔧 feat(desktop): add legacy local database detection and migration guidance (#11682)

* 🔧 feat(desktop): add legacy local database detection and migration guidance

- Add hasLegacyLocalDb method to SystemController for detecting legacy DB
- Update LoginStep to show migration link for users with legacy DB
- Add i18n translations for legacy database migration feature
- Improve common settings data sync configuration

* 🔧 test: mock getAppPath in electron for improved testing

- Add mock implementation of getAppPath in SystemCtr and macOS test files
- Update LoginStep to use urlJoin for constructing migration guide URL

Signed-off-by: Innei <tukon479@gmail.com>

---------

Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
Innei
2026-01-21 22:28:34 +08:00
committed by GitHub
parent 404c5776be
commit 5664b84ba8
11 changed files with 109 additions and 20 deletions

View File

@@ -23,6 +23,9 @@ export const userDataDir = app.getPath('userData');
export const appStorageDir = join(userDataDir, 'lobehub-storage');
// Legacy local database directory used in older desktop versions
export const legacyLocalDbDir = join(appStorageDir, 'lobehub-local-db');
// ------ Application storage directory ---- //
// Local storage files (simulating S3)

View File

@@ -1,8 +1,10 @@
import { ElectronAppState, ThemeMode } from '@lobechat/electron-client-ipc';
import { app, dialog, nativeTheme, shell } from 'electron';
import { macOS } from 'electron-is';
import { pathExists, readdir } from 'fs-extra';
import process from 'node:process';
import { legacyLocalDbDir } from '@/const/dir';
import { createLogger } from '@/utils/logger';
import {
getAccessibilityStatus,
@@ -214,6 +216,23 @@ export default class SystemController extends ControllerModule {
return nativeTheme.themeSource;
}
/**
* Detect whether user used the legacy local database in older desktop versions.
* Legacy path: {app.getPath('userData')}/lobehub-storage/lobehub-local-db
*/
@IpcMethod()
async hasLegacyLocalDb(): Promise<boolean> {
if (!(await pathExists(legacyLocalDbDir))) return false;
try {
const entries = await readdir(legacyLocalDbDir);
return entries.length > 0;
} catch {
// If directory exists but cannot be read, treat as "used" to surface guidance.
return true;
}
}
private async setSystemThemeMode(themeMode: ThemeMode) {
nativeTheme.themeSource = themeMode;
}

View File

@@ -56,6 +56,7 @@ vi.mock('@/utils/logger', () => ({
// Mock electron
vi.mock('electron', () => ({
app: {
getAppPath: vi.fn(() => '/mock/app/path'),
getLocale: vi.fn(() => 'en-US'),
getPath: vi.fn((name: string) => `/mock/path/${name}`),
},

View File

@@ -13,6 +13,7 @@ vi.mock('electron', () => ({
setApplicationMenu: vi.fn(),
},
app: {
getAppPath: vi.fn(() => '/mock/app/path'),
getName: vi.fn(() => 'LobeChat'),
getPath: vi.fn((type: string) => {
if (type === 'logs') return '/path/to/logs';

View File

@@ -73,6 +73,7 @@
"screen5.badge": "Sign in",
"screen5.description": "Sign in to sync Agents, Groups, settings, and Context across all devices.",
"screen5.errors.desktopOnlyOidc": "OIDC authorization is only available in the desktop app runtime.",
"screen5.legacyLocalDb.link": "Migrate legacy local database",
"screen5.methods.cloud.description": "Sign in with your LobeHub Cloud account to sync everything seamlessly",
"screen5.methods.cloud.name": "LobeHub Cloud",
"screen5.methods.selfhost.description": "Connect to your own LobeHub server instance",

View File

@@ -73,6 +73,7 @@
"screen5.badge": "登录",
"screen5.description": "登录以在所有设备间同步代理、群组、设置和上下文。",
"screen5.errors.desktopOnlyOidc": "OIDC 授权仅在桌面端运行时可用。",
"screen5.legacyLocalDb.link": "迁移旧版本地数据库",
"screen5.methods.cloud.description": "使用您的 LobeHub 云账户登录,实现无缝同步",
"screen5.methods.cloud.name": "LobeHub Cloud",
"screen5.methods.selfhost.description": "连接到你自己的 LobeHub 服务实例",

View File

@@ -35,12 +35,12 @@
"prebuild": "tsx scripts/prebuild.mts && npm run lint",
"build": "cross-env NODE_OPTIONS=--max-old-space-size=8192 next build --webpack",
"postbuild": "npm run build-sitemap && npm run build-migrate-db",
"build-migrate-db": "bun run db:migrate",
"build-sitemap": "tsx ./scripts/buildSitemapIndex/index.ts",
"build:analyze": "NODE_OPTIONS=--max-old-space-size=81920 ANALYZE=true next build --webpack",
"build:docker": "npm run prebuild && NODE_OPTIONS=--max-old-space-size=8192 DOCKER=true next build --webpack && npm run build-sitemap",
"build:electron": "cross-env NODE_OPTIONS=--max-old-space-size=8192 NEXT_PUBLIC_IS_DESKTOP_APP=1 tsx scripts/electronWorkflow/buildNextApp.mts",
"build:vercel": "npm run prebuild && cross-env NODE_OPTIONS=--max-old-space-size=6144 next build --webpack && npm run postbuild",
"build-migrate-db": "bun run db:migrate",
"build-sitemap": "tsx ./scripts/buildSitemapIndex/index.ts",
"clean:node_modules": "bash -lc 'set -e; echo \"Removing all node_modules...\"; rm -rf node_modules; pnpm -r exec rm -rf node_modules; rm -rf apps/desktop/node_modules; echo \"All node_modules removed.\"'",
"db:generate": "drizzle-kit generate && npm run workflow:dbml",
"db:migrate": "MIGRATION_DB=1 tsx ./scripts/migrateServerDB/index.ts",
@@ -87,11 +87,11 @@
"start": "next start -p 3210",
"stylelint": "stylelint \"src/**/*.{js,jsx,ts,tsx}\" --fix",
"test": "npm run test-app && npm run test-server",
"test-app": "vitest run",
"test-app:coverage": "vitest --coverage --silent='passed-only'",
"test:e2e": "pnpm --filter @lobechat/e2e-tests test",
"test:e2e:smoke": "pnpm --filter @lobechat/e2e-tests test:smoke",
"test:update": "vitest -u",
"test-app": "vitest run",
"test-app:coverage": "vitest --coverage --silent='passed-only'",
"tunnel:cloudflare": "cloudflared tunnel --url http://localhost:3010",
"tunnel:ngrok": "ngrok http http://localhost:3011",
"type-check": "tsgo --noEmit",
@@ -207,7 +207,7 @@
"@lobehub/icons": "^4.0.2",
"@lobehub/market-sdk": "0.29.1",
"@lobehub/tts": "^4.0.2",
"@lobehub/ui": "^4.24.0",
"@lobehub/ui": "^4.25.0",
"@modelcontextprotocol/sdk": "^1.25.1",
"@neondatabase/serverless": "^1.0.2",
"@next/third-parties": "^16.1.1",

View File

@@ -11,15 +11,23 @@ import { cssVar } from 'antd-style';
import { Cloud, Server, Undo2Icon } from 'lucide-react';
import { memo, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import urlJoin from 'url-join';
import { OFFICIAL_SITE } from '@/const/url';
import { isDesktop } from '@/const/version';
import UserInfo from '@/features/User/UserInfo';
import { remoteServerService } from '@/services/electron/remoteServer';
import { electronSystemService } from '@/services/electron/system';
import { useElectronStore } from '@/store/electron';
import { setDesktopAutoOidcFirstOpenHandled } from '@/utils/electron/autoOidc';
import LobeMessage from '../components/LobeMessage';
const LEGACY_LOCAL_DB_MIGRATION_GUIDE_URL = urlJoin(
OFFICIAL_SITE,
'/docs/usage/migrate-from-local-database',
);
// 登录方式类型
type LoginMethod = 'cloud' | 'selfhost';
@@ -62,6 +70,7 @@ const LoginStep = memo<LoginStepProps>(({ onBack, onNext }) => {
const [remoteError, setRemoteError] = useState<string | null>(null);
const [isSigningOut, setIsSigningOut] = useState(false);
const [showEndpoint, setShowEndpoint] = useState(false);
const [hasLegacyLocalDb, setHasLegacyLocalDb] = useState(false);
const [
dataSyncConfig,
@@ -83,9 +92,24 @@ const LoginStep = memo<LoginStepProps>(({ onBack, onNext }) => {
s.disconnectRemoteServer,
]);
// Ensure remote server config is loaded early (desktop only hook)
useDataSyncConfig();
useEffect(() => {
if (!isDesktop) return;
let mounted = true;
electronSystemService
.hasLegacyLocalDb()
.then((value) => {
if (mounted) setHasLegacyLocalDb(value);
})
.catch(() => undefined);
return () => {
mounted = false;
};
}, []);
const isCloudAuthed = !!dataSyncConfig?.active && dataSyncConfig.storageMode === 'cloud';
const isSelfHostAuthed = !!dataSyncConfig?.active && dataSyncConfig.storageMode === 'selfHost';
const isSelfHostEndpointVerified =
@@ -441,6 +465,19 @@ const LoginStep = memo<LoginStepProps>(({ onBack, onNext }) => {
<Flexbox align={'flex-start'} gap={16} style={{ width: '100%' }} width={'100%'}>
{renderCloudContent()}
<Flexbox horizontal justify={'center'} style={{ width: '100%' }}>
{hasLegacyLocalDb && (
<Button
onClick={() =>
electronSystemService.openExternalLink(LEGACY_LOCAL_DB_MIGRATION_GUIDE_URL)
}
style={{ padding: 0 }}
type={'link'}
>
{t('screen5.legacyLocalDb.link', 'Migrate legacy local database')}
</Button>
)}
</Flexbox>
{!showEndpoint ? (
<Center width={'100%'}>
<Button
@@ -460,6 +497,7 @@ const LoginStep = memo<LoginStepProps>(({ onBack, onNext }) => {
OR
</Text>
</Divider>
{/* Self-host 选项 */}
{renderSelfhostContent()}
</>

View File

@@ -1,7 +1,14 @@
'use client';
import { Form, type FormGroupItemType, Icon, ImageSelect } from '@lobehub/ui';
import { Select, Skeleton } from '@lobehub/ui';
import {
Flexbox,
Form,
type FormGroupItemType,
Icon,
ImageSelect,
LobeSelect as Select,
Skeleton,
} from '@lobehub/ui';
import { Segmented, Switch } from 'antd';
import isEqual from 'fast-deep-equal';
import { Ban, Gauge, Loader2Icon, Monitor, Moon, Mouse, Sun, Waves } from 'lucide-react';
@@ -76,11 +83,19 @@ const Common = memo(() => {
},
{
children: (
<Select
defaultValue={language}
onChange={handleLangChange}
options={[{ label: t('settingCommon.lang.autoMode'), value: 'auto' }, ...localeOptions]}
/>
<Flexbox horizontal justify={'flex-end'}>
<Select
defaultValue={language}
onChange={handleLangChange}
options={[
{ label: t('settingCommon.lang.autoMode'), value: 'auto' },
...localeOptions,
]}
style={{
width: '50%',
}}
/>
</Flexbox>
),
label: t('settingCommon.lang.title'),
},
@@ -136,13 +151,18 @@ const Common = memo(() => {
{
children: (
<Select
options={[
{ label: t('settingCommon.responseLanguage.auto'), value: '' },
...localeOptions,
]}
placeholder={t('settingCommon.responseLanguage.placeholder')}
/>
<Flexbox horizontal justify={'flex-end'}>
<Select
options={[
{ label: t('settingCommon.responseLanguage.auto'), value: '' },
...localeOptions,
]}
placeholder={t('settingCommon.responseLanguage.placeholder')}
style={{
width: '50%',
}}
/>
</Flexbox>
),
desc: t('settingCommon.responseLanguage.desc'),
label: t('settingCommon.responseLanguage.title'),

View File

@@ -90,6 +90,7 @@ export default {
'Sign in to sync Agents, Groups, settings, and Context across all devices.',
'screen5.errors.desktopOnlyOidc':
'OIDC authorization is only available in the desktop app runtime.',
'screen5.legacyLocalDb.link': 'Migrate legacy local database',
'screen5.methods.cloud.description':
'Sign in with your LobeHub Cloud account to sync everything seamlessly',
'screen5.methods.cloud.name': 'LobeHub Cloud',

View File

@@ -48,6 +48,10 @@ class ElectronSystemService {
return this.ipc.system.openExternalLink(url);
}
async hasLegacyLocalDb(): Promise<boolean> {
return this.ipc.system.hasLegacyLocalDb();
}
showContextMenu = async (type: string, data?: any) => {
return this.ipc.menu.showContextMenu({ data, type });
};