Add configuration merge service

This commit is contained in:
2025-06-23 12:03:56 +08:00
parent d6dd34db87
commit 4f8272e290
16 changed files with 627 additions and 208 deletions

View File

@@ -891,6 +891,11 @@ export class KeyBindingConfig {
* KeyBindingMetadata 快捷键配置元数据
*/
export class KeyBindingMetadata {
/**
* 配置版本
*/
"version": string;
/**
* 最后更新时间
*/
@@ -898,6 +903,9 @@ export class KeyBindingMetadata {
/** Creates a new KeyBindingMetadata instance. */
constructor($$source: Partial<KeyBindingMetadata> = {}) {
if (!("version" in $$source)) {
this["version"] = "";
}
if (!("lastUpdated" in $$source)) {
this["lastUpdated"] = "";
}

View File

@@ -19,6 +19,10 @@ export default {
noLanguageFound: 'No language found',
formatHint: 'Current block supports formatting, use Ctrl+Shift+F shortcut for formatting',
},
languages: {
'zh-CN': 'Chinese',
'en-US': 'English'
},
systemTheme: {
dark: 'Dark',
light: 'Light',

View File

@@ -19,6 +19,10 @@ export default {
noLanguageFound: '未找到匹配的语言',
formatHint: '当前区块支持格式化,使用 Ctrl+Shift+F 进行格式化',
},
languages: {
'zh-CN': '简体中文',
'en-US': 'English'
},
systemTheme: {
dark: '深色',
light: '浅色',

View File

@@ -149,7 +149,7 @@ const DEFAULT_CONFIG: AppConfig = {
},
metadata: {
version: '1.0.0',
lastUpdated: new Date().toString()
lastUpdated: new Date().toString(),
}
};

View File

@@ -17,76 +17,69 @@ export const useDocumentStore = defineStore('document', () => {
const isSaveInProgress = computed(() => isSaving.value);
const lastSavedTime = computed(() => lastSaved.value);
// 状态管理包装器
const withStateGuard = async <T>(
operation: () => Promise<T>,
stateRef: typeof isLoading | typeof isSaving
): Promise<T | null> => {
if (stateRef.value) return null;
stateRef.value = true;
try {
return await operation();
} finally {
stateRef.value = false;
}
};
// 加载文档
const loadDocument = () => withStateGuard(
async () => {
const loadDocument = async (): Promise<Document | null> => {
if (isLoading.value) return null;
isLoading.value = true;
try {
const doc = await DocumentService.GetActiveDocument();
activeDocument.value = doc;
return doc;
},
isLoading
);
} catch (error) {
return null;
} finally {
isLoading.value = false;
}
};
// 保存文档
const saveDocument = async (content: string): Promise<boolean> => {
const result = await withStateGuard(
async () => {
if (isSaving.value) return false;
isSaving.value = true;
try {
await DocumentService.UpdateActiveDocumentContent(content);
lastSaved.value = new Date();
// 使用可选链更新本地副本
// 更新本地副本
if (activeDocument.value) {
activeDocument.value.content = content;
activeDocument.value.meta.lastUpdated = lastSaved.value;
}
return true;
},
isSaving
);
return result ?? false;
} catch (error) {
return false;
} finally {
isSaving.value = false;
}
};
// 强制保存文档到磁盘
const forceSaveDocument = async (): Promise<boolean> => {
const result = await withStateGuard(
async () => {
// 直接调用强制保存API
await DocumentService.ForceSave();
if (isSaving.value) return false;
isSaving.value = true;
try {
await DocumentService.ForceSave();
lastSaved.value = new Date();
// 使用可选链更新时间戳
// 更新时间戳
if (activeDocument.value) {
activeDocument.value.meta.lastUpdated = lastSaved.value;
}
return true;
},
isSaving
);
return result ?? false;
} catch (error) {
return false;
} finally {
isSaving.value = false;
}
};
// 初始化
const initialize = async () => {
const initialize = async (): Promise<void> => {
await loadDocument();
};

View File

@@ -5,7 +5,6 @@ import {EditorState, Extension} from '@codemirror/state';
import {useConfigStore} from './configStore';
import {useDocumentStore} from './documentStore';
import {useThemeStore} from './themeStore';
import {useI18n} from 'vue-i18n';
import {SystemThemeType} from '@/../bindings/voidraft/internal/models/models';
import {DocumentService} from '@/../bindings/voidraft/internal/services';
import {ensureSyntaxTree} from "@codemirror/language"
@@ -28,7 +27,6 @@ export const useEditorStore = defineStore('editor', () => {
const configStore = useConfigStore();
const documentStore = useDocumentStore();
const themeStore = useThemeStore();
const { t } = useI18n();
// 状态
const documentStats = ref<DocumentStats>({
@@ -267,10 +265,7 @@ export const useEditorStore = defineStore('editor', () => {
editorContainer,
// 方法
setEditorView,
setEditorContainer,
updateDocumentStats,
applyFontSize,
createEditor,
reconfigureTabSettings,
reconfigureFontSettings,

View File

@@ -1,5 +1,5 @@
import {defineStore} from 'pinia';
import { ref, computed } from 'vue';
import {computed, ref} from 'vue';
import * as runtime from '@wailsio/runtime';
export interface SystemEnvironment {
@@ -50,8 +50,7 @@ export const useSystemStore = defineStore('system', () => {
error.value = null;
try {
const env = await runtime.System.Environment();
environment.value = env;
environment.value = await runtime.System.Environment();
} catch (err) {
error.value = 'Failed to get system environment';
environment.value = null;

View File

@@ -5,7 +5,12 @@ import {computed, onUnmounted, ref} from 'vue';
import SettingSection from '../components/SettingSection.vue';
import SettingItem from '../components/SettingItem.vue';
import ToggleSwitch from '../components/ToggleSwitch.vue';
import {DialogService, MigrationService, MigrationProgress, MigrationStatus} from '@/../bindings/voidraft/internal/services';
import {
DialogService,
MigrationProgress,
MigrationService,
MigrationStatus
} from '@/../bindings/voidraft/internal/services';
import * as runtime from '@wailsio/runtime';
const {t} = useI18n();
@@ -230,7 +235,7 @@ const currentDataPath = computed(() => configStore.config.general.dataPath);
const selectDataDirectory = async () => {
if (isMigrating.value) return;
try {
const selectedPath = await DialogService.SelectDirectory();
// 检查用户是否取消了选择或路径为空
@@ -266,9 +271,6 @@ const selectDataDirectory = async () => {
hideProgressTimer = setTimeout(hideProgress, 5000);
}
} catch (dialogError) {
console.error(dialogError);
}
};
// 清理定时器
@@ -292,7 +294,8 @@ onUnmounted(() => {
<div class="hotkey-controls">
<div class="hotkey-modifiers">
<label class="modifier-label" :class="{ active: modifierKeys.ctrl }" @click="toggleModifier('ctrl')">
<input type="checkbox" :checked="modifierKeys.ctrl" class="hidden-checkbox" :disabled="!enableGlobalHotkey">
<input type="checkbox" :checked="modifierKeys.ctrl" class="hidden-checkbox"
:disabled="!enableGlobalHotkey">
<span class="modifier-key">Ctrl</span>
</label>
<label class="modifier-label" :class="{ active: modifierKeys.shift }" @click="toggleModifier('shift')">
@@ -301,11 +304,13 @@ onUnmounted(() => {
<span class="modifier-key">Shift</span>
</label>
<label class="modifier-label" :class="{ active: modifierKeys.alt }" @click="toggleModifier('alt')">
<input type="checkbox" :checked="modifierKeys.alt" class="hidden-checkbox" :disabled="!enableGlobalHotkey">
<input type="checkbox" :checked="modifierKeys.alt" class="hidden-checkbox"
:disabled="!enableGlobalHotkey">
<span class="modifier-key">Alt</span>
</label>
<label class="modifier-label" :class="{ active: modifierKeys.win }" @click="toggleModifier('win')">
<input type="checkbox" :checked="modifierKeys.win" class="hidden-checkbox" :disabled="!enableGlobalHotkey">
<input type="checkbox" :checked="modifierKeys.win" class="hidden-checkbox"
:disabled="!enableGlobalHotkey">
<span class="modifier-key">Win</span>
</label>
</div>

1
go.mod
View File

@@ -5,6 +5,7 @@ go 1.23.0
toolchain go1.24.2
require (
github.com/Masterminds/semver/v3 v3.3.1
github.com/knadh/koanf/parsers/json v1.0.0
github.com/knadh/koanf/providers/file v1.2.0
github.com/knadh/koanf/providers/structs v1.0.0

2
go.sum
View File

@@ -1,5 +1,7 @@
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4=
github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=

View File

@@ -153,3 +153,23 @@ func NewDefaultAppConfig() *AppConfig {
},
}
}
// GetVersion 获取配置版本
func (ac *AppConfig) GetVersion() string {
return ac.Metadata.Version
}
// SetVersion 设置配置版本
func (ac *AppConfig) SetVersion(version string) {
ac.Metadata.Version = version
}
// SetLastUpdated 设置最后更新时间
func (ac *AppConfig) SetLastUpdated(timeStr string) {
ac.Metadata.LastUpdated = timeStr
}
// GetDefaultConfig 获取默认配置
func (ac *AppConfig) GetDefaultConfig() any {
return NewDefaultAppConfig()
}

View File

@@ -93,6 +93,7 @@ const (
// KeyBindingMetadata 快捷键配置元数据
type KeyBindingMetadata struct {
Version string `json:"version"` // 配置版本
LastUpdated string `json:"lastUpdated"` // 最后更新时间
}
@@ -107,6 +108,7 @@ func NewDefaultKeyBindingConfig() *KeyBindingConfig {
return &KeyBindingConfig{
KeyBindings: NewDefaultKeyBindings(),
Metadata: KeyBindingMetadata{
Version: "1.0.0",
LastUpdated: time.Now().Format(time.RFC3339),
},
}
@@ -504,3 +506,23 @@ func NewDefaultKeyBindings() []KeyBinding {
},
}
}
// GetVersion 获取配置版本
func (kbc *KeyBindingConfig) GetVersion() string {
return kbc.Metadata.Version
}
// SetVersion 设置配置版本
func (kbc *KeyBindingConfig) SetVersion(version string) {
kbc.Metadata.Version = version
}
// SetLastUpdated 设置最后更新时间
func (kbc *KeyBindingConfig) SetLastUpdated(timeStr string) {
kbc.Metadata.LastUpdated = timeStr
}
// GetDefaultConfig 获取默认配置
func (kbc *KeyBindingConfig) GetDefaultConfig() any {
return NewDefaultKeyBindingConfig()
}

View File

@@ -0,0 +1,325 @@
package services
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"reflect"
"sort"
"time"
"voidraft/internal/models"
"github.com/Masterminds/semver/v3"
jsonparser "github.com/knadh/koanf/parsers/json"
"github.com/knadh/koanf/providers/structs"
"github.com/knadh/koanf/v2"
"github.com/wailsapp/wails/v3/pkg/services/log"
)
const (
// CurrentAppConfigVersion 当前应用配置版本
CurrentAppConfigVersion = "1.0.0"
// CurrentKeyBindingConfigVersion 当前快捷键配置版本
CurrentKeyBindingConfigVersion = "1.0.0"
// BackupFilePattern 备份文件名模式
BackupFilePattern = "%s.backup.%s.json"
// 资源限制常量
MaxConfigFileSize = 10 * 1024 * 1024 // 10MB
MaxRecursionDepth = 50 // 最大递归深度
)
// Migratable 可迁移的配置接口
type Migratable interface {
GetVersion() string // 获取当前版本
SetVersion(string) // 设置版本
SetLastUpdated(string) // 设置最后更新时间
GetDefaultConfig() any // 获取默认配置
}
// ConfigMigrationService 配置迁移服务
type ConfigMigrationService[T Migratable] struct {
logger *log.LoggerService
pathManager *PathManager
configName string
targetVersion string
configPath string
}
// MigrationResult 迁移结果
type MigrationResult struct {
Migrated, ConfigUpdated bool
FromVersion, ToVersion string
BackupPath string
}
// NewConfigMigrationService 创建配置迁移服务
func NewConfigMigrationService[T Migratable](
logger *log.LoggerService,
pathManager *PathManager,
configName, targetVersion, configPath string,
) *ConfigMigrationService[T] {
return &ConfigMigrationService[T]{
logger: orDefault(logger, log.New()),
pathManager: orDefault(pathManager, NewPathManager()),
configName: configName,
targetVersion: targetVersion,
configPath: configPath,
}
}
// MigrateConfig 迁移配置文件
func (cms *ConfigMigrationService[T]) MigrateConfig(existingConfig *koanf.Koanf) (*MigrationResult, error) {
currentVersion := orDefault(existingConfig.String("metadata.version"), "0.0.0")
result := &MigrationResult{
FromVersion: currentVersion,
ToVersion: cms.targetVersion,
}
if needsMigration, err := cms.needsMigration(currentVersion); err != nil {
return result, fmt.Errorf("version comparison failed: %w", err)
} else if !needsMigration {
return result, nil
}
// 资源检查和备份
if err := cms.checkResourceLimits(); err != nil {
return result, fmt.Errorf("resource limit check failed: %w", err)
}
if backupPath, err := cms.createBackupOptimized(); err != nil {
return result, fmt.Errorf("backup creation failed: %w", err)
} else {
result.BackupPath = backupPath
}
// 自动恢复检查
cms.tryQuickRecovery(existingConfig)
// 执行迁移
if configUpdated, err := cms.performOptimizedMigration(existingConfig); err != nil {
return result, fmt.Errorf("migration failed: %w", err)
} else {
result.Migrated = true
result.ConfigUpdated = configUpdated
}
return result, nil
}
// needsMigration 检查是否需要迁移
func (cms *ConfigMigrationService[T]) needsMigration(current string) (bool, error) {
currentVer, err := semver.NewVersion(current)
if err != nil {
return true, nil
}
targetVer, err := semver.NewVersion(cms.targetVersion)
if err != nil {
return false, fmt.Errorf("invalid target version: %s", cms.targetVersion)
}
return currentVer.LessThan(targetVer), nil
}
// checkResourceLimits 检查资源限制
func (cms *ConfigMigrationService[T]) checkResourceLimits() error {
if info, err := os.Stat(cms.configPath); err == nil && info.Size() > MaxConfigFileSize {
return fmt.Errorf("config file size (%d bytes) exceeds limit (%d bytes)", info.Size(), MaxConfigFileSize)
}
return nil
}
// createBackupOptimized 优化的备份创建(单次扫描删除旧备份)
func (cms *ConfigMigrationService[T]) createBackupOptimized() (string, error) {
if _, err := os.Stat(cms.configPath); os.IsNotExist(err) {
return "", nil
}
configDir := cms.pathManager.GetConfigDir()
timestamp := time.Now().Format("20060102150405")
newBackupPath := filepath.Join(configDir, fmt.Sprintf(BackupFilePattern, cms.configName, timestamp))
// 单次扫描:删除旧备份并创建新备份
pattern := filepath.Join(configDir, fmt.Sprintf("%s.backup.*.json", cms.configName))
if matches, err := filepath.Glob(pattern); err == nil {
for _, oldBackup := range matches {
if oldBackup != newBackupPath {
os.Remove(oldBackup) // 忽略删除错误,继续处理
}
}
}
return newBackupPath, copyFile(cms.configPath, newBackupPath)
}
// tryQuickRecovery 快速恢复检查(避免完整的备份恢复)
func (cms *ConfigMigrationService[T]) tryQuickRecovery(existingConfig *koanf.Koanf) {
var testConfig T
if existingConfig.Unmarshal("", &testConfig) != nil {
cms.logger.Info("Config appears corrupted, attempting quick recovery")
if backupPath := cms.findLatestBackupQuick(); backupPath != "" {
if data, err := os.ReadFile(backupPath); err == nil {
existingConfig.Delete("")
existingConfig.Load(&BytesProvider{data}, jsonparser.Parser())
cms.logger.Info("Quick recovery successful")
}
}
}
}
// findLatestBackupQuick 快速查找最新备份(优化排序)
func (cms *ConfigMigrationService[T]) findLatestBackupQuick() string {
pattern := filepath.Join(cms.pathManager.GetConfigDir(), fmt.Sprintf("%s.backup.*.json", cms.configName))
matches, err := filepath.Glob(pattern)
if err != nil || len(matches) == 0 {
return ""
}
sort.Strings(matches) // 字典序排序,时间戳格式确保正确性
return matches[len(matches)-1]
}
// performOptimizedMigration 优化的迁移执行
func (cms *ConfigMigrationService[T]) performOptimizedMigration(existingConfig *koanf.Koanf) (bool, error) {
// 直接从koanf实例获取配置避免额外序列化
var currentConfig T
if err := existingConfig.Unmarshal("", &currentConfig); err != nil {
return false, fmt.Errorf("unmarshal existing config failed: %w", err)
}
defaultConfig, ok := currentConfig.GetDefaultConfig().(T)
if !ok {
return false, fmt.Errorf("default config type mismatch")
}
return cms.mergeInPlace(existingConfig, currentConfig, defaultConfig)
}
// mergeInPlace 就地合并配置
func (cms *ConfigMigrationService[T]) mergeInPlace(existingConfig *koanf.Koanf, currentConfig, defaultConfig T) (bool, error) {
// 创建临时合并实例
mergeKoanf := koanf.New(".")
// 使用快速加载链
if err := chainLoad(mergeKoanf,
func() error { return mergeKoanf.Load(structs.Provider(defaultConfig, "json"), nil) },
func() error {
return mergeKoanf.Load(structs.Provider(currentConfig, "json"), nil,
koanf.WithMergeFunc(cms.fastMerge))
},
); err != nil {
return false, fmt.Errorf("config merge failed: %w", err)
}
// 更新元数据(直接操作,无需重新序列化)
mergeKoanf.Set("metadata.version", cms.targetVersion)
mergeKoanf.Set("metadata.lastUpdated", time.Now().Format(time.RFC3339))
// 一次性序列化和原子写入
configBytes, err := mergeKoanf.Marshal(jsonparser.Parser())
if err != nil {
return false, fmt.Errorf("marshal config failed: %w", err)
}
if len(configBytes) > MaxConfigFileSize {
return false, fmt.Errorf("merged config size exceeds limit")
}
// 原子写入
return true, cms.atomicWrite(existingConfig, configBytes)
}
// atomicWrite 原子写入操作
func (cms *ConfigMigrationService[T]) atomicWrite(existingConfig *koanf.Koanf, configBytes []byte) error {
tempPath := cms.configPath + ".tmp"
if err := os.WriteFile(tempPath, configBytes, 0644); err != nil {
return fmt.Errorf("write temp config failed: %w", err)
}
if err := os.Rename(tempPath, cms.configPath); err != nil {
os.Remove(tempPath)
return fmt.Errorf("atomic rename failed: %w", err)
}
// 重新加载到原实例
existingConfig.Delete("")
return existingConfig.Load(&BytesProvider{configBytes}, jsonparser.Parser())
}
// fastMerge 快速合并函数(优化版本)
func (cms *ConfigMigrationService[T]) fastMerge(src, dest map[string]interface{}) error {
return cms.fastMergeRecursive(src, dest, 0)
}
// fastMergeRecursive 快速递归合并(单次遍历,最小化反射使用)
func (cms *ConfigMigrationService[T]) fastMergeRecursive(src, dest map[string]interface{}, depth int) error {
if depth > MaxRecursionDepth {
return fmt.Errorf("recursion depth exceeded")
}
for key, srcVal := range src {
if destVal, exists := dest[key]; exists {
// 优先检查map类型最常见情况
if srcMap, srcOK := srcVal.(map[string]interface{}); srcOK {
if destMap, destOK := destVal.(map[string]interface{}); destOK {
if err := cms.fastMergeRecursive(srcMap, destMap, depth+1); err != nil {
return err
}
continue
}
}
// 快速空值检查(避免反射)
if srcVal == nil || srcVal == "" || srcVal == 0 {
continue
}
}
dest[key] = srcVal
}
return nil
}
// BytesProvider 轻量字节提供器
type BytesProvider struct{ data []byte }
func (bp *BytesProvider) ReadBytes() ([]byte, error) { return bp.data, nil }
func (bp *BytesProvider) Read() (map[string]interface{}, error) {
var result map[string]interface{}
return result, json.Unmarshal(bp.data, &result)
}
// 工具函数
func orDefault[T any](value, defaultValue T) T {
var zero T
if reflect.DeepEqual(value, zero) {
return defaultValue
}
return value
}
func copyFile(src, dst string) error {
data, err := os.ReadFile(src)
if err != nil {
return err
}
return os.WriteFile(dst, data, 0644)
}
func chainLoad(k *koanf.Koanf, loaders ...func() error) error {
for _, loader := range loaders {
if err := loader(); err != nil {
return err
}
}
return nil
}
// 工厂函数
func NewAppConfigMigrationService(logger *log.LoggerService, pathManager *PathManager) *ConfigMigrationService[*models.AppConfig] {
return NewConfigMigrationService[*models.AppConfig](
logger, pathManager, "settings", CurrentAppConfigVersion, pathManager.GetSettingsPath())
}
func NewKeyBindingMigrationService(logger *log.LoggerService, pathManager *PathManager) *ConfigMigrationService[*models.KeyBindingConfig] {
return NewConfigMigrationService[*models.KeyBindingConfig](
logger, pathManager, "keybindings", CurrentKeyBindingConfigVersion, pathManager.GetKeybindsPath())
}

View File

@@ -25,6 +25,8 @@ type ConfigService struct {
// 配置通知服务
notificationService *ConfigNotificationService
// 配置迁移服务
migrationService *ConfigMigrationService[*models.AppConfig]
}
// ConfigError 配置错误
@@ -65,16 +67,18 @@ func NewConfigService(logger *log.LoggerService, pathManager *PathManager) *Conf
// 使用"."作为键路径分隔符
k := koanf.New(".")
notificationService := NewConfigNotificationService(k, logger)
migrationService := NewAppConfigMigrationService(logger, pathManager)
// 构造配置服务实例
service := &ConfigService{
koanf: k,
logger: logger,
pathManager: pathManager,
notificationService: notificationService,
migrationService: migrationService,
}
// 初始化配置通知服务
service.notificationService = NewConfigNotificationService(k, logger)
// 初始化配置
if err := service.initConfig(); err != nil {
panic(err)
@@ -108,12 +112,25 @@ func (cs *ConfigService) initConfig() error {
return cs.createDefaultConfig()
}
// 配置文件存在,直接加载
// 配置文件存在,先加载现有配置
cs.fileProvider = file.Provider(configPath)
if err := cs.koanf.Load(cs.fileProvider, jsonparser.Parser()); err != nil {
return &ConfigError{Operation: "load_config_file", Err: err}
}
// 检查并执行配置迁移
if cs.migrationService != nil {
result, err := cs.migrationService.MigrateConfig(cs.koanf)
if err != nil {
return &ConfigError{Operation: "migrate_config", Err: err}
}
if result.Migrated && result.ConfigUpdated {
// 迁移完成且配置已更新,重新创建文件提供器以监听新文件
cs.fileProvider = file.Provider(configPath)
}
}
return nil
}
@@ -176,7 +193,7 @@ func (cs *ConfigService) GetConfig() (*models.AppConfig, error) {
defer cs.mu.RUnlock()
var config models.AppConfig
if err := cs.koanf.Unmarshal("", &config); err != nil {
if err := cs.koanf.UnmarshalWithConf("", &config, koanf.UnmarshalConf{Tag: "json"}); err != nil {
return nil, &ConfigError{Operation: "unmarshal_config", Err: err}
}

View File

@@ -26,6 +26,9 @@ type KeyBindingService struct {
ctx context.Context
cancel context.CancelFunc
initOnce sync.Once
// 配置迁移服务
migrationService *ConfigMigrationService[*models.KeyBindingConfig]
}
// KeyBindingError 快捷键错误
@@ -64,12 +67,15 @@ func NewKeyBindingService(logger *log.LoggerService, pathManager *PathManager) *
k := koanf.New(".")
migrationService := NewKeyBindingMigrationService(logger, pathManager)
service := &KeyBindingService{
koanf: k,
logger: logger,
pathManager: pathManager,
ctx: ctx,
cancel: cancel,
migrationService: migrationService,
}
// 异步初始化
@@ -109,12 +115,25 @@ func (kbs *KeyBindingService) initConfig() error {
return kbs.createDefaultConfig()
}
// 配置文件存在,直接加载
// 配置文件存在,先加载现有配置
kbs.fileProvider = file.Provider(configPath)
if err := kbs.koanf.Load(kbs.fileProvider, jsonparser.Parser()); err != nil {
return &KeyBindingError{"load_config_file", "", err}
}
// 检查并执行配置迁移
if kbs.migrationService != nil {
result, err := kbs.migrationService.MigrateConfig(kbs.koanf)
if err != nil {
return &KeyBindingError{"migrate_config", "", err}
}
if result.Migrated && result.ConfigUpdated {
// 迁移完成且配置已更新,重新创建文件提供器以监听新文件
kbs.fileProvider = file.Provider(configPath)
}
}
return nil
}

View File

@@ -41,6 +41,11 @@ func (pm *PathManager) GetKeybindsPath() string {
return pm.keybindsPath
}
// GetConfigDir 获取配置目录路径
func (pm *PathManager) GetConfigDir() string {
return pm.configDir
}
// EnsureConfigDir 确保配置目录存在
func (pm *PathManager) EnsureConfigDir() error {
return os.MkdirAll(pm.configDir, 0755)