diff --git a/apps/cli/src/http/routes/nlp.ts b/apps/cli/src/http/routes/nlp.ts index f8637a24..9ec1f32d 100644 --- a/apps/cli/src/http/routes/nlp.ts +++ b/apps/cli/src/http/routes/nlp.ts @@ -35,7 +35,7 @@ function ensureDb(dbManager: DatabaseManager, sessionId: string) { export function registerNlpRoutes(server: FastifyInstance, dbManager: DatabaseManager): void { const pathProvider = (dbManager as any)['pathProvider'] if (pathProvider) { - const nlpDir = path.join(pathProvider.getUserDataDir(), 'nlp') + const nlpDir = path.join(pathProvider.getSystemDir(), 'nlp') initNlpDir(nlpDir) ensureDefaultDict(nlpDir).catch((err) => console.warn('[NLP] Auto-download zh-CN dict failed:', err)) } @@ -81,5 +81,5 @@ export function registerNlpRoutes(server: FastifyInstance, dbManager: DatabaseMa function resolveNlpDir(dbManager: DatabaseManager): string { const pathProvider = (dbManager as any)['pathProvider'] - return path.join(pathProvider.getUserDataDir(), 'nlp') + return path.join(pathProvider.getSystemDir(), 'nlp') } diff --git a/apps/desktop/main/ipc/cache.ts b/apps/desktop/main/ipc/cache.ts index e88c55a5..d39fa58e 100644 --- a/apps/desktop/main/ipc/cache.ts +++ b/apps/desktop/main/ipc/cache.ts @@ -6,6 +6,7 @@ import * as path from 'path' import type { IpcContext } from './types' import { getUserDataDir, + getDefaultUserDataDir, getSystemDataDir, getDatabaseDir, getCacheDir, @@ -143,6 +144,7 @@ export function registerCacheHandlers(_context: IpcContext): void { ipcMain.handle('cache:getDataDir', async () => { return { path: getUserDataDir(), + defaultPath: getDefaultUserDataDir(), isCustom: Boolean(getCustomDataDir()), } }) diff --git a/apps/desktop/main/nlp/dictManager.ts b/apps/desktop/main/nlp/dictManager.ts index 09d8f9e0..c9276b89 100644 --- a/apps/desktop/main/nlp/dictManager.ts +++ b/apps/desktop/main/nlp/dictManager.ts @@ -1,12 +1,11 @@ /** * Electron 词库管理器 * - * 封装 Electron 特定的 nlpDir 路径解析(使用 app.getPath), + * 封装 Electron 特定的 nlpDir 路径解析, * 实际词库操作委托给 @openchatlab/node-runtime 的共享实现。 */ import * as path from 'path' -import { app } from 'electron' import { isDictDownloaded as _isDictDownloaded, getDictList as _getDictList, @@ -16,14 +15,12 @@ import { ensureDefaultDict as _ensureDefaultDict, } from '@openchatlab/node-runtime' import type { DictInfo } from '@openchatlab/core' +import { getSystemDataDir } from '../paths' export type { DictInfo } -const NLP_DIR_NAME = 'nlp' - export function getNlpDir(): string { - const userDataPath = app.getPath('userData') - return path.join(userDataPath, 'data', NLP_DIR_NAME) + return path.join(getSystemDataDir(), 'nlp') } export function isDictDownloaded(dictId: string): boolean { diff --git a/apps/desktop/main/paths.ts b/apps/desktop/main/paths.ts index 04b68762..73132af7 100644 --- a/apps/desktop/main/paths.ts +++ b/apps/desktop/main/paths.ts @@ -90,7 +90,7 @@ export function getUserDataDir(): string { return _userDataDir } -function getDefaultUserDataDir(): string { +export function getDefaultUserDataDir(): string { return path.join(os.homedir(), '.chatlab', 'data') } @@ -180,6 +180,7 @@ export function setCustomDataDir( } writeConfigField('data', 'user_data_dir', newDir) + writeConfigField('data', 'electron_migration_done', true) _userDataDir = newDir let canDeleteOldDir = false @@ -264,6 +265,7 @@ export function setCustomDataDir( } writeConfigField('data', 'user_data_dir', normalized) + writeConfigField('data', 'electron_migration_done', true) if (canDeleteOldDir) { writeStorageConfig({ pendingDeleteDir: oldDir }) @@ -582,6 +584,9 @@ const SYSTEM_SUBDIRS = ['ai', 'settings', 'cache', 'logs', 'temp', 'nlp'] * Electron 启动并写入了默认值 ~/.chatlab/data,导致迁移被跳过。 */ export function needsUnifiedDirMigration(): boolean { + const config = loadConfig() + if (config.data.electron_migration_done) return false + const oldDataDir = resolveOldElectronDataDir() const oldDbDir = path.join(oldDataDir, 'databases') if (!fs.existsSync(oldDbDir)) return false @@ -589,7 +594,6 @@ export function needsUnifiedDirMigration(): boolean { const hasDb = fs.readdirSync(oldDbDir).some((f) => f.endsWith('.db')) if (!hasDb) return false - const config = loadConfig() const currentUserDataDir = config.data.user_data_dir || getDefaultUserDataDir() if (path.resolve(currentUserDataDir) === path.resolve(oldDataDir)) return false @@ -708,6 +712,8 @@ export function migrateToUnifiedDirs(): { success: boolean; error?: string } { writeMigrationLog(getLogsDir(), summary, ensureDir) console.log(`[Migration] ${summary}`) + writeConfigField('data', 'electron_migration_done', true) + return { success: failedDirs.length === 0 } } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error) diff --git a/packages/config/src/loader.ts b/packages/config/src/loader.ts index 7951b557..c63b8348 100644 --- a/packages/config/src/loader.ts +++ b/packages/config/src/loader.ts @@ -111,7 +111,7 @@ function loadEnvConfig(): Record { * 如果文件不存在则创建,如果已存在则保留其他内容,只更新指定字段。 * 采用简单的 TOML 序列化:仅支持一级 section + 字符串/数字值。 */ -export function writeConfigField(section: string, key: string, value: string | number): void { +export function writeConfigField(section: string, key: string, value: string | number | boolean): void { if (!fs.existsSync(CONFIG_DIR)) { fs.mkdirSync(CONFIG_DIR, { recursive: true }) } diff --git a/packages/config/src/schema.ts b/packages/config/src/schema.ts index 98dc6b8f..9948a8bb 100644 --- a/packages/config/src/schema.ts +++ b/packages/config/src/schema.ts @@ -16,6 +16,7 @@ export const llmConfigSchema = z.object({ export const dataConfigSchema = z.object({ user_data_dir: z.string().default(''), + electron_migration_done: z.boolean().default(false), }) export const apiConfigSchema = z.object({ diff --git a/src/components/common/Settings/DataStorage/StorageManageSection.vue b/src/components/common/Settings/DataStorage/StorageManageSection.vue index af89be20..eb56ec9f 100644 --- a/src/components/common/Settings/DataStorage/StorageManageSection.vue +++ b/src/components/common/Settings/DataStorage/StorageManageSection.vue @@ -35,6 +35,7 @@ const cacheInfo = ref(null) const isLoading = ref(false) const clearingId = ref(null) const dataDir = ref('') +const defaultDataDir = ref('') const isCustomDataDir = ref(false) const isUpdatingDataDir = ref(false) const dataDirError = ref(null) @@ -75,10 +76,15 @@ async function loadCacheInfo() { } // 加载数据目录 +const canMigrateToDefault = computed(() => { + return IS_ELECTRON && dataDir.value && defaultDataDir.value && dataDir.value !== defaultDataDir.value +}) + async function loadDataDir() { try { const info = await window.cacheApi.getDataDir() dataDir.value = info.path + defaultDataDir.value = info.defaultPath || '' isCustomDataDir.value = info.isCustom } catch (error) { console.error('获取数据目录失败:', error) @@ -138,10 +144,15 @@ async function selectDataDir() { } } -// 恢复默认数据目录 async function resetDataDir() { dataDirError.value = null - // 显示确认弹窗 + pendingNewDir.value = null + pendingMigrate.value = true + showConfirmModal.value = true +} + +function migrateToDefaultDir() { + dataDirError.value = null pendingNewDir.value = null pendingMigrate.value = true showConfirmModal.value = true @@ -340,6 +351,31 @@ defineExpose({ + +
+
+

+ {{ t('settings.storage.dataLocation.migrateHint') }} +

+

+ {{ defaultDataDir }} +

+
+ + {{ t('settings.storage.dataLocation.migrateAction') }} + +
+

{{ t('settings.storage.dataLocation.restartTip') }}

diff --git a/src/i18n/locales/en-US/settings.json b/src/i18n/locales/en-US/settings.json index c0119ff5..07d43331 100644 --- a/src/i18n/locales/en-US/settings.json +++ b/src/i18n/locales/en-US/settings.json @@ -209,6 +209,8 @@ "choose": "Choose", "reset": "Reset", "open": "Open", + "migrateHint": "Data is still at a legacy location. Migrate to the unified directory:", + "migrateAction": "Migrate Now", "restartTip": "Please restart the app after switching. Data will be migrated from the old directory.", "confirmTitle": "Confirm Directory Change", "confirmMessage": "You are about to change the data storage location. Existing data will be migrated to the new directory.", diff --git a/src/i18n/locales/ja-JP/settings.json b/src/i18n/locales/ja-JP/settings.json index 75be7214..1ddba94c 100644 --- a/src/i18n/locales/ja-JP/settings.json +++ b/src/i18n/locales/ja-JP/settings.json @@ -209,6 +209,8 @@ "choose": "場所を選択", "reset": "デフォルトに戻す", "open": "開く", + "migrateHint": "データがまだ旧ディレクトリにあります。統一ディレクトリへの移行をお勧めします:", + "migrateAction": "今すぐ移行", "restartTip": "ディレクトリ変更後はアプリを再起動してください。旧ディレクトリのキャッシュは自動で移行されます", "confirmTitle": "データディレクトリの切り替えを確認", "confirmMessage": "データの保存先を切り替えます。既存データは新しいディレクトリに移行されます。", diff --git a/src/i18n/locales/zh-CN/settings.json b/src/i18n/locales/zh-CN/settings.json index 8ea901df..fd3f9e56 100644 --- a/src/i18n/locales/zh-CN/settings.json +++ b/src/i18n/locales/zh-CN/settings.json @@ -209,6 +209,8 @@ "choose": "选择位置", "reset": "恢复默认", "open": "打开", + "migrateHint": "检测到数据仍在旧位置,建议迁移至统一目录:", + "migrateAction": "一键迁移", "restartTip": "切换目录后请重启应用,旧目录下缓存会跟随迁移", "confirmTitle": "确认切换数据目录", "confirmMessage": "您即将切换数据存储位置,现有数据将被迁移到新目录。", diff --git a/src/i18n/locales/zh-TW/settings.json b/src/i18n/locales/zh-TW/settings.json index f804cf9c..ddcc7e44 100644 --- a/src/i18n/locales/zh-TW/settings.json +++ b/src/i18n/locales/zh-TW/settings.json @@ -209,6 +209,8 @@ "choose": "選擇位置", "reset": "還原預設", "open": "開啟", + "migrateHint": "偵測到資料仍在舊位置,建議遷移至統一目錄:", + "migrateAction": "一鍵遷移", "restartTip": "切換目錄後請重新啟動應用程式,舊目錄下快取會跟隨遷移", "confirmTitle": "確認切換資料目錄", "confirmMessage": "您即將切換資料儲存位置,現有資料將被遷移到新目錄。",