mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-10-25 17:54:34 +08:00
feat(cli): block incompatible data directories at startup
This commit is contained in:
1 parent
c518a4d968
commit
84b42a68f7
6 files changed
+183
-4
No files matched your search
+3
-1
@@ -27,6 +27,7 @@ import {
|
||||
import { getVersion } from './version'
|
||||
import { resolveCliPath } from './paths'
|
||||
import { isPortAvailable, formatPortInUseError } from './http/port'
|
||||
import { assertCliDataDirCompatible } from './runtime-compat'
|
||||
|
||||
const program = new Command()
|
||||
|
||||
@@ -519,6 +520,7 @@ function initRuntime() {
|
||||
const userDataDir = config.data.user_data_dir || undefined
|
||||
const pathProvider = new NodePathProvider(userDataDir)
|
||||
pathProvider.ensureAllDirs()
|
||||
const runtime = assertCliDataDirCompatible(pathProvider, 'cli')
|
||||
|
||||
if (hasPendingElectronDataWarning() || !verifyCliDataPath(pathProvider.getDatabaseDir())) {
|
||||
printElectronDataError()
|
||||
@@ -526,7 +528,7 @@ function initRuntime() {
|
||||
}
|
||||
|
||||
const nativeBinding = resolveNativeBinding()
|
||||
const dbManager = new DatabaseManager(pathProvider, { nativeBinding })
|
||||
const dbManager = new DatabaseManager(pathProvider, { nativeBinding, runtime })
|
||||
return { config, pathProvider, dbManager }
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ import { createCliRunAgentStream } from '../ai/agent-stream-runner'
|
||||
import { initSync, cleanupSync } from '../sync'
|
||||
import { resolveCliPath } from '../paths'
|
||||
import { resolveApiKey, writeAuthProfile } from '@openchatlab/config'
|
||||
import { assertCliDataDirCompatible } from '../runtime-compat'
|
||||
|
||||
let server: FastifyInstance | null = null
|
||||
let dbManager: DatabaseManager | null = null
|
||||
@@ -112,6 +113,7 @@ export async function startHttpServer(options?: HttpServerOptions): Promise<{
|
||||
const userDataDir = config.data.user_data_dir || undefined
|
||||
const pathProvider = new NodePathProvider(userDataDir)
|
||||
pathProvider.ensureAllDirs()
|
||||
const runtime = assertCliDataDirCompatible(pathProvider, 'cli')
|
||||
|
||||
if (hasPendingElectronDataWarning() || !verifyCliDataPath(pathProvider.getDatabaseDir())) {
|
||||
console.error(
|
||||
@@ -147,7 +149,7 @@ export async function startHttpServer(options?: HttpServerOptions): Promise<{
|
||||
})
|
||||
await migrationRunner.run()
|
||||
const nativeBinding = resolveNativeBinding()
|
||||
dbManager = new DatabaseManager(pathProvider, { nativeBinding })
|
||||
dbManager = new DatabaseManager(pathProvider, { nativeBinding, runtime })
|
||||
|
||||
const aiDataDir = pathProvider.getAiDataDir()
|
||||
aiChatManager = new AIChatManager(aiDataDir, { nativeBinding })
|
||||
|
||||
+4
-2
@@ -15,6 +15,7 @@ import {
|
||||
import { startMcpServer } from 'chatlab-mcp'
|
||||
import { getVersion } from './version'
|
||||
import { resolveCliPath } from './paths'
|
||||
import { assertCliDataDirCompatible } from './runtime-compat'
|
||||
|
||||
function resolveNativeBinding(): string | undefined {
|
||||
if (process.versions.electron) return undefined
|
||||
@@ -23,11 +24,12 @@ function resolveNativeBinding(): string | undefined {
|
||||
return undefined
|
||||
}
|
||||
|
||||
function initMcpRuntime(): DatabaseManager {
|
||||
export function initMcpRuntime(): DatabaseManager {
|
||||
const config = loadConfig()
|
||||
const userDataDir = config.data.user_data_dir || undefined
|
||||
const pathProvider = new NodePathProvider(userDataDir)
|
||||
pathProvider.ensureAllDirs()
|
||||
const runtime = assertCliDataDirCompatible(pathProvider, 'mcp')
|
||||
|
||||
if (hasPendingElectronDataWarning() || !verifyCliDataPath(pathProvider.getDatabaseDir())) {
|
||||
console.error('[MCP] Electron desktop data detected but databases not found.')
|
||||
@@ -36,7 +38,7 @@ function initMcpRuntime(): DatabaseManager {
|
||||
}
|
||||
|
||||
const nativeBinding = resolveNativeBinding()
|
||||
return new DatabaseManager(pathProvider, { nativeBinding })
|
||||
return new DatabaseManager(pathProvider, { nativeBinding, runtime })
|
||||
}
|
||||
|
||||
export async function startCliMcpServer(): Promise<void> {
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
import os from 'node:os'
|
||||
import path from 'node:path'
|
||||
import test from 'node:test'
|
||||
import { startHttpServer, stopHttpServer } from './http'
|
||||
import { initMcpRuntime } from './mcp'
|
||||
|
||||
function makeTempDir(): string {
|
||||
const baseDir = fs.existsSync('/private/tmp') ? '/private/tmp' : os.tmpdir()
|
||||
return fs.mkdtempSync(path.join(baseDir, 'chatlab-cli-startup-'))
|
||||
}
|
||||
|
||||
function writeIncompatibleMeta(userDataDir: string): void {
|
||||
fs.mkdirSync(userDataDir, { recursive: true })
|
||||
fs.writeFileSync(
|
||||
path.join(userDataDir, '.chatlab-meta.json'),
|
||||
JSON.stringify({
|
||||
formatVersion: 1,
|
||||
minRuntimeVersion: '999.0.0',
|
||||
dataCompatibilityVersion: 1,
|
||||
reasons: ['segment-schema'],
|
||||
updatedBy: { runtime: 'desktop', module: 'chat-db-migration', version: '999.0.0' },
|
||||
updatedAt: 1780830000,
|
||||
}),
|
||||
'utf-8'
|
||||
)
|
||||
}
|
||||
|
||||
async function withDataDirEnv<T>(userDataDir: string, fn: () => Promise<T> | T): Promise<T> {
|
||||
const previousDataDir = process.env.CHATLAB_DATA_DIR
|
||||
const previousOverride = process.env.CHATLAB_ALLOW_INCOMPATIBLE_DATA_DIR
|
||||
process.env.CHATLAB_DATA_DIR = userDataDir
|
||||
delete process.env.CHATLAB_ALLOW_INCOMPATIBLE_DATA_DIR
|
||||
|
||||
try {
|
||||
return await fn()
|
||||
} finally {
|
||||
if (previousDataDir === undefined) {
|
||||
delete process.env.CHATLAB_DATA_DIR
|
||||
} else {
|
||||
process.env.CHATLAB_DATA_DIR = previousDataDir
|
||||
}
|
||||
|
||||
if (previousOverride === undefined) {
|
||||
delete process.env.CHATLAB_ALLOW_INCOMPATIBLE_DATA_DIR
|
||||
} else {
|
||||
process.env.CHATLAB_ALLOW_INCOMPATIBLE_DATA_DIR = previousOverride
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test('startHttpServer fails before serving when data directory requires a newer runtime', async () => {
|
||||
const userDataDir = makeTempDir()
|
||||
writeIncompatibleMeta(userDataDir)
|
||||
|
||||
await withDataDirEnv(userDataDir, async () => {
|
||||
await assert.rejects(
|
||||
() => startHttpServer({ port: 0, host: '127.0.0.1', token: 'test_token', requireAuth: false }),
|
||||
/requires ChatLab 999\.0\.0 or newer/
|
||||
)
|
||||
})
|
||||
|
||||
await stopHttpServer()
|
||||
})
|
||||
|
||||
test('initMcpRuntime fails before starting MCP when data directory requires a newer runtime', async () => {
|
||||
const userDataDir = makeTempDir()
|
||||
writeIncompatibleMeta(userDataDir)
|
||||
|
||||
await withDataDirEnv(userDataDir, () => {
|
||||
assert.throws(() => initMcpRuntime(), /requires ChatLab 999\.0\.0 or newer/)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,54 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
import os from 'node:os'
|
||||
import path from 'node:path'
|
||||
import test from 'node:test'
|
||||
import type { PathProvider } from '@openchatlab/core'
|
||||
import { assertCliDataDirCompatible } from './runtime-compat'
|
||||
|
||||
function makeTempDir(): string {
|
||||
const baseDir = fs.existsSync('/private/tmp') ? '/private/tmp' : os.tmpdir()
|
||||
return fs.mkdtempSync(path.join(baseDir, 'chatlab-cli-compat-'))
|
||||
}
|
||||
|
||||
function makePathProvider(userDataDir: string): PathProvider {
|
||||
return {
|
||||
getSystemDir: () => path.join(userDataDir, '..', 'system'),
|
||||
getUserDataDir: () => userDataDir,
|
||||
getDatabaseDir: () => path.join(userDataDir, 'databases'),
|
||||
getAiDataDir: () => path.join(userDataDir, '..', 'system', 'ai'),
|
||||
getSettingsDir: () => path.join(userDataDir, '..', 'system', 'settings'),
|
||||
getCacheDir: () => path.join(userDataDir, '..', 'system', 'cache'),
|
||||
getTempDir: () => path.join(userDataDir, '..', 'system', 'temp'),
|
||||
getLogsDir: () => path.join(userDataDir, '..', 'system', 'logs'),
|
||||
getDownloadsDir: () => path.join(userDataDir, '..', 'downloads'),
|
||||
}
|
||||
}
|
||||
|
||||
test('assertCliDataDirCompatible formats a startup error for older CLI runtimes', () => {
|
||||
const userDataDir = makeTempDir()
|
||||
fs.writeFileSync(
|
||||
path.join(userDataDir, '.chatlab-meta.json'),
|
||||
JSON.stringify({
|
||||
formatVersion: 1,
|
||||
minRuntimeVersion: '999.0.0',
|
||||
dataCompatibilityVersion: 1,
|
||||
reasons: ['segment-schema'],
|
||||
updatedBy: { runtime: 'desktop', module: 'chat-db-migration', version: '999.0.0' },
|
||||
updatedAt: 1780830000,
|
||||
}),
|
||||
'utf-8'
|
||||
)
|
||||
|
||||
assert.throws(
|
||||
() => assertCliDataDirCompatible(makePathProvider(userDataDir), 'cli', { version: '0.25.0' }),
|
||||
(error) => {
|
||||
assert.ok(error instanceof Error)
|
||||
assert.match(error.message, /requires ChatLab 999\.0\.0 or newer/)
|
||||
assert.match(error.message, /Current cli version: 0\.25\.0/)
|
||||
assert.match(error.message, new RegExp(userDataDir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')))
|
||||
assert.match(error.message, /npm install -g chatlab-cli@latest/)
|
||||
return true
|
||||
}
|
||||
)
|
||||
})
|
||||
@@ -0,0 +1,45 @@
|
||||
import type { PathProvider } from '@openchatlab/core'
|
||||
import {
|
||||
assertDataDirCompatible,
|
||||
DataDirCompatibilityError,
|
||||
type RuntimeIdentity,
|
||||
type RuntimeKind,
|
||||
} from '@openchatlab/node-runtime'
|
||||
import { getVersion } from './version'
|
||||
|
||||
export function createCliRuntimeIdentity(kind: Extract<RuntimeKind, 'cli' | 'mcp'>): RuntimeIdentity {
|
||||
return { version: getVersion(), kind }
|
||||
}
|
||||
|
||||
export function assertCliDataDirCompatible(
|
||||
pathProvider: PathProvider,
|
||||
kind: Extract<RuntimeKind, 'cli' | 'mcp'>,
|
||||
options?: { version?: string }
|
||||
): RuntimeIdentity {
|
||||
const runtime: RuntimeIdentity = { version: options?.version ?? getVersion(), kind }
|
||||
|
||||
try {
|
||||
assertDataDirCompatible(pathProvider, runtime)
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof DataDirCompatibilityError &&
|
||||
error.code === 'DATA_DIR_REQUIRES_NEWER_RUNTIME' &&
|
||||
error.minRuntimeVersion
|
||||
) {
|
||||
throw new Error(formatCliDataDirCompatibilityError(error, runtime), { cause: error })
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
|
||||
return runtime
|
||||
}
|
||||
|
||||
function formatCliDataDirCompatibilityError(error: DataDirCompatibilityError, runtime: RuntimeIdentity): string {
|
||||
return [
|
||||
`ChatLab data directory requires ChatLab ${error.minRuntimeVersion} or newer.`,
|
||||
`Current ${runtime.kind} version: ${runtime.version}.`,
|
||||
`Data directory: ${error.userDataDir}.`,
|
||||
'Upgrade: npm install -g chatlab-cli@latest',
|
||||
].join('\n')
|
||||
}
|
||||
Reference in new issue
Block a user