mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-10-26 02:04:37 +08:00
fix: establish runtime identity for data directory gates
Use the packaged app version (not Electron's dev-mode 0.0.0) across all compatibility checks. Make DatabaseManager require a RuntimeIdentity so every gate write carries a verified version and kind. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
3f96d31819
commit
1c7781d85b
14 files changed
+89
-25
No files matched your search
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "chatlab-cli",
|
||||
"version": "0.0.0",
|
||||
"version": "0.25.1",
|
||||
"description": "ChatLab CLI & Service - Chat history analysis tool",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
|
||||
@@ -8,6 +8,7 @@ import type { FastifyInstance } from 'fastify'
|
||||
import { app } from 'electron'
|
||||
import { successResponse } from '../errors'
|
||||
import * as worker from '../../worker/workerManager'
|
||||
import { getDesktopAppVersion } from '../../runtime-compat'
|
||||
|
||||
export function registerSystemRoutes(server: FastifyInstance): void {
|
||||
server.get('/api/v1/status', async () => {
|
||||
@@ -21,7 +22,7 @@ export function registerSystemRoutes(server: FastifyInstance): void {
|
||||
|
||||
return successResponse({
|
||||
name: 'ChatLab API',
|
||||
version: app.getVersion(),
|
||||
version: getDesktopAppVersion(app.getVersion()),
|
||||
uptime: Math.floor(process.uptime()),
|
||||
sessionCount,
|
||||
})
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
import { migrateAllDatabases, checkMigrationNeeded } from './database/core'
|
||||
import { initLocale } from './i18n'
|
||||
import { MigrationRunner, ALL_MIGRATIONS } from '@openchatlab/config'
|
||||
import { assertDesktopDataDirCompatible } from './runtime-compat'
|
||||
import { assertDesktopDataDirCompatible, getDesktopAppVersion } from './runtime-compat'
|
||||
import type { RuntimeIdentity } from '@openchatlab/node-runtime/src/data-dir-compat'
|
||||
import {
|
||||
applyCurrentTitleBarOverlay,
|
||||
@@ -118,7 +118,7 @@ class MainProcess {
|
||||
|
||||
let runtime: RuntimeIdentity
|
||||
try {
|
||||
runtime = assertDesktopDataDirCompatible(getPathProvider(), app.getVersion())
|
||||
runtime = assertDesktopDataDirCompatible(getPathProvider(), getDesktopAppVersion(app.getVersion()))
|
||||
} catch (error) {
|
||||
console.error('[Main] Data directory compatibility check failed:', error)
|
||||
dialog.showErrorBox(
|
||||
|
||||
@@ -42,7 +42,7 @@ import { getManager as getAIChatManager } from './ai/chats'
|
||||
import { getManager as getAssistantManager } from './ai/assistant/manager'
|
||||
import { getManager as getSkillManager } from './ai/skills/manager'
|
||||
import { createElectronRunAgentStream } from './ai/agent-stream-runner'
|
||||
import { assertDesktopDataDirCompatible } from './runtime-compat'
|
||||
import { assertDesktopDataDirCompatible, getDesktopAppVersion } from './runtime-compat'
|
||||
|
||||
export interface InternalEndpoint {
|
||||
baseUrl: string
|
||||
@@ -85,7 +85,7 @@ export async function startInternalServer(pathProvider: PathProvider): Promise<I
|
||||
try {
|
||||
const token = `int_${randomBytes(32).toString('hex')}`
|
||||
const { app } = await import('electron')
|
||||
const runtime = assertDesktopDataDirCompatible(pathProvider, app.getVersion())
|
||||
const runtime = assertDesktopDataDirCompatible(pathProvider, getDesktopAppVersion(app.getVersion()))
|
||||
|
||||
newDbManager = new DatabaseManager(pathProvider, { runtime })
|
||||
const sessionAdapter = createDatabaseManagerAdapter(newDbManager)
|
||||
@@ -142,7 +142,7 @@ export async function startInternalServer(pathProvider: PathProvider): Promise<I
|
||||
dbManager: newDbManager,
|
||||
sessionAdapter,
|
||||
pathProvider,
|
||||
getVersion: () => app.getVersion(),
|
||||
getVersion: () => getDesktopAppVersion(app.getVersion()),
|
||||
mergeSessionCache: newMergeCache,
|
||||
streamImport: electronStreamImport,
|
||||
aiDataDir,
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
applyTitleBarOverlayColor,
|
||||
resetCurrentTitleBarOverlayColor,
|
||||
} from '../window-titlebar'
|
||||
import { getDesktopAppVersion } from '../runtime-compat'
|
||||
|
||||
type AppWithQuitFlag = typeof app & { isQuiting?: boolean }
|
||||
// 通过类型扩展记录应用退出意图,避免使用 @ts-ignore。
|
||||
@@ -107,8 +108,7 @@ export function registerWindowHandlers(ctx: IpcContext): void {
|
||||
|
||||
// ==================== 应用信息 ====================
|
||||
ipcMain.handle('app:getVersion', () => {
|
||||
const v = app.getVersion()
|
||||
return v && v !== '0.0.0' ? v : typeof __APP_VERSION__ !== 'undefined' ? __APP_VERSION__ : v
|
||||
return getDesktopAppVersion(app.getVersion())
|
||||
})
|
||||
|
||||
// 重启应用
|
||||
|
||||
@@ -4,7 +4,7 @@ import os from 'node:os'
|
||||
import path from 'node:path'
|
||||
import test from 'node:test'
|
||||
import type { PathProvider } from '@openchatlab/core'
|
||||
import { assertDesktopDataDirCompatible } from './runtime-compat'
|
||||
import { assertDesktopDataDirCompatible, resolveDesktopAppVersion } from './runtime-compat'
|
||||
|
||||
function makeTempDir(): string {
|
||||
const baseDir = fs.existsSync('/private/tmp') ? '/private/tmp' : os.tmpdir()
|
||||
@@ -25,6 +25,30 @@ function makePathProvider(userDataDir: string): PathProvider {
|
||||
}
|
||||
}
|
||||
|
||||
test('resolveDesktopAppVersion falls back to bundled app version when Electron reports 0.0.0', () => {
|
||||
assert.equal(resolveDesktopAppVersion('0.0.0', '0.25.1'), '0.25.1')
|
||||
})
|
||||
|
||||
test('assertDesktopDataDirCompatible accepts a gated data directory with the bundled fallback version', () => {
|
||||
const userDataDir = makeTempDir()
|
||||
fs.writeFileSync(
|
||||
path.join(userDataDir, '.chatlab-meta.json'),
|
||||
JSON.stringify({
|
||||
formatVersion: 1,
|
||||
minRuntimeVersion: '0.25.1',
|
||||
dataCompatibilityVersion: 1,
|
||||
reasons: ['segment-schema'],
|
||||
updatedBy: { runtime: 'cli', module: 'chat-db-migration', version: '0.25.1' },
|
||||
updatedAt: 1780830000,
|
||||
}),
|
||||
'utf-8'
|
||||
)
|
||||
|
||||
assert.doesNotThrow(() =>
|
||||
assertDesktopDataDirCompatible(makePathProvider(userDataDir), resolveDesktopAppVersion('0.0.0', '0.25.1'))
|
||||
)
|
||||
})
|
||||
|
||||
test('assertDesktopDataDirCompatible formats a startup error for older desktop runtimes', () => {
|
||||
const userDataDir = makeTempDir()
|
||||
fs.writeFileSync(
|
||||
|
||||
@@ -5,6 +5,20 @@ import {
|
||||
type RuntimeIdentity,
|
||||
} from '@openchatlab/node-runtime/src/data-dir-compat'
|
||||
|
||||
export function resolveDesktopAppVersion(electronVersion: string | null | undefined, bundledVersion?: string): string {
|
||||
const normalizedElectronVersion = normalizeVersion(electronVersion)
|
||||
if (normalizedElectronVersion && normalizedElectronVersion !== '0.0.0') return normalizedElectronVersion
|
||||
|
||||
const normalizedBundledVersion = normalizeVersion(bundledVersion)
|
||||
if (normalizedBundledVersion) return normalizedBundledVersion
|
||||
|
||||
return normalizedElectronVersion || '0.0.0'
|
||||
}
|
||||
|
||||
export function getDesktopAppVersion(electronVersion: string | null | undefined): string {
|
||||
return resolveDesktopAppVersion(electronVersion, typeof __APP_VERSION__ !== 'undefined' ? __APP_VERSION__ : undefined)
|
||||
}
|
||||
|
||||
export function createDesktopRuntimeIdentity(version: string): RuntimeIdentity {
|
||||
return { version, kind: 'desktop' }
|
||||
}
|
||||
@@ -29,6 +43,10 @@ export function assertDesktopDataDirCompatible(pathProvider: PathProvider, versi
|
||||
return runtime
|
||||
}
|
||||
|
||||
function normalizeVersion(version: string | null | undefined): string {
|
||||
return typeof version === 'string' ? version.trim() : ''
|
||||
}
|
||||
|
||||
function formatDesktopDataDirCompatibilityError(error: DataDirCompatibilityError, runtime: RuntimeIdentity): string {
|
||||
return [
|
||||
`ChatLab data directory requires ChatLab ${error.minRuntimeVersion} or newer.`,
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { app, session } from 'electron'
|
||||
import { getDesktopAppVersion } from '../runtime-compat'
|
||||
|
||||
export function buildChatLabUserAgentHeaders(): Record<string, string> {
|
||||
const chatLabVersion = (() => {
|
||||
try {
|
||||
return app.getVersion() || 'dev'
|
||||
return getDesktopAppVersion(app.getVersion()) || 'dev'
|
||||
} catch {
|
||||
return 'dev'
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import type { StreamImportResult } from './import'
|
||||
|
||||
import { getDatabaseDir, getCacheDir, getTempDir, ensureDir } from '../paths'
|
||||
import { getNlpDir } from '../nlp/dictManager'
|
||||
import { getDesktopAppVersion } from '../runtime-compat'
|
||||
|
||||
// Worker 实例
|
||||
let worker: Worker | null = null
|
||||
@@ -88,7 +89,7 @@ function checkAndResetAnalysisCache(): void {
|
||||
}
|
||||
|
||||
const versionFile = path.join(queryCacheDir, '.cache_version')
|
||||
const currentVersion = app.getVersion()
|
||||
const currentVersion = getDesktopAppVersion(app.getVersion())
|
||||
|
||||
let lastVersion: string | null = null
|
||||
try {
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ChatLab",
|
||||
"version": "0.25.0",
|
||||
"version": "0.25.1",
|
||||
"private": true,
|
||||
"description": "本地化的聊天记录分析工具,通过 SQL 和 AI Agent 回顾你的社交记忆",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "chatlab-mcp",
|
||||
"version": "0.1.0",
|
||||
"version": "0.25.1",
|
||||
"description": "ChatLab MCP Server — shared core for CLI and Desktop, also usable standalone via npx",
|
||||
"type": "module",
|
||||
"main": "./dist/index.mjs",
|
||||
|
||||
@@ -16,9 +16,8 @@ function main(): void {
|
||||
const userDataDir = config.data.user_data_dir || undefined
|
||||
const pathProvider = new NodePathProvider(userDataDir)
|
||||
pathProvider.ensureAllDirs()
|
||||
const dbManager = new DatabaseManager(pathProvider)
|
||||
|
||||
const version = process.env.npm_package_version ?? '0.1.0'
|
||||
const version = process.env.npm_package_version ?? '0.25.1'
|
||||
const dbManager = new DatabaseManager(pathProvider, { runtime: { version, kind: 'mcp' } })
|
||||
|
||||
startMcpServer({ version, dbManager }).catch((err) => {
|
||||
console.error('[chatlab-mcp] Fatal error:', err)
|
||||
|
||||
@@ -30,6 +30,15 @@ function createPathProvider(root: string): PathProvider {
|
||||
}
|
||||
}
|
||||
|
||||
test('constructor rejects missing runtime unless the test-only bypass is explicit', () => {
|
||||
const root = makeTempDir()
|
||||
|
||||
assert.throws(() => new DatabaseManager(createPathProvider(root), { nativeBinding }), /runtime identity is required/i)
|
||||
assert.doesNotThrow(
|
||||
() => new DatabaseManager(createPathProvider(root), { nativeBinding, allowMissingRuntimeForTests: true })
|
||||
)
|
||||
})
|
||||
|
||||
test('open migrates legacy member name columns before readonly queries', () => {
|
||||
const root = makeTempDir()
|
||||
const dbDir = path.join(root, 'data', 'databases')
|
||||
@@ -67,7 +76,7 @@ test('open migrates legacy member name columns before readonly queries', () => {
|
||||
`)
|
||||
rawDb.close()
|
||||
|
||||
const manager = new DatabaseManager(createPathProvider(root), { nativeBinding })
|
||||
const manager = new DatabaseManager(createPathProvider(root), { nativeBinding, allowMissingRuntimeForTests: true })
|
||||
const db = manager.open('legacy')
|
||||
assert.ok(db)
|
||||
|
||||
@@ -127,7 +136,7 @@ test('open backfills FTS index when migrating legacy sessions', () => {
|
||||
`)
|
||||
rawDb.close()
|
||||
|
||||
const manager = new DatabaseManager(createPathProvider(root), { nativeBinding })
|
||||
const manager = new DatabaseManager(createPathProvider(root), { nativeBinding, allowMissingRuntimeForTests: true })
|
||||
const db = manager.open('fts-legacy')
|
||||
assert.ok(db)
|
||||
|
||||
@@ -182,7 +191,7 @@ test('open migrates v2 chat_session schema to current segment schema', () => {
|
||||
`)
|
||||
rawDb.close()
|
||||
|
||||
const manager = new DatabaseManager(createPathProvider(root), { nativeBinding })
|
||||
const manager = new DatabaseManager(createPathProvider(root), { nativeBinding, allowMissingRuntimeForTests: true })
|
||||
const db = manager.open('v2-segment-schema')
|
||||
assert.ok(db)
|
||||
|
||||
@@ -524,7 +533,7 @@ test('open preserves readonly access for current-schema databases', { skip: proc
|
||||
fs.chmodSync(dbDir, 0o555)
|
||||
|
||||
try {
|
||||
const manager = new DatabaseManager(createPathProvider(root), { nativeBinding })
|
||||
const manager = new DatabaseManager(createPathProvider(root), { nativeBinding, allowMissingRuntimeForTests: true })
|
||||
const db = manager.open('current-readonly')
|
||||
assert.ok(db)
|
||||
assert.equal(db.readonly, true)
|
||||
@@ -556,7 +565,7 @@ test('listSessionIds ignores non-ChatLab sqlite databases without migrating them
|
||||
`)
|
||||
rawDb.close()
|
||||
|
||||
const manager = new DatabaseManager(createPathProvider(root), { nativeBinding })
|
||||
const manager = new DatabaseManager(createPathProvider(root), { nativeBinding, allowMissingRuntimeForTests: true })
|
||||
|
||||
assert.deepEqual(manager.listSessionIds(), [])
|
||||
manager.closeAll()
|
||||
|
||||
@@ -28,19 +28,30 @@ function createMigrationDeps(overrides?: MigrationDeps): MigrationDeps {
|
||||
}
|
||||
}
|
||||
|
||||
interface DatabaseManagerOptions {
|
||||
nativeBinding?: string
|
||||
migrationDeps?: MigrationDeps
|
||||
runtime?: RuntimeIdentity
|
||||
allowMissingRuntimeForTests?: boolean
|
||||
}
|
||||
|
||||
export class DatabaseManager {
|
||||
private cache = new Map<string, DatabaseAdapter>()
|
||||
private nativeBinding?: string
|
||||
private migrationDeps?: MigrationDeps
|
||||
private runtime?: RuntimeIdentity
|
||||
private runtime: RuntimeIdentity | null
|
||||
|
||||
constructor(
|
||||
private pathProvider: PathProvider,
|
||||
options?: { nativeBinding?: string; migrationDeps?: MigrationDeps; runtime?: RuntimeIdentity }
|
||||
options: DatabaseManagerOptions = {}
|
||||
) {
|
||||
if (!options.runtime && !options.allowMissingRuntimeForTests) {
|
||||
throw new Error('DatabaseManager runtime identity is required for data directory compatibility checks.')
|
||||
}
|
||||
|
||||
this.nativeBinding = options?.nativeBinding
|
||||
this.migrationDeps = createMigrationDeps(options?.migrationDeps)
|
||||
this.runtime = options?.runtime
|
||||
this.runtime = options?.runtime ?? null
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user