From 61f293ce6fd9bf5144b70807d1abeb13b131fea3 Mon Sep 17 00:00:00 2001 From: landaiqing Date: Sat, 7 Jun 2025 21:12:07 +0800 Subject: [PATCH] :sparkles: Improved settings --- frontend/bindings/time/models.ts | 36 - .../internal/services/configservice.ts | 2 +- .../internal/services/migrationservice.ts | 6 +- .../voidraft/internal/services/models.ts | 88 --- frontend/components.d.ts | 1 - .../migration/MigrationProgress.vue | 540 -------------- frontend/src/composables/useWebSocket.ts | 423 +++++++++++ frontend/src/i18n/locales/en-US.ts | 33 +- frontend/src/i18n/locales/zh-CN.ts | 35 +- frontend/src/stores/configStore.ts | 10 +- .../src/views/settings/pages/GeneralPage.vue | 676 +++++++++++++----- go.mod | 23 + go.sum | 64 ++ internal/services/config_service.go | 45 +- internal/services/http_service.go | 156 ++++ internal/services/migration_service.go | 255 ++----- internal/services/service_manager.go | 26 +- internal/services/websocket_service.go | 159 ++++ 18 files changed, 1480 insertions(+), 1098 deletions(-) delete mode 100644 frontend/src/components/migration/MigrationProgress.vue create mode 100644 frontend/src/composables/useWebSocket.ts create mode 100644 internal/services/http_service.go create mode 100644 internal/services/websocket_service.go diff --git a/frontend/bindings/time/models.ts b/frontend/bindings/time/models.ts index aca1ad1..6646738 100644 --- a/frontend/bindings/time/models.ts +++ b/frontend/bindings/time/models.ts @@ -5,42 +5,6 @@ // @ts-ignore: Unused imports import {Create as $Create} from "@wailsio/runtime"; -/** - * A Duration represents the elapsed time between two instants - * as an int64 nanosecond count. The representation limits the - * largest representable duration to approximately 290 years. - */ -export enum Duration { - /** - * The Go zero value for the underlying type of the enum. - */ - $zero = 0, - - minDuration = -9223372036854775808, - maxDuration = 9223372036854775807, - - /** - * Common durations. There is no definition for units of Day or larger - * to avoid confusion across daylight savings time zone transitions. - * - * To count the number of units in a [Duration], divide: - * - * second := time.Second - * fmt.Print(int64(second/time.Millisecond)) // prints 1000 - * - * To convert an integer number of units to a Duration, multiply: - * - * seconds := 10 - * fmt.Print(time.Duration(seconds)*time.Second) // prints 10s - */ - Nanosecond = 1, - Microsecond = 1000, - Millisecond = 1000000, - Second = 1000000000, - Minute = 60000000000, - Hour = 3600000000000, -}; - /** * A Time represents an instant in time with nanosecond precision. * diff --git a/frontend/bindings/voidraft/internal/services/configservice.ts b/frontend/bindings/voidraft/internal/services/configservice.ts index 24ab801..172a111 100644 --- a/frontend/bindings/voidraft/internal/services/configservice.ts +++ b/frontend/bindings/voidraft/internal/services/configservice.ts @@ -35,7 +35,7 @@ export function GetConfig(): Promise & { cancel(): vo } /** - * ResetConfig 重置为默认配置 + * ResetConfig 强制重置所有配置为默认值 */ export function ResetConfig(): Promise & { cancel(): void } { let $resultPromise = $Call.ByID(3593047389) as any; diff --git a/frontend/bindings/voidraft/internal/services/migrationservice.ts b/frontend/bindings/voidraft/internal/services/migrationservice.ts index b29a615..1605694 100644 --- a/frontend/bindings/voidraft/internal/services/migrationservice.ts +++ b/frontend/bindings/voidraft/internal/services/migrationservice.ts @@ -51,10 +51,10 @@ export function ServiceShutdown(): Promise & { cancel(): void } { } /** - * SetProgressCallback 设置进度回调 + * SetProgressBroadcaster 设置进度广播函数 */ -export function SetProgressCallback(callback: $models.MigrationProgressCallback): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(75752256, callback) as any; +export function SetProgressBroadcaster(broadcaster: any): Promise & { cancel(): void } { + let $resultPromise = $Call.ByID(3244071921, broadcaster) as any; return $resultPromise; } diff --git a/frontend/bindings/voidraft/internal/services/models.ts b/frontend/bindings/voidraft/internal/services/models.ts index 568167c..f848bad 100644 --- a/frontend/bindings/voidraft/internal/services/models.ts +++ b/frontend/bindings/voidraft/internal/services/models.ts @@ -5,10 +5,6 @@ // @ts-ignore: Unused imports import {Create as $Create} from "@wailsio/runtime"; -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore: Unused imports -import * as time$0 from "../../../time/models.js"; - /** * MemoryStats 内存统计信息 */ @@ -85,88 +81,24 @@ export class MigrationProgress { */ "status": MigrationStatus; - /** - * 当前处理的文件 - */ - "currentFile": string; - - /** - * 已处理文件数 - */ - "processedFiles": number; - - /** - * 总文件数 - */ - "totalFiles": number; - - /** - * 已处理字节数 - */ - "processedBytes": number; - - /** - * 总字节数 - */ - "totalBytes": number; - /** * 进度百分比 (0-100) */ "progress": number; - /** - * 状态消息 - */ - "message": string; - /** * 错误信息 */ "error"?: string; - /** - * 开始时间 - */ - "startTime": time$0.Time; - - /** - * 估计剩余时间 - */ - "estimatedTime": time$0.Duration; - /** Creates a new MigrationProgress instance. */ constructor($$source: Partial = {}) { if (!("status" in $$source)) { this["status"] = ("" as MigrationStatus); } - if (!("currentFile" in $$source)) { - this["currentFile"] = ""; - } - if (!("processedFiles" in $$source)) { - this["processedFiles"] = 0; - } - if (!("totalFiles" in $$source)) { - this["totalFiles"] = 0; - } - if (!("processedBytes" in $$source)) { - this["processedBytes"] = 0; - } - if (!("totalBytes" in $$source)) { - this["totalBytes"] = 0; - } if (!("progress" in $$source)) { this["progress"] = 0; } - if (!("message" in $$source)) { - this["message"] = ""; - } - if (!("startTime" in $$source)) { - this["startTime"] = null; - } - if (!("estimatedTime" in $$source)) { - this["estimatedTime"] = (0 as time$0.Duration); - } Object.assign(this, $$source); } @@ -180,11 +112,6 @@ export class MigrationProgress { } } -/** - * MigrationProgressCallback 进度回调函数类型 - */ -export type MigrationProgressCallback = any; - /** * MigrationStatus 迁移状态 */ @@ -194,16 +121,6 @@ export enum MigrationStatus { */ $zero = "", - /** - * 空闲状态 - */ - MigrationStatusIdle = "idle", - - /** - * 准备中 - */ - MigrationStatusPreparing = "preparing", - /** * 迁移中 */ @@ -218,9 +135,4 @@ export enum MigrationStatus { * 失败 */ MigrationStatusFailed = "failed", - - /** - * 取消 - */ - MigrationStatusCancelled = "cancelled", }; diff --git a/frontend/components.d.ts b/frontend/components.d.ts index bb1da7d..f8c07ae 100644 --- a/frontend/components.d.ts +++ b/frontend/components.d.ts @@ -9,7 +9,6 @@ export {} declare module 'vue' { export interface GlobalComponents { MemoryMonitor: typeof import('./src/components/monitor/MemoryMonitor.vue')['default'] - MigrationProgress: typeof import('./src/components/migration/MigrationProgress.vue')['default'] RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] Toolbar: typeof import('./src/components/toolbar/Toolbar.vue')['default'] diff --git a/frontend/src/components/migration/MigrationProgress.vue b/frontend/src/components/migration/MigrationProgress.vue deleted file mode 100644 index e3de665..0000000 --- a/frontend/src/components/migration/MigrationProgress.vue +++ /dev/null @@ -1,540 +0,0 @@ - - - - - \ No newline at end of file diff --git a/frontend/src/composables/useWebSocket.ts b/frontend/src/composables/useWebSocket.ts new file mode 100644 index 0000000..e967b99 --- /dev/null +++ b/frontend/src/composables/useWebSocket.ts @@ -0,0 +1,423 @@ +import { ref, onUnmounted, reactive, computed, watch, nextTick } from 'vue'; + +// 基础WebSocket消息接口 +interface WebSocketMessage { + type: string; + data: T; +} + +// 迁移进度接口(与后端保持一致) +interface MigrationProgress { + status: 'migrating' | 'completed' | 'failed'; + progress: number; // 0-100 + error?: string; +} + +// 连接状态枚举 +enum ConnectionState { + DISCONNECTED = 'disconnected', + CONNECTING = 'connecting', + CONNECTED = 'connected', + RECONNECTING = 'reconnecting', + ERROR = 'error' +} + +// WebSocket配置选项 +interface WebSocketOptions { + url?: string; + reconnectInterval?: number; + maxReconnectAttempts?: number; + debug?: boolean; + autoConnect?: boolean; + protocols?: string | string[]; + heartbeat?: { + enabled: boolean; + interval: number; + message: string; + }; +} + +// 消息处理器类型 +type MessageHandler = (data: T) => void; + +// 事件处理器映射 +interface EventHandlers { + [messageType: string]: MessageHandler[]; +} + +// 连接事件类型 +type ConnectionEventType = 'connect' | 'disconnect' | 'error' | 'reconnect'; +type ConnectionEventHandler = (event?: Event | CloseEvent | ErrorEvent) => void; + +export function useWebSocket(options: WebSocketOptions = {}) { + const { + url = 'ws://localhost:8899/ws/migration', + reconnectInterval = 3000, + maxReconnectAttempts = 10, + debug = false, + autoConnect = true, + protocols, + heartbeat = { enabled: false, interval: 30000, message: 'ping' } + } = options; + + // === 状态管理 === + const connectionState = ref(ConnectionState.DISCONNECTED); + const connectionError = ref(null); + const reconnectAttempts = ref(0); + const lastMessage = ref(null); + const messageHistory = ref([]); + + // 迁移进度状态(保持向后兼容) + const migrationProgress = reactive({ + status: 'completed', + progress: 0 + }); + + // === 计算属性 === + const isConnected = computed(() => connectionState.value === ConnectionState.CONNECTED); + const isConnecting = computed(() => + connectionState.value === ConnectionState.CONNECTING || + connectionState.value === ConnectionState.RECONNECTING + ); + const canReconnect = computed(() => reconnectAttempts.value < maxReconnectAttempts); + + // === 内部状态 === + let ws: WebSocket | null = null; + let reconnectTimer: number | null = null; + let heartbeatTimer: number | null = null; + let isManualDisconnect = false; + + // 事件处理器 + const eventHandlers: EventHandlers = {}; + const connectionEventHandlers: Map = new Map(); + + // === 工具函数 === + const log = (level: 'info' | 'warn' | 'error', message: string, ...args: any[]) => { + if (debug) { + console[level](`[WebSocket] ${message}`, ...args); + } + }; + + const clearTimers = () => { + if (reconnectTimer) { + clearTimeout(reconnectTimer); + reconnectTimer = null; + } + if (heartbeatTimer) { + clearInterval(heartbeatTimer); + heartbeatTimer = null; + } + }; + + const updateConnectionState = (newState: ConnectionState, error?: string) => { + connectionState.value = newState; + connectionError.value = error || null; + log('info', `Connection state changed to: ${newState}`, error); + }; + + // === 事件系统 === + const on = (messageType: string, handler: MessageHandler) => { + if (!eventHandlers[messageType]) { + eventHandlers[messageType] = []; + } + eventHandlers[messageType].push(handler as MessageHandler); + + // 返回取消订阅函数 + return () => off(messageType, handler); + }; + + const off = (messageType: string, handler: MessageHandler) => { + if (eventHandlers[messageType]) { + const index = eventHandlers[messageType].indexOf(handler as MessageHandler); + if (index > -1) { + eventHandlers[messageType].splice(index, 1); + } + } + }; + + const onConnection = (eventType: ConnectionEventType, handler: ConnectionEventHandler) => { + if (!connectionEventHandlers.has(eventType)) { + connectionEventHandlers.set(eventType, []); + } + connectionEventHandlers.get(eventType)!.push(handler); + + return () => offConnection(eventType, handler); + }; + + const offConnection = (eventType: ConnectionEventType, handler: ConnectionEventHandler) => { + const handlers = connectionEventHandlers.get(eventType); + if (handlers) { + const index = handlers.indexOf(handler); + if (index > -1) { + handlers.splice(index, 1); + } + } + }; + + const emit = (eventType: ConnectionEventType, event?: Event | CloseEvent | ErrorEvent) => { + const handlers = connectionEventHandlers.get(eventType); + if (handlers) { + handlers.forEach(handler => { + try { + handler(event); + } catch (error) { + log('error', `Error in ${eventType} event handler:`, error); + } + }); + } + }; + + // === 消息处理 === + const handleMessage = (event: MessageEvent) => { + try { + const message: WebSocketMessage = JSON.parse(event.data); + log('info', 'Received message:', message); + + // 更新消息历史 + lastMessage.value = message; + messageHistory.value.push(message); + + // 限制历史记录长度 + if (messageHistory.value.length > 100) { + messageHistory.value.shift(); + } + + // 特殊处理迁移进度消息(保持向后兼容) + if (message.type === 'migration_progress') { + Object.assign(migrationProgress, message.data); + } + + // 触发注册的处理器 + const handlers = eventHandlers[message.type]; + if (handlers) { + handlers.forEach(handler => { + try { + handler(message.data); + } catch (error) { + log('error', `Error in message handler for ${message.type}:`, error); + } + }); + } + } catch (error) { + log('error', 'Failed to parse message:', error, event.data); + } + }; + + // === 心跳机制 === + const startHeartbeat = () => { + if (!heartbeat.enabled || heartbeatTimer) return; + + heartbeatTimer = window.setInterval(() => { + if (isConnected.value) { + send(heartbeat.message); + } + }, heartbeat.interval); + }; + + const stopHeartbeat = () => { + if (heartbeatTimer) { + clearInterval(heartbeatTimer); + heartbeatTimer = null; + } + }; + + // === 连接管理 === + const connect = async (): Promise => { + if (isConnecting.value || isConnected.value) { + log('warn', 'Already connecting or connected'); + return; + } + + updateConnectionState(ConnectionState.CONNECTING); + isManualDisconnect = false; + + try { + log('info', 'Connecting to:', url); + + ws = new WebSocket(url, protocols); + + // 连接超时处理 + const connectTimeout = setTimeout(() => { + if (ws && ws.readyState === WebSocket.CONNECTING) { + ws.close(); + updateConnectionState(ConnectionState.ERROR, 'Connection timeout'); + } + }, 10000); + + ws.onopen = () => { + clearTimeout(connectTimeout); + log('info', 'Connected successfully'); + updateConnectionState(ConnectionState.CONNECTED); + reconnectAttempts.value = 0; + clearTimers(); + startHeartbeat(); + emit('connect'); + }; + + ws.onmessage = handleMessage; + + ws.onclose = (event) => { + clearTimeout(connectTimeout); + stopHeartbeat(); + log('info', 'Connection closed:', event.code, event.reason); + + const wasConnected = connectionState.value === ConnectionState.CONNECTED; + updateConnectionState(ConnectionState.DISCONNECTED); + ws = null; + + emit('disconnect', event); + + // 自动重连逻辑 + if (!isManualDisconnect && event.code !== 1000 && canReconnect.value) { + scheduleReconnect(); + } else if (reconnectAttempts.value >= maxReconnectAttempts) { + updateConnectionState(ConnectionState.ERROR, 'Max reconnection attempts reached'); + } + }; + + ws.onerror = (event) => { + clearTimeout(connectTimeout); + log('error', 'Connection error:', event); + updateConnectionState(ConnectionState.ERROR, 'WebSocket connection error'); + emit('error', event); + }; + + } catch (error) { + log('error', 'Failed to create WebSocket:', error); + updateConnectionState(ConnectionState.ERROR, 'Failed to create WebSocket connection'); + } + }; + + const disconnect = (code: number = 1000, reason: string = 'Manual disconnect') => { + isManualDisconnect = true; + clearTimers(); + stopHeartbeat(); + + if (ws) { + log('info', 'Disconnecting manually'); + ws.close(code, reason); + } + + updateConnectionState(ConnectionState.DISCONNECTED); + }; + + const scheduleReconnect = () => { + if (!canReconnect.value || isManualDisconnect) return; + + clearTimers(); + reconnectAttempts.value++; + updateConnectionState(ConnectionState.RECONNECTING, + `Reconnecting... (${reconnectAttempts.value}/${maxReconnectAttempts})`); + + log('info', `Scheduling reconnect attempt ${reconnectAttempts.value}/${maxReconnectAttempts} in ${reconnectInterval}ms`); + + reconnectTimer = window.setTimeout(() => { + connect(); + }, reconnectInterval); + + emit('reconnect'); + }; + + const reconnect = () => { + disconnect(); + reconnectAttempts.value = 0; + nextTick(() => { + connect(); + }); + }; + + // === 消息发送 === + const send = (message: any): boolean => { + if (!isConnected.value || !ws) { + log('warn', 'Cannot send message: not connected'); + return false; + } + + try { + const data = typeof message === 'string' ? message : JSON.stringify(message); + ws.send(data); + log('info', 'Sent message:', data); + return true; + } catch (error) { + log('error', 'Failed to send message:', error); + return false; + } + }; + + const sendMessage = (type: string, data?: T): boolean => { + return send({ type, data }); + }; + + // === 状态查询 === + const getConnectionInfo = () => ({ + state: connectionState.value, + error: connectionError.value, + reconnectAttempts: reconnectAttempts.value, + maxReconnectAttempts, + canReconnect: canReconnect.value, + url, + readyState: ws?.readyState, + protocol: ws?.protocol, + extensions: ws?.extensions + }); + + // === 初始化 === + if (autoConnect) { + nextTick(() => { + connect(); + }); + } + + // === 清理 === + onUnmounted(() => { + disconnect(); + }); + + // 监听连接状态变化,用于调试 + if (debug) { + watch(connectionState, (newState, oldState) => { + log('info', `State transition: ${oldState} -> ${newState}`); + }); + } + + return { + // === 状态(只读) === + connectionState: computed(() => connectionState.value), + isConnected, + isConnecting, + connectionError: computed(() => connectionError.value), + reconnectAttempts: computed(() => reconnectAttempts.value), + canReconnect, + lastMessage: computed(() => lastMessage.value), + messageHistory: computed(() => messageHistory.value), + + // === 向后兼容的状态 === + migrationProgress, + + // === 连接控制 === + connect, + disconnect, + reconnect, + + // === 消息发送 === + send, + sendMessage, + + // === 事件系统 === + on, + off, + onConnection, + offConnection, + + // === 工具方法 === + getConnectionInfo, + clearHistory: () => { + messageHistory.value = []; + lastMessage.value = null; + } + }; +} + +// 导出类型 +export type { WebSocketMessage, MigrationProgress, WebSocketOptions, MessageHandler, ConnectionEventType }; +export { ConnectionState }; \ No newline at end of file diff --git a/frontend/src/i18n/locales/en-US.ts b/frontend/src/i18n/locales/en-US.ts index 98cc573..1aa2e56 100644 --- a/frontend/src/i18n/locales/en-US.ts +++ b/frontend/src/i18n/locales/en-US.ts @@ -46,36 +46,12 @@ export default { saveSuccess: 'Save settings updated', saveFailed: 'Failed to update save settings' }, - migration: { - inProgress: 'Migrating data to new path...', - success: 'Data migration completed', - failed: 'Data migration failed' - } }, migration: { - title: 'Data Migration', - preparing: 'Preparing', + started: 'Starting data migration', migrating: 'Migrating', - completed: 'Completed', - failed: 'Failed', - cancelled: 'Cancelled', - idle: 'Idle', - currentFile: 'Current File', - files: 'Files', - size: 'Size', - timeRemaining: 'Time Remaining', - complete: 'Complete', - retry: 'Retry', - close: 'Close', - migrationInProgress: 'Migrating data to new path...', - migrationCompleted: 'Data migration completed', - migrationFailed: 'Data migration failed', - recursiveCopyError: 'Target path cannot be a subdirectory of source path, this would cause infinite recursive copying', - targetNotDirectory: 'Target path exists but is not a directory', - targetNotEmpty: 'Target directory is not empty, cannot migrate', - compressing: 'Compressing source directory...', - extracting: 'Extracting to target location...', - cleaning: 'Cleaning up source directory...' + completed: 'Migration Completed', + failed: 'Migration Failed' }, settings: { title: 'Settings', @@ -89,10 +65,11 @@ export default { comingSoon: 'Coming Soon...', save: 'Save', reset: 'Reset', + cancel: 'Cancel', dangerZone: 'Danger Zone', resetAllSettings: 'Reset All Settings', resetDescription: 'This will restore all settings to their default values. This action cannot be undone.', - confirmReset: 'Are you sure you want to reset all settings? This action cannot be undone.', + confirmReset: 'Click again to confirm reset', globalHotkey: 'Global Keyboard Shortcuts', enableGlobalHotkey: 'Enable Global Hotkeys', window: 'Window/Application', diff --git a/frontend/src/i18n/locales/zh-CN.ts b/frontend/src/i18n/locales/zh-CN.ts index 2f478aa..573682c 100644 --- a/frontend/src/i18n/locales/zh-CN.ts +++ b/frontend/src/i18n/locales/zh-CN.ts @@ -46,36 +46,12 @@ export default { saveSuccess: '保存设置已更新', saveFailed: '保存设置更新失败' }, - migration: { - inProgress: '正在迁移数据到新路径...', - success: '数据迁移完成', - failed: '数据迁移失败' - } }, migration: { - title: '数据迁移', - preparing: '准备中', - migrating: '迁移中', - completed: '已完成', - failed: '失败', - cancelled: '已取消', - idle: '空闲', - currentFile: '当前文件', - files: '文件', - size: '大小', - timeRemaining: '剩余时间', - complete: '完成', - retry: '重试', - close: '关闭', - migrationInProgress: '正在迁移数据到新路径...', - migrationCompleted: '数据迁移完成', - migrationFailed: '数据迁移失败', - recursiveCopyError: '目标路径不能是源路径的子目录,这会导致无限递归复制', - targetNotDirectory: '目标路径存在但不是目录', - targetNotEmpty: '目标目录不为空,无法迁移', - compressing: '正在压缩源目录...', - extracting: '正在解压到目标位置...', - cleaning: '正在清理源目录...' + started: '开始迁移数据', + migrating: '迁移中', + completed: '迁移已完成', + failed: '迁移失败' }, settings: { title: '设置', @@ -89,10 +65,11 @@ export default { comingSoon: '即将推出...', save: '保存', reset: '重置', + cancel: '取消', dangerZone: '危险操作', resetAllSettings: '重置所有设置', resetDescription: '这将恢复所有设置为默认值,此操作无法撤销', - confirmReset: '确定要重置所有设置吗?此操作无法撤销。', + confirmReset: '再次点击确认重置', globalHotkey: '全局键盘快捷键', enableGlobalHotkey: '启用全局热键', window: '窗口/应用程序', diff --git a/frontend/src/stores/configStore.ts b/frontend/src/stores/configStore.ts index da57585..7b9d0ce 100644 --- a/frontend/src/stores/configStore.ts +++ b/frontend/src/stores/configStore.ts @@ -270,8 +270,16 @@ export const useConfigStore = defineStore('config', () => { state.isLoading = true; try { + // 调用后端重置配置 await safeCall(() => ConfigService.ResetConfig(), 'config.resetFailed', 'config.resetSuccess'); - await safeCall(() => initConfig(), 'config.loadFailed', 'config.loadSuccess'); + + // 立即重新加载后端配置以确保前端状态同步 + await safeCall(async () => { + const appConfig = await ConfigService.GetConfig(); + if (appConfig) { + state.config = JSON.parse(JSON.stringify(appConfig)) as AppConfig; + } + }, 'config.loadFailed', 'config.loadSuccess'); } finally { state.isLoading = false; } diff --git a/frontend/src/views/settings/pages/GeneralPage.vue b/frontend/src/views/settings/pages/GeneralPage.vue index 101236d..ead8220 100644 --- a/frontend/src/views/settings/pages/GeneralPage.vue +++ b/frontend/src/views/settings/pages/GeneralPage.vue @@ -1,21 +1,164 @@ \ No newline at end of file diff --git a/go.mod b/go.mod index 897348c..aba22ef 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,8 @@ toolchain go1.24.2 require ( github.com/fsnotify/fsnotify v1.9.0 + github.com/gin-gonic/gin v1.10.1 + github.com/lxzan/gws v1.8.9 github.com/spf13/viper v1.20.1 github.com/wailsapp/wails/v3 v3.0.0-alpha.9 ) @@ -16,26 +18,43 @@ require ( github.com/ProtonMail/go-crypto v1.3.0 // indirect github.com/adrg/xdg v0.5.3 // indirect github.com/bep/debounce v1.2.1 // indirect + github.com/bytedance/sonic v1.11.6 // indirect + github.com/bytedance/sonic/loader v0.1.1 // indirect github.com/cloudflare/circl v1.6.1 // indirect + github.com/cloudwego/base64x v0.1.4 // indirect + github.com/cloudwego/iasm v0.2.0 // indirect github.com/cyphar/filepath-securejoin v0.4.1 // indirect + github.com/dolthub/maphash v0.1.0 // indirect github.com/ebitengine/purego v0.8.4 // indirect github.com/emirpasic/gods v1.18.1 // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.6.2 // indirect github.com/go-git/go-git/v5 v5.16.0 // indirect github.com/go-ole/go-ole v1.3.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.20.0 // indirect github.com/go-viper/mapstructure/v2 v2.2.1 // indirect + github.com/goccy/go-json v0.10.2 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/google/uuid v1.6.0 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 // indirect + github.com/json-iterator/go v1.1.12 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect + github.com/klauspost/compress v1.17.11 // indirect + github.com/klauspost/cpuid/v2 v2.2.7 // indirect github.com/leaanthony/go-ansi-parser v1.6.1 // indirect github.com/leaanthony/u v1.1.1 // indirect + github.com/leodido/go-urn v1.4.0 // indirect github.com/lmittmann/tint v1.1.1 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pjbgf/sha1cd v0.3.2 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect @@ -49,15 +68,19 @@ require ( github.com/spf13/cast v1.9.2 // indirect github.com/spf13/pflag v1.0.6 // indirect github.com/subosito/gotenv v1.6.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect github.com/wailsapp/go-webview2 v1.0.21 // indirect github.com/wailsapp/mimetype v1.4.1 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect go.uber.org/multierr v1.11.0 // indirect + golang.org/x/arch v0.8.0 // indirect golang.org/x/crypto v0.38.0 // indirect golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect golang.org/x/net v0.40.0 // indirect golang.org/x/sys v0.33.0 // indirect golang.org/x/text v0.25.0 // indirect + google.golang.org/protobuf v1.34.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 3682ddb..ad5f06f 100644 --- a/go.sum +++ b/go.sum @@ -13,13 +13,23 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPd github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= +github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= +github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= +github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= +github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= +github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dolthub/maphash v0.1.0 h1:bsQ7JsF4FkkWyrP3oCnFJgrCUAFbFf3kOl4L/QxPDyQ= +github.com/dolthub/maphash v0.1.0/go.mod h1:gkg4Ch4CdCDu5h6PMriVLawB7koZ+5ijb9puGMV50a4= github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw= github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= @@ -30,6 +40,12 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ= +github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= @@ -42,22 +58,41 @@ github.com/go-git/go-git/v5 v5.16.0 h1:k3kuOEpkc0DeY7xlL6NaaNg39xdgQbtH5mwCafHO9 github.com/go-git/go-git/v5 v5.16.0/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= +github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 h1:njuLRcjAuMKr7kI3D85AXWkw6/+v9PwtV6M6o11sWHQ= github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= +github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= +github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -69,14 +104,23 @@ github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU= github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M= github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/lmittmann/tint v1.1.1 h1:xmmGuinUsCSxWdwH1OqMUQ4tzQsq3BdjJLAAmVKJ9Dw= github.com/lmittmann/tint v1.1.1/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= +github.com/lxzan/gws v1.8.9 h1:VU3SGUeWlQrEwfUSfokcZep8mdg/BrUF+y73YYshdBM= +github.com/lxzan/gws v1.8.9/go.mod h1:d9yHaR1eDTBHagQC6KY7ycUOaz5KWeqQtP3xu7aMK8Y= github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= @@ -114,12 +158,23 @@ github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/wailsapp/go-webview2 v1.0.21 h1:k3dtoZU4KCoN/AEIbWiPln3P2661GtA2oEgA2Pb+maA= github.com/wailsapp/go-webview2 v1.0.21/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc= github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs= @@ -130,6 +185,9 @@ github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= +golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= @@ -147,6 +205,7 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= @@ -157,6 +216,8 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= +google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= @@ -167,5 +228,8 @@ gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/internal/services/config_service.go b/internal/services/config_service.go index 8c98da8..0a203dd 100644 --- a/internal/services/config_service.go +++ b/internal/services/config_service.go @@ -229,21 +229,48 @@ func (cs *ConfigService) Get(key string) interface{} { return cs.viper.Get(key) } -// ResetConfig 重置为默认配置 -func (cs *ConfigService) ResetConfig() error { +// ResetConfig 强制重置所有配置为默认值 +func (cs *ConfigService) ResetConfig() { cs.mu.Lock() defer cs.mu.Unlock() - // 重新设置默认值 - setDefaults(cs.viper) + defaultConfig := models.NewDefaultAppConfig() - // 使用 WriteConfig 写入配置文件(会触发文件监听) + // 通用设置 - 批量设置到viper中 + cs.viper.Set("general.always_on_top", defaultConfig.General.AlwaysOnTop) + cs.viper.Set("general.data_path", defaultConfig.General.DataPath) + cs.viper.Set("general.enable_global_hotkey", defaultConfig.General.EnableGlobalHotkey) + cs.viper.Set("general.global_hotkey.ctrl", defaultConfig.General.GlobalHotkey.Ctrl) + cs.viper.Set("general.global_hotkey.shift", defaultConfig.General.GlobalHotkey.Shift) + cs.viper.Set("general.global_hotkey.alt", defaultConfig.General.GlobalHotkey.Alt) + cs.viper.Set("general.global_hotkey.win", defaultConfig.General.GlobalHotkey.Win) + cs.viper.Set("general.global_hotkey.key", defaultConfig.General.GlobalHotkey.Key) + + // 编辑设置 - 批量设置到viper中 + cs.viper.Set("editing.font_size", defaultConfig.Editing.FontSize) + cs.viper.Set("editing.font_family", defaultConfig.Editing.FontFamily) + cs.viper.Set("editing.font_weight", defaultConfig.Editing.FontWeight) + cs.viper.Set("editing.line_height", defaultConfig.Editing.LineHeight) + cs.viper.Set("editing.enable_tab_indent", defaultConfig.Editing.EnableTabIndent) + cs.viper.Set("editing.tab_size", defaultConfig.Editing.TabSize) + cs.viper.Set("editing.tab_type", defaultConfig.Editing.TabType) + cs.viper.Set("editing.auto_save_delay", defaultConfig.Editing.AutoSaveDelay) + + // 外观设置 - 批量设置到viper中 + cs.viper.Set("appearance.language", defaultConfig.Appearance.Language) + + // 元数据 - 批量设置到viper中 + cs.viper.Set("metadata.version", defaultConfig.Metadata.Version) + cs.viper.Set("metadata.last_updated", time.Now()) + + // 一次性写入配置文件,触发配置变更通知 if err := cs.viper.WriteConfig(); err != nil { - return &ConfigError{Operation: "reset_config", Err: err} + cs.logger.Error("Config: Failed to write config during reset", "error", err) + } else { + cs.logger.Info("Config: All settings have been reset to defaults") + // 手动触发配置变更检查,确保通知系统能感知到变更 + cs.notificationService.CheckConfigChanges() } - - cs.logger.Info("Config: Successfully reset to default configuration") - return nil } // SetHotkeyChangeCallback 设置热键配置变更回调 diff --git a/internal/services/http_service.go b/internal/services/http_service.go new file mode 100644 index 0000000..6d8074a --- /dev/null +++ b/internal/services/http_service.go @@ -0,0 +1,156 @@ +package services + +import ( + "context" + "errors" + "net/http" + "time" + + "github.com/gin-gonic/gin" + "github.com/wailsapp/wails/v3/pkg/services/log" +) + +// HTTPService HTTP 服务 +type HTTPService struct { + logger *log.LoggerService + server *http.Server + wsService *WebSocketService +} + +// NewHTTPService 创建 HTTP 服务 +func NewHTTPService(logger *log.LoggerService, wsService *WebSocketService) *HTTPService { + if logger == nil { + logger = log.New() + } + + return &HTTPService{ + logger: logger, + wsService: wsService, + } +} + +// StartServer 启动 HTTP 服务器 +func (hs *HTTPService) StartServer(port string) error { + // 设置 Gin 为发布模式 + gin.SetMode(gin.ReleaseMode) + + // 创建 Gin 路由器 + router := gin.New() + + // 添加中间件 + router.Use(gin.Recovery()) + router.Use(hs.corsMiddleware()) + + // 设置路由 + hs.setupRoutes(router) + + // 创建 HTTP 服务器 + hs.server = &http.Server{ + Addr: ":" + port, + Handler: router, + ReadTimeout: 30 * time.Second, + WriteTimeout: 30 * time.Second, + IdleTimeout: 60 * time.Second, + MaxHeaderBytes: 1 << 20, // 1MB + } + + hs.logger.Info("HTTP: Starting server", "port", port) + + // 启动服务器 + go func() { + if err := hs.server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { + hs.logger.Error("HTTP: Server failed to start", "error", err) + } + }() + + return nil +} + +// setupRoutes 设置路由 +func (hs *HTTPService) setupRoutes(router *gin.Engine) { + // WebSocket 端点 + router.GET("/ws/migration", hs.handleWebSocket) + + // API 端点组 + api := router.Group("/api") + { + api.GET("/health", hs.handleHealth) + api.GET("/ws/clients", hs.handleWSClients) + } +} + +// handleWebSocket 处理 WebSocket 连接 +func (hs *HTTPService) handleWebSocket(c *gin.Context) { + if hs.wsService == nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "WebSocket service not available"}) + return + } + + hs.wsService.HandleUpgrade(c.Writer, c.Request) +} + +// handleHealth 健康检查端点 +func (hs *HTTPService) handleHealth(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "status": "ok", + "timestamp": time.Now().Unix(), + "service": "voidraft-http", + }) +} + +// handleWSClients 获取 WebSocket 客户端数量 +func (hs *HTTPService) handleWSClients(c *gin.Context) { + if hs.wsService == nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "WebSocket service not available"}) + return + } + + count := hs.wsService.GetConnectedClientsCount() + c.JSON(http.StatusOK, gin.H{ + "connected_clients": count, + }) +} + +// corsMiddleware CORS 中间件 +func (hs *HTTPService) corsMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + c.Header("Access-Control-Allow-Origin", "*") + c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") + c.Header("Access-Control-Allow-Headers", "Origin, Content-Type, Accept, Authorization") + c.Header("Access-Control-Allow-Credentials", "true") + + if c.Request.Method == "OPTIONS" { + c.AbortWithStatus(204) + return + } + + c.Next() + } +} + +// StopServer 停止 HTTP 服务器 +func (hs *HTTPService) StopServer() error { + if hs.server == nil { + return nil + } + + hs.logger.Info("HTTP: Stopping server...") + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + return hs.server.Shutdown(ctx) +} + +// ServiceShutdown 服务关闭 +func (hs *HTTPService) ServiceShutdown() error { + hs.logger.Info("HTTP: Service is shutting down...") + + if err := hs.StopServer(); err != nil { + hs.logger.Error("HTTP: Failed to stop server", "error", err) + return err + } + + hs.logger.Info("HTTP: Service shutdown completed") + return nil +} diff --git a/internal/services/migration_service.go b/internal/services/migration_service.go index 1ca98cc..2e1af67 100644 --- a/internal/services/migration_service.go +++ b/internal/services/migration_service.go @@ -3,7 +3,6 @@ package services import ( "archive/zip" "context" - "errors" "fmt" "io" "os" @@ -19,40 +18,26 @@ import ( type MigrationStatus string const ( - MigrationStatusIdle MigrationStatus = "idle" // 空闲状态 - MigrationStatusPreparing MigrationStatus = "preparing" // 准备中 MigrationStatusMigrating MigrationStatus = "migrating" // 迁移中 MigrationStatusCompleted MigrationStatus = "completed" // 完成 MigrationStatusFailed MigrationStatus = "failed" // 失败 - MigrationStatusCancelled MigrationStatus = "cancelled" // 取消 ) // MigrationProgress 迁移进度信息 type MigrationProgress struct { - Status MigrationStatus `json:"status"` // 迁移状态 - CurrentFile string `json:"currentFile"` // 当前处理的文件 - ProcessedFiles int `json:"processedFiles"` // 已处理文件数 - TotalFiles int `json:"totalFiles"` // 总文件数 - ProcessedBytes int64 `json:"processedBytes"` // 已处理字节数 - TotalBytes int64 `json:"totalBytes"` // 总字节数 - Progress float64 `json:"progress"` // 进度百分比 (0-100) - Message string `json:"message"` // 状态消息 - Error string `json:"error,omitempty"` // 错误信息 - StartTime time.Time `json:"startTime"` // 开始时间 - EstimatedTime time.Duration `json:"estimatedTime"` // 估计剩余时间 + Status MigrationStatus `json:"status"` // 迁移状态 + Progress float64 `json:"progress"` // 进度百分比 (0-100) + Error string `json:"error,omitempty"` // 错误信息 } -// MigrationProgressCallback 进度回调函数类型 -type MigrationProgressCallback func(progress MigrationProgress) - // MigrationService 迁移服务 type MigrationService struct { - logger *log.LoggerService - mu sync.RWMutex - currentProgress MigrationProgress - progressCallback MigrationProgressCallback - cancelFunc context.CancelFunc - ctx context.Context + logger *log.LoggerService + mu sync.RWMutex + currentProgress MigrationProgress + cancelFunc context.CancelFunc + ctx context.Context + progressBroadcaster func(MigrationProgress) // WebSocket广播函数 } // NewMigrationService 创建迁移服务 @@ -64,18 +49,12 @@ func NewMigrationService(logger *log.LoggerService) *MigrationService { return &MigrationService{ logger: logger, currentProgress: MigrationProgress{ - Status: MigrationStatusIdle, + Status: MigrationStatusCompleted, // 初始状态为完成 + Progress: 0, }, } } -// SetProgressCallback 设置进度回调 -func (ms *MigrationService) SetProgressCallback(callback MigrationProgressCallback) { - ms.mu.Lock() - defer ms.mu.Unlock() - ms.progressCallback = callback -} - // GetProgress 获取当前进度 func (ms *MigrationService) GetProgress() MigrationProgress { ms.mu.RLock() @@ -83,15 +62,15 @@ func (ms *MigrationService) GetProgress() MigrationProgress { return ms.currentProgress } -// updateProgress 更新进度并触发回调 +// updateProgress 更新进度 func (ms *MigrationService) updateProgress(progress MigrationProgress) { ms.mu.Lock() ms.currentProgress = progress - callback := ms.progressCallback ms.mu.Unlock() - if callback != nil { - callback(progress) + // 通过WebSocket广播进度 + if ms.progressBroadcaster != nil { + ms.progressBroadcaster(progress) } } @@ -111,22 +90,18 @@ func (ms *MigrationService) MigrateDirectory(srcPath, dstPath string) error { ms.mu.Unlock() }() - ms.logger.Info("Migration: Starting directory migration", - "from", srcPath, - "to", dstPath) + ms.logger.Info("Migration: Starting directory migration", "from", srcPath, "to", dstPath) // 初始化进度 progress := MigrationProgress{ - Status: MigrationStatusPreparing, - Message: "Preparing migration...", - StartTime: time.Now(), + Status: MigrationStatusMigrating, + Progress: 0, } ms.updateProgress(progress) // 检查源目录是否存在 if _, err := os.Stat(srcPath); os.IsNotExist(err) { progress.Status = MigrationStatusCompleted - progress.Message = "Source directory does not exist, skipping migration" progress.Progress = 100 ms.updateProgress(progress) return nil @@ -137,69 +112,38 @@ func (ms *MigrationService) MigrateDirectory(srcPath, dstPath string) error { dstAbs, _ := filepath.Abs(dstPath) if srcAbs == dstAbs { progress.Status = MigrationStatusCompleted - progress.Message = "Paths are identical, no migration needed" progress.Progress = 100 ms.updateProgress(progress) return nil } - // 检查目标路径是否是源路径的子目录,防止无限递归复制 + // 检查目标路径是否是源路径的子目录 if ms.isSubDirectory(srcAbs, dstAbs) { progress.Status = MigrationStatusFailed - progress.Error = "Target path cannot be a subdirectory of source path, this would cause infinite recursive copying" + progress.Error = "Target path cannot be a subdirectory of source path" ms.updateProgress(progress) - return fmt.Errorf("target path cannot be a subdirectory of source path: src=%s, dst=%s", srcAbs, dstAbs) + return fmt.Errorf("target path cannot be a subdirectory of source path") } - // 计算目录大小(用于显示进度) - totalFiles, totalBytes, err := ms.calculateDirectorySize(ctx, srcPath) - if err != nil { - progress.Status = MigrationStatusFailed - progress.Error = fmt.Sprintf("Failed to calculate directory size: %v", err) - ms.updateProgress(progress) - return err - } - - progress.TotalFiles = totalFiles - progress.TotalBytes = totalBytes - progress.Status = MigrationStatusMigrating - progress.Message = "Starting atomic migration..." - ms.updateProgress(progress) - // 执行原子迁移 - err = ms.atomicMove(ctx, srcPath, dstPath, &progress) + err := ms.atomicMove(ctx, srcPath, dstPath, &progress) if err != nil { - if errors.Is(ctx.Err(), context.Canceled) { - progress.Status = MigrationStatusCancelled - progress.Error = "Migration cancelled" - } else { - progress.Status = MigrationStatusFailed - progress.Error = fmt.Sprintf("Migration failed: %v", err) - } + progress.Status = MigrationStatusFailed + progress.Error = err.Error() ms.updateProgress(progress) return err } // 迁移完成 progress.Status = MigrationStatusCompleted - progress.Message = "Migration completed" progress.Progress = 100 - progress.ProcessedFiles = totalFiles - progress.ProcessedBytes = totalBytes - duration := time.Since(progress.StartTime) ms.updateProgress(progress) - ms.logger.Info("Migration: Directory migration completed", - "from", srcPath, - "to", dstPath, - "duration", duration, - "files", totalFiles, - "bytes", totalBytes) - + ms.logger.Info("Migration: Directory migration completed", "from", srcPath, "to", dstPath) return nil } -// atomicMove 原子移动目录 - 使用压缩-移动-解压的方式 +// atomicMove 原子移动目录 func (ms *MigrationService) atomicMove(ctx context.Context, srcPath, dstPath string, progress *MigrationProgress) error { // 检查是否取消 select { @@ -211,125 +155,98 @@ func (ms *MigrationService) atomicMove(ctx context.Context, srcPath, dstPath str // 确保目标目录的父目录存在 dstParent := filepath.Dir(dstPath) if err := os.MkdirAll(dstParent, 0755); err != nil { - return fmt.Errorf("Failed to create target parent directory: %v", err) + return fmt.Errorf("Failed to create target parent directory") } // 检查目标路径情况 if stat, err := os.Stat(dstPath); err == nil { if !stat.IsDir() { - return fmt.Errorf("Target path exists but is not a directory: %s", dstPath) + return fmt.Errorf("Target path exists but is not a directory") } - - // 检查目录是否为空 isEmpty, err := ms.isDirectoryEmpty(dstPath) if err != nil { - return fmt.Errorf("Failed to check if target directory is empty: %v", err) + return fmt.Errorf("Failed to check target directory") } - if !isEmpty { - return fmt.Errorf("Target directory is not empty: %s", dstPath) + return fmt.Errorf("Target directory is not empty") } - - // 目录存在但为空,可以继续迁移 - ms.logger.Info("Migration: Target directory exists but is empty, proceeding with migration") } // 尝试直接重命名(如果在同一分区,这会很快) - progress.Message = "Attempting fast move..." + progress.Progress = 20 ms.updateProgress(*progress) if err := os.Rename(srcPath, dstPath); err == nil { - // 重命名成功,这是最快的方式 ms.logger.Info("Migration: Fast rename successful") return nil + } else { + ms.logger.Info("Migration: Fast rename failed, using copy method", "error", err) } - // 重命名失败(可能跨分区),使用原子压缩迁移 - progress.Message = "Starting atomic compress migration..." + // 重命名失败,使用压缩迁移 + progress.Progress = 30 ms.updateProgress(*progress) return ms.atomicCompressMove(ctx, srcPath, dstPath, progress) } -// atomicCompressMove 原子压缩迁移 - 压缩、移动、解压、清理 +// atomicCompressMove 原子压缩迁移 func (ms *MigrationService) atomicCompressMove(ctx context.Context, srcPath, dstPath string, progress *MigrationProgress) error { - // 创建临时压缩文件 tempDir := os.TempDir() tempZipFile := filepath.Join(tempDir, fmt.Sprintf("voidraft_migration_%d.zip", time.Now().UnixNano())) - // 确保临时文件在函数结束时被清理 defer func() { if err := os.Remove(tempZipFile); err != nil && !os.IsNotExist(err) { - ms.logger.Error("Migration: Failed to clean up temporary zip file", "file", tempZipFile, "error", err) + ms.logger.Error("Migration: Failed to clean up temporary zip file", "error", err) } }() - // 第一步: 压缩源目录 - progress.Message = "Compressing source directory..." - progress.Progress = 10 + // 压缩源目录 + progress.Progress = 40 ms.updateProgress(*progress) - if err := ms.compressDirectory(ctx, srcPath, tempZipFile, progress); err != nil { - return fmt.Errorf("Failed to compress source directory: %v", err) + if err := ms.compressDirectory(ctx, srcPath, tempZipFile); err != nil { + return fmt.Errorf("Failed to compress source directory") } - // 检查是否取消 - select { - case <-ctx.Done(): - return ctx.Err() - default: - } - - // 第二步: 解压到目标位置 - progress.Message = "Extracting to target location..." + // 解压到目标位置 progress.Progress = 70 ms.updateProgress(*progress) - if err := ms.extractToDirectory(ctx, tempZipFile, dstPath, progress); err != nil { - return fmt.Errorf("Failed to extract to target location: %v", err) + if err := ms.extractToDirectory(ctx, tempZipFile, dstPath); err != nil { + return fmt.Errorf("Failed to extract to target location") } // 检查是否取消 select { case <-ctx.Done(): - // 如果取消,需要清理已解压的目标目录 os.RemoveAll(dstPath) return ctx.Err() default: } - // 第三步: 删除源目录 - progress.Message = "Cleaning up source directory..." + // 删除源目录 progress.Progress = 90 ms.updateProgress(*progress) if err := os.RemoveAll(srcPath); err != nil { ms.logger.Error("Migration: Failed to remove source directory", "error", err) - // 不返回错误,因为迁移已经成功 } - progress.Message = "Migration completed" - progress.Progress = 100 - ms.updateProgress(*progress) - - ms.logger.Info("Migration: Atomic compress-move completed successfully") return nil } // compressDirectory 压缩目录到zip文件 -func (ms *MigrationService) compressDirectory(ctx context.Context, srcDir, zipFile string, progress *MigrationProgress) error { - // 创建zip文件 +func (ms *MigrationService) compressDirectory(ctx context.Context, srcDir, zipFile string) error { zipWriter, err := os.Create(zipFile) if err != nil { - return fmt.Errorf("Failed to create zip file: %v", err) + return fmt.Errorf("Failed to create temporary file") } defer zipWriter.Close() - // 创建zip writer zw := zip.NewWriter(zipWriter) defer zw.Close() - // 遍历源目录并添加到zip return filepath.Walk(srcDir, func(filePath string, info os.FileInfo, err error) error { if err != nil { return err @@ -342,31 +259,22 @@ func (ms *MigrationService) compressDirectory(ctx context.Context, srcDir, zipFi default: } - // 计算相对路径 relPath, err := filepath.Rel(srcDir, filePath) if err != nil { return err } - // 跳过根目录 if relPath == "." { return nil } - // 更新当前处理的文件 - progress.CurrentFile = relPath - ms.updateProgress(*progress) - - // 创建zip中的文件头 header, err := zip.FileInfoHeader(info) if err != nil { return err } - // 使用/作为路径分隔符(zip标准) header.Name = strings.ReplaceAll(relPath, string(filepath.Separator), "/") - // 处理目录 if info.IsDir() { header.Name += "/" header.Method = zip.Store @@ -374,13 +282,11 @@ func (ms *MigrationService) compressDirectory(ctx context.Context, srcDir, zipFi header.Method = zip.Deflate } - // 写入zip文件头 writer, err := zw.CreateHeader(header) if err != nil { return err } - // 如果是文件,复制内容 if !info.IsDir() { file, err := os.Open(filePath) if err != nil { @@ -399,20 +305,17 @@ func (ms *MigrationService) compressDirectory(ctx context.Context, srcDir, zipFi } // extractToDirectory 从zip文件解压到目录 -func (ms *MigrationService) extractToDirectory(ctx context.Context, zipFile, dstDir string, progress *MigrationProgress) error { - // 打开zip文件 +func (ms *MigrationService) extractToDirectory(ctx context.Context, zipFile, dstDir string) error { reader, err := zip.OpenReader(zipFile) if err != nil { - return fmt.Errorf("Failed to open zip file: %v", err) + return fmt.Errorf("Failed to open temporary file") } defer reader.Close() - // 确保目标目录存在 if err := os.MkdirAll(dstDir, 0755); err != nil { - return fmt.Errorf("Failed to create target directory: %v", err) + return fmt.Errorf("Failed to create target directory") } - // 解压每个文件 for _, file := range reader.File { // 检查是否取消 select { @@ -421,19 +324,13 @@ func (ms *MigrationService) extractToDirectory(ctx context.Context, zipFile, dst default: } - // 更新当前处理的文件 - progress.CurrentFile = file.Name - ms.updateProgress(*progress) - - // 构建目标文件路径 dstPath := filepath.Join(dstDir, file.Name) // 安全检查:防止zip slip攻击 if !strings.HasPrefix(dstPath, filepath.Clean(dstDir)+string(os.PathSeparator)) { - return fmt.Errorf("Invalid file path: %s", file.Name) + return fmt.Errorf("Invalid file path in archive") } - // 处理目录 if file.FileInfo().IsDir() { if err := os.MkdirAll(dstPath, file.FileInfo().Mode()); err != nil { return err @@ -441,12 +338,10 @@ func (ms *MigrationService) extractToDirectory(ctx context.Context, zipFile, dst continue } - // 确保父目录存在 if err := os.MkdirAll(filepath.Dir(dstPath), 0755); err != nil { return err } - // 解压文件 if err := ms.extractFile(file, dstPath); err != nil { return err } @@ -457,21 +352,18 @@ func (ms *MigrationService) extractToDirectory(ctx context.Context, zipFile, dst // extractFile 解压单个文件 func (ms *MigrationService) extractFile(file *zip.File, dstPath string) error { - // 打开zip中的文件 srcFile, err := file.Open() if err != nil { return err } defer srcFile.Close() - // 创建目标文件 dstFile, err := os.OpenFile(dstPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, file.FileInfo().Mode()) if err != nil { return err } defer dstFile.Close() - // 复制文件内容 _, err = io.Copy(dstFile, srcFile) return err } @@ -484,56 +376,23 @@ func (ms *MigrationService) isDirectoryEmpty(dirPath string) (bool, error) { } defer f.Close() - // 尝试读取一个条目 _, err = f.Readdir(1) if err == io.EOF { - // 目录为空 return true, nil } if err != nil { return false, err } - // 目录不为空 return false, nil } // isSubDirectory 检查target是否是parent的子目录 func (ms *MigrationService) isSubDirectory(parent, target string) bool { - // 确保路径以分隔符结尾,以避免误判 parent = filepath.Clean(parent) + string(filepath.Separator) target = filepath.Clean(target) + string(filepath.Separator) - - // 检查target是否以parent开头 return len(target) > len(parent) && target[:len(parent)] == parent } -// calculateDirectorySize 计算目录大小和文件数 -func (ms *MigrationService) calculateDirectorySize(ctx context.Context, dirPath string) (int, int64, error) { - var totalFiles int - var totalBytes int64 - - err := filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - - // 检查是否取消 - select { - case <-ctx.Done(): - return ctx.Err() - default: - } - - if !info.IsDir() { - totalFiles++ - totalBytes += info.Size() - } - return nil - }) - - return totalFiles, totalBytes, err -} - // CancelMigration 取消迁移 func (ms *MigrationService) CancelMigration() error { ms.mu.Lock() @@ -545,18 +404,22 @@ func (ms *MigrationService) CancelMigration() error { return nil } - return fmt.Errorf("no active migration to cancel") + return fmt.Errorf("No active migration to cancel") +} + +// SetProgressBroadcaster 设置进度广播函数 +func (ms *MigrationService) SetProgressBroadcaster(broadcaster func(MigrationProgress)) { + ms.mu.Lock() + defer ms.mu.Unlock() + ms.progressBroadcaster = broadcaster } // ServiceShutdown 服务关闭 func (ms *MigrationService) ServiceShutdown() error { ms.logger.Info("Migration: Service is shutting down...") - - // 取消正在进行的迁移 if err := ms.CancelMigration(); err != nil { ms.logger.Debug("Migration: No active migration to cancel during shutdown") } - ms.logger.Info("Migration: Service shutdown completed") return nil } diff --git a/internal/services/service_manager.go b/internal/services/service_manager.go index 082f62d..4bba02f 100644 --- a/internal/services/service_manager.go +++ b/internal/services/service_manager.go @@ -15,6 +15,8 @@ type ServiceManager struct { systemService *SystemService hotkeyService *HotkeyService dialogService *DialogService + websocketService *WebSocketService + httpService *HTTPService logger *log.LoggerService } @@ -41,8 +43,26 @@ func NewServiceManager() *ServiceManager { // 初始化对话服务 dialogService := NewDialogService(logger) + // 初始化 WebSocket 服务 + websocketService := NewWebSocketService(logger) + + // 初始化 HTTP 服务 + httpService := NewHTTPService(logger, websocketService) + + // 设置迁移服务的WebSocket广播 + migrationService.SetProgressBroadcaster(func(progress MigrationProgress) { + websocketService.BroadcastMigrationProgress(progress) + }) + + // 启动 HTTP 服务器 + err := httpService.StartServer("8899") + if err != nil { + logger.Error("Failed to start HTTP server", "error", err) + panic(err) + } + // 使用新的配置通知系统设置热键配置变更监听 - err := configService.SetHotkeyChangeCallback(func(enable bool, hotkey *models.HotkeyCombo) error { + err = configService.SetHotkeyChangeCallback(func(enable bool, hotkey *models.HotkeyCombo) error { return hotkeyService.UpdateHotkey(enable, hotkey) }) if err != nil { @@ -50,7 +70,7 @@ func NewServiceManager() *ServiceManager { panic(err) } - // 设置数据路径变更监听 + // 设置数据路径变更监听,处理配置重置和路径变更 err = configService.SetDataPathChangeCallback(func(oldPath, newPath string) error { return documentService.OnDataPathChanged(oldPath, newPath) }) @@ -73,6 +93,8 @@ func NewServiceManager() *ServiceManager { systemService: systemService, hotkeyService: hotkeyService, dialogService: dialogService, + websocketService: websocketService, + httpService: httpService, logger: logger, } } diff --git a/internal/services/websocket_service.go b/internal/services/websocket_service.go new file mode 100644 index 0000000..8f9e131 --- /dev/null +++ b/internal/services/websocket_service.go @@ -0,0 +1,159 @@ +package services + +import ( + "encoding/json" + "net/http" + "sync" + + "github.com/lxzan/gws" + "github.com/wailsapp/wails/v3/pkg/services/log" +) + +// WebSocketService WebSocket 服务 +type WebSocketService struct { + logger *log.LoggerService + upgrader *gws.Upgrader + clients map[*gws.Conn]bool + clientsMu sync.RWMutex +} + +// NewWebSocketService 创建 WebSocket 服务 +func NewWebSocketService(logger *log.LoggerService) *WebSocketService { + if logger == nil { + logger = log.New() + } + + ws := &WebSocketService{ + logger: logger, + clients: make(map[*gws.Conn]bool), + } + + // 创建 WebSocket 升级器 + ws.upgrader = gws.NewUpgrader(&WebSocketHandler{service: ws}, &gws.ServerOption{ + ParallelEnabled: true, + Recovery: gws.Recovery, + PermessageDeflate: gws.PermessageDeflate{Enabled: true}, + }) + + return ws +} + +// WebSocketHandler WebSocket 事件处理器 +type WebSocketHandler struct { + service *WebSocketService +} + +// OnOpen 连接建立时调用 +func (h *WebSocketHandler) OnOpen(socket *gws.Conn) { + h.service.logger.Info("WebSocket: Client connected") + + h.service.clientsMu.Lock() + h.service.clients[socket] = true + h.service.clientsMu.Unlock() +} + +// OnClose 连接关闭时调用 +func (h *WebSocketHandler) OnClose(socket *gws.Conn, err error) { + h.service.logger.Info("WebSocket: Client disconnected", "error", err) + + h.service.clientsMu.Lock() + delete(h.service.clients, socket) + h.service.clientsMu.Unlock() +} + +// OnPing 收到 Ping 时调用 +func (h *WebSocketHandler) OnPing(socket *gws.Conn, payload []byte) { + _ = socket.WritePong(nil) +} + +// OnPong 收到 Pong 时调用 +func (h *WebSocketHandler) OnPong(socket *gws.Conn, payload []byte) { + // Do nothing +} + +// OnMessage 收到消息时调用 +func (h *WebSocketHandler) OnMessage(socket *gws.Conn, message *gws.Message) { + defer message.Close() + + h.service.logger.Debug("WebSocket: Received message", "message", string(message.Bytes())) + +} + +// HandleUpgrade 处理 WebSocket 升级请求 +func (ws *WebSocketService) HandleUpgrade(w http.ResponseWriter, r *http.Request) { + socket, err := ws.upgrader.Upgrade(w, r) + if err != nil { + ws.logger.Error("WebSocket: Failed to upgrade connection", "error", err) + return + } + + // 启动读取循环(必须在 goroutine 中运行以防止阻塞) + go func() { + socket.ReadLoop() + }() +} + +// BroadcastMessage 广播消息给所有连接的客户端 +func (ws *WebSocketService) BroadcastMessage(messageType string, data interface{}) { + message := map[string]interface{}{ + "type": messageType, + "data": data, + } + + jsonData, err := json.Marshal(message) + if err != nil { + ws.logger.Error("WebSocket: Failed to marshal message", "error", err) + return + } + + ws.clientsMu.RLock() + clients := make([]*gws.Conn, 0, len(ws.clients)) + for client := range ws.clients { + clients = append(clients, client) + } + ws.clientsMu.RUnlock() + + // 使用广播器进行高效广播 + broadcaster := gws.NewBroadcaster(gws.OpcodeText, jsonData) + defer broadcaster.Close() + + for _, client := range clients { + if err := broadcaster.Broadcast(client); err != nil { + ws.logger.Error("WebSocket: Failed to broadcast to client", "error", err) + // 清理失效的连接 + ws.clientsMu.Lock() + delete(ws.clients, client) + ws.clientsMu.Unlock() + } + } + + ws.logger.Debug("WebSocket: Broadcasted message", "type", messageType, "clients", len(clients)) +} + +// BroadcastMigrationProgress 广播迁移进度 +func (ws *WebSocketService) BroadcastMigrationProgress(progress MigrationProgress) { + ws.BroadcastMessage("migration_progress", progress) +} + +// GetConnectedClientsCount 获取连接的客户端数量 +func (ws *WebSocketService) GetConnectedClientsCount() int { + ws.clientsMu.RLock() + defer ws.clientsMu.RUnlock() + return len(ws.clients) +} + +// ServiceShutdown 服务关闭 +func (ws *WebSocketService) ServiceShutdown() error { + ws.logger.Info("WebSocket: Service is shutting down...") + + // 关闭所有客户端连接 + ws.clientsMu.Lock() + for client := range ws.clients { + _ = client.WriteClose(1000, []byte("Server shutting down")) + } + ws.clients = make(map[*gws.Conn]bool) + ws.clientsMu.Unlock() + + ws.logger.Info("WebSocket: Service shutdown completed") + return nil +}