Files
ChatLab/packages/node-runtime/src/node-path-provider.ts
T

164 lines
4.4 KiB
TypeScript

/**
* PathProvider 的 Node.js 独立实现
*
* 为 CLI / npm 服务版提供路径管理,不依赖 Electron。
*
* 目录分为两类:
* - 系统数据:固定在 ~/.chatlab/,存放配置、日志、缓存、AI 数据等
* - 用户数据:可配置位置,存放聊天记录数据库等核心资产
*
* 用户数据目录(userDataDir)解析优先级:
* 1. 构造函数传入的 userDataDir 参数
* 2. CHATLAB_DATA_DIR 环境变量
* 3. ~/.chatlab/config.toml → [data] user_data_dir
* 4. 平台默认路径
*/
import * as fs from 'fs'
import * as os from 'os'
import * as path from 'path'
import type { PathProvider } from '@openchatlab/core'
import { writeConfigField, loadConfig } from '@openchatlab/config'
import { migrateFromElectronIfNeeded } from './migrations/electron-data-migration'
import { applyPendingNodeDataDirMigration } from './data-dir-switch'
import { getChatLabTempScopeDir } from './temp-workspace'
const SYSTEM_DIR = path.join(os.homedir(), '.chatlab')
/**
* Set after resolveUserDataDir() detects Electron installation but cannot find databases.
* CLI should check this and block execution with instructions.
*/
let _pendingElectronDataWarning = false
export function hasPendingElectronDataWarning(): boolean {
return _pendingElectronDataWarning
}
export function applyPendingNodeDataDirMigrationIfNeeded(): { success: boolean; skipped?: boolean; error?: string } {
if (process.env.CHATLAB_DATA_DIR) return { success: true, skipped: true }
return applyPendingNodeDataDirMigration(SYSTEM_DIR)
}
export class NodePathProvider implements PathProvider {
private systemDir: string
private userDataDir: string
constructor(userDataDir?: string) {
this.systemDir = SYSTEM_DIR
this.userDataDir = userDataDir || resolveUserDataDir()
}
getSystemDir(): string {
return this.systemDir
}
getUserDataDir(): string {
return this.userDataDir
}
getDatabaseDir(): string {
return path.join(this.userDataDir, 'databases')
}
getVectorDir(): string {
return path.join(this.userDataDir, 'vector')
}
getAiDataDir(): string {
return path.join(this.systemDir, 'ai')
}
getSettingsDir(): string {
return path.join(this.systemDir, 'settings')
}
getCacheDir(): string {
return path.join(this.systemDir, 'cache')
}
getTempDir(): string {
return getChatLabTempScopeDir('runtime')
}
getLogsDir(): string {
return path.join(this.systemDir, 'logs')
}
getDownloadsDir(): string {
return path.join(os.homedir(), 'Downloads')
}
/**
* 确保系统目录和用户数据目录都存在
*/
ensureAllDirs(): void {
const dirs = [
this.systemDir,
this.userDataDir,
this.getDatabaseDir(),
this.getVectorDir(),
this.getAiDataDir(),
this.getSettingsDir(),
this.getCacheDir(),
this.getTempDir(),
this.getLogsDir(),
]
for (const dir of dirs) {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true })
}
}
const markerPath = path.join(this.userDataDir, '.chatlab')
if (!fs.existsSync(markerPath)) {
fs.writeFileSync(markerPath, 'ChatLab Data Directory', 'utf-8')
}
}
}
function resolveUserDataDir(): string {
const envDir = process.env.CHATLAB_DATA_DIR
if (envDir) {
return expandHome(envDir)
}
const config = loadConfig()
if (config.data.user_data_dir) {
return expandHome(config.data.user_data_dir)
}
// First run: check for existing Electron desktop data before using default
const result = migrateFromElectronIfNeeded(SYSTEM_DIR)
if (result.migrated && result.userDataDir) {
return result.userDataDir
}
const defaultDir = getDefaultNodeUserDataDir()
if (result.electronDetected) {
// Electron was used but we couldn't find databases — likely a custom data directory.
// Don't persist default to config.toml so migration retries on next startup.
_pendingElectronDataWarning = true
return defaultDir
}
writeConfigField('data', 'user_data_dir', defaultDir)
return defaultDir
}
export function getDefaultNodeUserDataDir(): string {
return path.join(os.homedir(), '.chatlab', 'data')
}
export function getSystemLogsDir(): string {
return path.join(SYSTEM_DIR, 'logs')
}
function expandHome(filePath: string): string {
if (filePath.startsWith('~/') || filePath === '~') {
return path.join(os.homedir(), filePath.slice(1))
}
return filePath
}