mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-22 08:40:08 +08:00
feat: app's version history log (#11097)
* feat: integrate version tracking in app initialization - Added versionService to record the current version during app startup. - This change prepares for upcoming data refactoring in version 2. * fix: lint from other PRs & format * feat: enhance version tracking with meaningful change detection - Updated VersionService to check for changes in version, OS, environment, packaged status, and install mode before recording a new entry. - Improved logging to reflect whether version information has changed or remained the same.
This commit is contained in:
parent
346af4d338
commit
7dce1d776b
@ -30,6 +30,7 @@ import {
|
|||||||
import selectionService, { initSelectionService } from './services/SelectionService'
|
import selectionService, { initSelectionService } from './services/SelectionService'
|
||||||
import { registerShortcuts } from './services/ShortcutService'
|
import { registerShortcuts } from './services/ShortcutService'
|
||||||
import { TrayService } from './services/TrayService'
|
import { TrayService } from './services/TrayService'
|
||||||
|
import { versionService } from './services/VersionService'
|
||||||
import { windowService } from './services/WindowService'
|
import { windowService } from './services/WindowService'
|
||||||
import { initWebviewHotkeys } from './services/WebviewService'
|
import { initWebviewHotkeys } from './services/WebviewService'
|
||||||
|
|
||||||
@ -110,6 +111,10 @@ if (!app.requestSingleInstanceLock()) {
|
|||||||
// Some APIs can only be used after this event occurs.
|
// Some APIs can only be used after this event occurs.
|
||||||
|
|
||||||
app.whenReady().then(async () => {
|
app.whenReady().then(async () => {
|
||||||
|
// Record current version for tracking
|
||||||
|
// A preparation for v2 data refactoring
|
||||||
|
versionService.recordCurrentVersion()
|
||||||
|
|
||||||
initWebviewHotkeys()
|
initWebviewHotkeys()
|
||||||
// Set app user model id for windows
|
// Set app user model id for windows
|
||||||
electronApp.setAppUserModelId(import.meta.env.VITE_MAIN_BUNDLE_ID || 'com.kangfenmao.CherryStudio')
|
electronApp.setAppUserModelId(import.meta.env.VITE_MAIN_BUNDLE_ID || 'com.kangfenmao.CherryStudio')
|
||||||
|
|||||||
285
src/main/services/VersionService.ts
Normal file
285
src/main/services/VersionService.ts
Normal file
@ -0,0 +1,285 @@
|
|||||||
|
import { loggerService } from '@logger'
|
||||||
|
import { app } from 'electron'
|
||||||
|
import fs from 'fs'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
const logger = loggerService.withContext('VersionService')
|
||||||
|
|
||||||
|
type OS = 'win' | 'mac' | 'linux' | 'unknown'
|
||||||
|
type Environment = 'prod' | 'dev'
|
||||||
|
type Packaged = 'packaged' | 'unpackaged'
|
||||||
|
type Mode = 'install' | 'portable'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Version record stored in version.log
|
||||||
|
*/
|
||||||
|
interface VersionRecord {
|
||||||
|
version: string
|
||||||
|
os: OS
|
||||||
|
environment: Environment
|
||||||
|
packaged: Packaged
|
||||||
|
mode: Mode
|
||||||
|
timestamp: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service for tracking application version history
|
||||||
|
* Stores version information in userData/version.log for data migration and diagnostics
|
||||||
|
*/
|
||||||
|
class VersionService {
|
||||||
|
private readonly VERSION_LOG_FILE = 'version.log'
|
||||||
|
private versionLogPath: string | null = null
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
// Lazy initialization of path since app.getPath may not be available during construction
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the full path to version.log file
|
||||||
|
* @returns {string} Full path to version log file
|
||||||
|
*/
|
||||||
|
private getVersionLogPath(): string {
|
||||||
|
if (!this.versionLogPath) {
|
||||||
|
this.versionLogPath = path.join(app.getPath('userData'), this.VERSION_LOG_FILE)
|
||||||
|
}
|
||||||
|
return this.versionLogPath
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets current operating system identifier
|
||||||
|
* @returns {OS} OS identifier
|
||||||
|
*/
|
||||||
|
private getCurrentOS(): OS {
|
||||||
|
switch (process.platform) {
|
||||||
|
case 'win32':
|
||||||
|
return 'win'
|
||||||
|
case 'darwin':
|
||||||
|
return 'mac'
|
||||||
|
case 'linux':
|
||||||
|
return 'linux'
|
||||||
|
default:
|
||||||
|
return 'unknown'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets current environment (production or development)
|
||||||
|
* @returns {Environment} Environment identifier
|
||||||
|
*/
|
||||||
|
private getCurrentEnvironment(): Environment {
|
||||||
|
return import.meta.env.MODE === 'production' ? 'prod' : 'dev'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets packaging status
|
||||||
|
* @returns {Packaged} Packaging status
|
||||||
|
*/
|
||||||
|
private getPackagedStatus(): Packaged {
|
||||||
|
return app.isPackaged ? 'packaged' : 'unpackaged'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets installation mode (install or portable)
|
||||||
|
* @returns {Mode} Installation mode
|
||||||
|
*/
|
||||||
|
private getInstallMode(): Mode {
|
||||||
|
return process.env.PORTABLE_EXECUTABLE_DIR !== undefined ? 'portable' : 'install'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates version log line for current application state
|
||||||
|
* @returns {string} Pipe-separated version record line
|
||||||
|
*/
|
||||||
|
private generateCurrentVersionLine(): string {
|
||||||
|
const version = app.getVersion()
|
||||||
|
const os = this.getCurrentOS()
|
||||||
|
const environment = this.getCurrentEnvironment()
|
||||||
|
const packaged = this.getPackagedStatus()
|
||||||
|
const mode = this.getInstallMode()
|
||||||
|
const timestamp = new Date().toISOString()
|
||||||
|
|
||||||
|
return `${version}|${os}|${environment}|${packaged}|${mode}|${timestamp}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a version log line into a VersionRecord object
|
||||||
|
* @param {string} line - Pipe-separated version record line
|
||||||
|
* @returns {VersionRecord | null} Parsed version record or null if invalid
|
||||||
|
*/
|
||||||
|
private parseVersionLine(line: string): VersionRecord | null {
|
||||||
|
try {
|
||||||
|
const parts = line.trim().split('|')
|
||||||
|
if (parts.length !== 6) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const [version, os, environment, packaged, mode, timestamp] = parts
|
||||||
|
|
||||||
|
// Validate data
|
||||||
|
if (
|
||||||
|
!version ||
|
||||||
|
!['win', 'mac', 'linux', 'unknown'].includes(os) ||
|
||||||
|
!['prod', 'dev'].includes(environment) ||
|
||||||
|
!['packaged', 'unpackaged'].includes(packaged) ||
|
||||||
|
!['install', 'portable'].includes(mode) ||
|
||||||
|
!timestamp
|
||||||
|
) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
version,
|
||||||
|
os: os as OS,
|
||||||
|
environment: environment as Environment,
|
||||||
|
packaged: packaged as Packaged,
|
||||||
|
mode: mode as Mode,
|
||||||
|
timestamp
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn(`Failed to parse version line: ${line}`, error as Error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the last 1KB from version.log and returns all lines
|
||||||
|
* Uses reverse reading from file end to avoid reading the entire file
|
||||||
|
* @returns {string[]} Array of version lines from the last 1KB
|
||||||
|
*/
|
||||||
|
private readLastVersionLines(): string[] {
|
||||||
|
const logPath = this.getVersionLogPath()
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!fs.existsSync(logPath)) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const stats = fs.statSync(logPath)
|
||||||
|
const fileSize = stats.size
|
||||||
|
|
||||||
|
if (fileSize === 0) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read from the end of the file, 1KB is enough to find previous version
|
||||||
|
// Typical line: "1.7.0-beta.3|win|prod|packaged|install|2025-01-15T08:30:00.000Z\n" (~70 bytes)
|
||||||
|
// 1KB can store ~14 lines, which is more than enough
|
||||||
|
const bufferSize = Math.min(1024, fileSize)
|
||||||
|
const buffer = Buffer.alloc(bufferSize)
|
||||||
|
|
||||||
|
const fd = fs.openSync(logPath, 'r')
|
||||||
|
try {
|
||||||
|
const startPosition = Math.max(0, fileSize - bufferSize)
|
||||||
|
fs.readSync(fd, buffer, 0, bufferSize, startPosition)
|
||||||
|
|
||||||
|
const content = buffer.toString('utf-8')
|
||||||
|
const lines = content
|
||||||
|
.trim()
|
||||||
|
.split('\n')
|
||||||
|
.filter((line) => line.trim())
|
||||||
|
|
||||||
|
return lines
|
||||||
|
} finally {
|
||||||
|
fs.closeSync(fd)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to read version log:', error as Error)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appends a version record line to version.log
|
||||||
|
* @param {string} line - Version record line to append
|
||||||
|
*/
|
||||||
|
private appendVersionLine(line: string): void {
|
||||||
|
const logPath = this.getVersionLogPath()
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs.appendFileSync(logPath, line + '\n', 'utf-8')
|
||||||
|
logger.debug(`Version recorded: ${line}`)
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to append version log:', error as Error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Records the current version on application startup
|
||||||
|
* Only adds a new record if the version has changed since the last run
|
||||||
|
*/
|
||||||
|
recordCurrentVersion(): void {
|
||||||
|
try {
|
||||||
|
const currentLine = this.generateCurrentVersionLine()
|
||||||
|
const lines = this.readLastVersionLines()
|
||||||
|
|
||||||
|
// Add new record if this is the first run or version has changed
|
||||||
|
if (lines.length === 0) {
|
||||||
|
logger.info('First run detected, creating version log')
|
||||||
|
this.appendVersionLine(currentLine)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastLine = lines[lines.length - 1]
|
||||||
|
const lastRecord = this.parseVersionLine(lastLine)
|
||||||
|
const currentVersion = app.getVersion()
|
||||||
|
|
||||||
|
// Check if any meaningful field has changed (version, os, environment, packaged, mode)
|
||||||
|
const currentOS = this.getCurrentOS()
|
||||||
|
const currentEnvironment = this.getCurrentEnvironment()
|
||||||
|
const currentPackaged = this.getPackagedStatus()
|
||||||
|
const currentMode = this.getInstallMode()
|
||||||
|
|
||||||
|
const hasMeaningfulChange =
|
||||||
|
!lastRecord ||
|
||||||
|
lastRecord.version !== currentVersion ||
|
||||||
|
lastRecord.os !== currentOS ||
|
||||||
|
lastRecord.environment !== currentEnvironment ||
|
||||||
|
lastRecord.packaged !== currentPackaged ||
|
||||||
|
lastRecord.mode !== currentMode
|
||||||
|
|
||||||
|
if (hasMeaningfulChange) {
|
||||||
|
logger.info(`Version information changed, recording new entry`)
|
||||||
|
this.appendVersionLine(currentLine)
|
||||||
|
} else {
|
||||||
|
logger.debug(`Version information not changed, skip recording`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to record current version:', error as Error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the previous version record (last record with different version than current)
|
||||||
|
* Reads from the last 1KB of version.log to find the most recent different version
|
||||||
|
* Useful for detecting version upgrades and running migrations
|
||||||
|
* @returns {VersionRecord | null} Previous version record or null if not available
|
||||||
|
*/
|
||||||
|
getPreviousVersion(): VersionRecord | null {
|
||||||
|
try {
|
||||||
|
const lines = this.readLastVersionLines()
|
||||||
|
if (lines.length === 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentVersion = app.getVersion()
|
||||||
|
|
||||||
|
// Read from the end backwards to find the first different version
|
||||||
|
for (let i = lines.length - 1; i >= 0; i--) {
|
||||||
|
const record = this.parseVersionLine(lines[i])
|
||||||
|
if (record && record.version !== currentVersion) {
|
||||||
|
return record
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to get previous version:', error as Error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Singleton instance of VersionService
|
||||||
|
*/
|
||||||
|
export const versionService = new VersionService()
|
||||||
Loading…
Reference in New Issue
Block a user