Private
Public Access
1
0
Files
u-desk/app.go

1324 lines
36 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// [fs-only] 数据库客户端模块已移除feature/fs-only 分支)
// 保留模块:文件系统 | Markdown编辑器 | 版本历史(抽屉) | 系统信息 | 更新检查 | PDF导出
// 顶部Tab仅file-system数据库 db-cli 已删除)
package main
import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
stdruntime "runtime"
"strconv"
"strings"
"sync"
"time"
"u-desk/internal/api"
"u-desk/internal/common"
"u-desk/internal/filewatch"
"u-desk/internal/filesystem"
"u-desk/internal/hotkey"
osssvc "u-desk/internal/ossdrv"
"u-desk/internal/service"
"u-desk/internal/sftp"
"u-desk/internal/storage"
"u-desk/internal/storage/models"
"u-desk/internal/system"
"golang.org/x/sys/windows/registry"
"github.com/wailsapp/wails/v3/pkg/application"
"github.com/wailsapp/wails/v3/pkg/w32"
"gorm.io/gorm"
)
// App 应用结构体
type App struct {
ctx context.Context
mainWindow *application.WebviewWindow
updateAPI *api.UpdateAPI
updateTicker *time.Ticker
configAPI *api.ConfigAPI
pdfAPI *api.PdfAPI
filesystem *filesystem.FileSystemService
sftpService *sftp.Service
ossService *osssvc.Service
profileSvc *service.ProfileService
fileWatcher *filewatch.Watcher
isAlwaysOnTop bool
mu sync.Mutex
unregisterHotkey func()
}
// App 方法命名约定:
// - 多参数操作 → XxxRequest 结构体Wails 自动生成 TS 类型)
// - 单参数查询/简单操作 → 直接参数
// NewApp 创建新的应用实例
func NewApp() *App {
return &App{}
}
// SetMainWindow 设置主窗口引用(由 main.go 在创建窗口后调用)
func (a *App) SetMainWindow(w *application.WebviewWindow) {
a.mainWindow = w
a.fileWatcher = filewatch.NewWatcher(func(name string, data ...any) {
if a.mainWindow != nil {
a.mainWindow.EmitEvent(name, data...)
}
})
}
// RegisterGlobalHotkey 注册 Ctrl+Shift+B 全局热键(需在窗口创建后调用)
func (a *App) RegisterGlobalHotkey() {
if a.mainWindow == nil {
return
}
a.mu.Lock()
defer a.mu.Unlock()
if a.unregisterHotkey != nil {
return
}
hwnd := uintptr(a.mainWindow.NativeWindow())
if hwnd == 0 {
fmt.Println("[全局热键] HWND 为 0注册跳过")
return
}
const id int32 = 1
if err := hotkey.Register(hwnd, id, hotkey.ModControl|hotkey.ModShift, 0x42); err != nil {
fmt.Printf("[全局热键] RegisterHotKey Ctrl+Shift+B 失败: %v\n", err)
return
}
fmt.Println("[全局热键] Ctrl+Shift+B 已注册")
a.unregisterHotkey = func() { hotkey.Unregister(hwnd, id) }
}
// HandleHotkey 处理全局热键回调:切换 BgmBar 显示/隐藏
func (a *App) HandleHotkey() {
if a.mainWindow == nil {
return
}
a.mainWindow.EmitEvent("toggle-bgm-bar")
}
// ServiceStartup Wails v3 服务启动生命周期(替代 v2 的 Startup
func (a *App) ServiceStartup(ctx context.Context, _ application.ServiceOptions) error {
a.ctx = ctx
// dev 模式打开 DevTools
openDevTools(a.mainWindow)
// 1. 核心初始化SQLite必须同步很快
if _, err := storage.InitFast(); err != nil {
return fmt.Errorf("SQLite 初始化失败,应用无法启动: %w", err)
}
// 2. 初始化配置服务
configService, err := api.NewConfigAPI()
if err != nil {
return fmt.Errorf("配置服务初始化失败: %w", err)
}
a.configAPI = configService
// 2.5. 迁移旧配置
_ = a.configAPI.MigrateTabConfig()
// 2.6. 初始化PDF导出API
fmt.Println("[启动] 初始化PDF导出模块...")
pdfAPI, err := api.NewPdfAPI()
if err != nil {
fmt.Printf("[启动] PDF导出API初始化失败: %v\n", err)
} else {
a.pdfAPI = pdfAPI
fmt.Println("[启动] PDF导出模块初始化完成")
}
// 3. 初始化版本号(提前触发缓存,避免后续重复计算)
version := service.GetCurrentVersion()
fmt.Printf("[启动] 当前版本: %s\n", version)
// 4. 读取配置,获取可见的 Tabs
visibleTabs := a.getVisibleTabs()
fmt.Printf("[启动] 可用的模块: %v\n", visibleTabs)
// 4. 根据配置初始化模块(条件初始化)
if err := a.initModulesByConfig(visibleTabs); err != nil {
return fmt.Errorf("模块初始化失败: %w", err)
}
// 5. 清理过期的下载缓存
storage.CleanupExpiredCache()
// 6. 异步初始化UpdateAPI涉及网络请求完全异步
go func() {
if updateAPI, err := api.NewUpdateAPI("https://c.1216.top/last-version.json"); err == nil {
a.mu.Lock()
a.updateAPI = updateAPI
a.mu.Unlock()
a.updateAPI.SetContext(ctx)
a.updateAPI.SetEventEmitter(func(name string, data ...any) {
if a.mainWindow != nil {
a.mainWindow.EmitEvent(name, data...)
}
})
a.startAutoUpdateCheck()
}
}()
// 延迟注册全局热键(轮询等待原生窗口创建完成)
// RegisterHotKey 必须在创建窗口的同一线程调用,
// 通过 PostMessage 将注册请求投递到主线程消息循环
go func() {
for i := 0; i < 20; i++ {
time.Sleep(200 * time.Millisecond)
if a.mainWindow == nil {
return
}
hwnd := uintptr(a.mainWindow.NativeWindow())
if hwnd != 0 {
hotkey.PostMessage(hwnd, hotkey.WM_APP_HOTKEY, 0, 0)
return
}
}
fmt.Println("[全局热键] 等待窗口超时")
}()
return nil
}
// getVisibleTabs 获取配置中的可见 Tabs
func (a *App) getVisibleTabs() []string {
config, err := a.configAPI.GetAppConfig()
if err != nil {
fmt.Printf("[启动] 读取配置失败,使用默认配置: %v\n", err)
return common.DefaultVisibleTabs
}
success, ok := config["success"].(bool)
if !ok || !success {
fmt.Printf("[启动] 配置读取失败,使用默认配置\n")
return common.DefaultVisibleTabs
}
data, ok := config["data"].(map[string]interface{})
if !ok {
return common.DefaultVisibleTabs
}
visibleTabsInterface, ok := data["visibleTabs"].([]interface{})
if !ok {
return common.DefaultVisibleTabs
}
visibleTabs := common.InterfaceSliceToStringSlice(visibleTabsInterface)
if len(visibleTabs) == 0 {
return common.DefaultVisibleTabs
}
return visibleTabs
}
// initModulesByConfig 根据配置初始化模块
func (a *App) initModulesByConfig(visibleTabs []string) error {
if common.Contains(visibleTabs, common.TabFileSystem) {
fmt.Println("[启动] 初始化文件系统模块...")
fsConfig := filesystem.DefaultConfig()
var err error
a.filesystem, err = filesystem.NewFileSystemService(fsConfig)
if err != nil {
return fmt.Errorf("文件系统服务初始化失败: %w", err)
}
go a.startFileServer()
fmt.Println("[启动] 文件系统模块初始化完成")
} else {
fmt.Println("[启动] 跳过文件系统模块(未启用)")
}
return nil
}
// startFileServer 启动文件服务器
func (a *App) startFileServer() {
if _, err := filesystem.StartLocalFileServer(); err != nil {
fmt.Printf("[文件服务器] 启动失败: %v\n", err)
return
}
fmt.Printf("[文件服务器] 启动在 http://%s\n", filesystem.GetLocalFileServerAddr())
}
// ServiceShutdown Wails v3 服务关闭生命周期(替代 v2 的 Shutdown
func (a *App) ServiceShutdown() error {
if a.updateTicker != nil {
a.updateTicker.Stop()
}
if a.unregisterHotkey != nil {
a.unregisterHotkey()
}
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if a.filesystem != nil {
fmt.Println("[文件系统服务] 正在关闭...")
if err := a.filesystem.Close(shutdownCtx); err != nil {
fmt.Printf("[文件系统服务] 关闭失败: %v\n", err)
} else {
fmt.Println("[文件系统服务] 已关闭")
}
}
fmt.Println("[文件服务器] 正在关闭...")
if err := filesystem.ShutdownLocalFileServer(); err != nil {
fmt.Printf("[文件服务器] 关闭失败: %v\n", err)
} else {
fmt.Println("[文件服务器] 已关闭")
}
// 关闭所有 SFTP 连接 + 清理临时文件
if a.sftpService != nil {
sftp.GetManager().Shutdown()
}
storage.CleanupExpiredCache()
// 关闭所有 OSS 连接
osssvc.GetManager().Shutdown()
return nil
}
// GetSystemInfo 获取系统信息
func (a *App) GetSystemInfo() (map[string]interface{}, error) {
return system.GetSystemInfo()
}
// GetCPUInfo 获取 CPU 信息
func (a *App) GetCPUInfo() (map[string]interface{}, error) {
return system.GetCPUInfo()
}
// GetMemoryInfo 获取内存信息
func (a *App) GetMemoryInfo() (map[string]interface{}, error) {
return system.GetMemoryInfo()
}
// GetDiskInfo 获取磁盘信息
func (a *App) GetDiskInfo() ([]map[string]interface{}, error) {
return system.GetDiskInfo()
}
// ReadFile 读取文件
func (a *App) ReadFile(path string) (string, error) {
return a.filesystem.ReadFile(path)
}
// WatchFile 开始监听指定文件的变化,变化时发送 file-changed 事件
func (a *App) WatchFile(path string) error {
if a.fileWatcher == nil {
return fmt.Errorf("文件监听器未初始化")
}
return a.fileWatcher.WatchFile(path)
}
// UnwatchFile 停止监听文件变化
func (a *App) UnwatchFile() {
if a.fileWatcher != nil {
a.fileWatcher.UnwatchFile()
}
}
// WriteFileRequest 写入文件请求结构体
type WriteFileRequest struct {
Path string `json:"path"`
Content string `json:"content"`
}
// WriteFile 写入文件
func (a *App) WriteFile(req WriteFileRequest) error {
return a.filesystem.WriteFile(req.Path, req.Content)
}
// SaveBase64FileRequest 保存 Base64 编码的二进制文件
type SaveBase64FileRequest struct {
Path string `json:"path"`
Content string `json:"content"` // base64 编码的文件内容
}
// SaveBase64File 将 base64 内容解码后写入文件(用于图片等二进制数据)
func (a *App) SaveBase64File(req SaveBase64FileRequest) error {
return a.filesystem.SaveBase64File(req.Path, req.Content)
}
// ListDir 列出目录
func (a *App) ListDir(path string) ([]map[string]interface{}, error) {
return a.filesystem.ListDir(path)
}
// CreateDir 创建目录
func (a *App) CreateDir(path string) (*filesystem.FileOperationResult, error) {
return a.filesystem.CreateDir(path)
}
// CreateFile 创建文件
func (a *App) CreateFile(path string) (*filesystem.FileOperationResult, error) {
return a.filesystem.CreateFile(path)
}
// DeletePath 删除文件或目录
func (a *App) DeletePath(path string) (*filesystem.FileOperationResult, error) {
return a.filesystem.DeletePath(path)
}
// RenamePathRequest 重命名文件或目录请求结构体
type RenamePathRequest struct {
OldPath string `json:"oldPath"`
NewPath string `json:"newPath"`
}
// RenamePath 重命名文件或目录
func (a *App) RenamePath(req RenamePathRequest) (*filesystem.FileOperationResult, error) {
return a.filesystem.RenamePath(req.OldPath, req.NewPath)
}
// GetFileInfo 获取文件信息
func (a *App) GetFileInfo(path string) (map[string]interface{}, error) {
return a.filesystem.GetFileInfo(path)
}
// GetEnvVars 获取环境变量
func (a *App) GetEnvVars() (map[string]string, error) {
envVars := make(map[string]string)
for _, env := range os.Environ() {
if key, value, found := strings.Cut(env, "="); found {
envVars[key] = value
}
}
return envVars, nil
}
// OpenPath 使用系统默认程序打开文件或目录
func (a *App) OpenPath(path string) error {
return a.filesystem.OpenPath(path)
}
// ========== Zip 文件操作接口 ==========
// ListZipContents 列出 zip 文件内容
func (a *App) ListZipContents(zipPath string) ([]map[string]interface{}, error) {
return a.filesystem.ListZipContents(zipPath)
}
// ExtractFileFromZip 从 zip 文件中提取单个文件内容
func (a *App) ExtractFileFromZip(zipPath, filePath string) (string, error) {
return a.filesystem.ExtractFileFromZip(zipPath, filePath)
}
// ExtractFileFromZipToTemp 从 zip 文件中提取单个文件到临时目录
func (a *App) ExtractFileFromZipToTemp(zipPath, filePath string) (string, error) {
return a.filesystem.ExtractFileFromZipToTemp(zipPath, filePath)
}
// GetZipFileInfo 获取 zip 文件中特定文件的信息
func (a *App) GetZipFileInfo(zipPath, filePath string) (map[string]interface{}, error) {
return a.filesystem.GetZipFileInfo(zipPath, filePath)
}
// ResolveShortcut 解析快捷方式文件,返回目标路径信息
func (a *App) ResolveShortcut(lnkPath string) (map[string]interface{}, error) {
targetPath, err := a.filesystem.ResolveShortcut(lnkPath)
if err != nil {
return map[string]interface{}{
"success": false,
"message": err.Error(),
}, err
}
fileInfo, err := a.filesystem.GetFileInfo(targetPath)
if err != nil {
return map[string]interface{}{
"success": true,
"targetPath": targetPath,
"targetExists": false,
"targetAccessible": false,
}, nil
}
return map[string]interface{}{
"success": true,
"targetPath": targetPath,
"targetExists": true,
"targetAccessible": true,
"targetInfo": fileInfo,
}, nil
}
// getWindowsSpecialFolder 从注册表读取 Windows 特殊文件夹的真实路径
func getWindowsSpecialFolder(guid string, fallbackName string) string {
key, err := registry.OpenKey(registry.CURRENT_USER,
`Software\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders`,
registry.READ)
if err != nil {
return ""
}
defer key.Close()
val, _, err := key.GetStringValue(guid)
if err != nil || val == "" {
return ""
}
path := os.ExpandEnv(val)
if _, err := os.Stat(path); err != nil {
return ""
}
return path
}
// GetCommonPaths 获取常用系统路径
func (a *App) GetCommonPaths() (map[string]string, error) {
homeDir, err := os.UserHomeDir()
if err != nil {
return nil, err
}
paths := map[string]string{
"home": homeDir,
}
folderGUIDs := map[string]string{
"desktop": "{B4BFCC3A-DB2C-424C-B029-7FE99A87C641}",
"documents": "{D20B4C7F-5EA7-424C-B25E-039F6F1FCC8A}",
"downloads": "{374DE290-123F-4565-9164-39C4925E467B}",
}
for name, guid := range folderGUIDs {
if p := getWindowsSpecialFolder(guid, name); p != "" {
paths[name] = p
} else {
paths[name] = filepath.Join(homeDir, strings.ToUpper(name[:1])+name[1:])
}
}
if stdruntime.GOOS == "windows" {
for _, drive := range "ABCDEFGHIJKLMNOPQRSTUVWXYZ" {
path := string(drive) + ":\\"
if _, err := os.Stat(path); err == nil {
key := fmt.Sprintf("root_%c", drive)
paths[key] = path
}
}
}
return paths, nil
}
// Reload 重新加载窗口(用于菜单项)
func (a *App) Reload() {
if a.mainWindow != nil {
a.mainWindow.Reload()
}
}
// ClearCache 清理本地缓存(用于菜单项)
func (a *App) ClearCache() {
if a.mainWindow != nil {
a.mainWindow.EmitEvent("clear-cache")
}
}
// ========== 窗口控制方法 ==========
// WindowMinimize 最小化窗口
func (a *App) WindowMinimize() {
if a.mainWindow != nil {
a.mainWindow.Minimise()
}
}
// WindowMaximize 最大化/还原窗口
func (a *App) WindowMaximize() {
if a.mainWindow == nil {
return
}
if a.mainWindow.IsMaximised() {
a.mainWindow.UnMaximise()
} else {
a.mainWindow.Maximise()
}
}
// WindowClose 关闭窗口
func (a *App) WindowClose() {
application.Get().Quit()
}
// WindowIsMaximized 检查窗口是否最大化
func (a *App) WindowIsMaximized() bool {
if a.mainWindow != nil {
return a.mainWindow.IsMaximised()
}
return false
}
// WindowToggleAlwaysOnTop 切换窗口置顶
func (a *App) WindowToggleAlwaysOnTop() bool {
if a.mainWindow == nil {
return false
}
a.isAlwaysOnTop = !a.isAlwaysOnTop
a.mainWindow.SetAlwaysOnTop(a.isAlwaysOnTop)
return a.isAlwaysOnTop
}
// SetWindowTitleBarColor 设置原生标题栏颜色 + 主题模式0x00BBGGRR 格式)
func (a *App) SetWindowTitleBarColor(color uint32, isDark bool) {
if a.mainWindow == nil || stdruntime.GOOS != "windows" {
return
}
hwnd := uintptr(a.mainWindow.NativeWindow())
if hwnd == 0 {
return
}
w32.SetTheme(hwnd, isDark)
w32.SetTitleBarColour(hwnd, color)
}
// ========== 版本更新管理接口 ==========
// requireUpdateAPI 检查 updateAPI 是否已初始化
func (a *App) requireUpdateAPI() (*api.UpdateAPI, error) {
a.mu.Lock()
defer a.mu.Unlock()
if a.updateAPI == nil {
return nil, fmt.Errorf("更新功能正在初始化中")
}
return a.updateAPI, nil
}
// CheckUpdate 检查更新
func (a *App) CheckUpdate() (map[string]interface{}, error) {
api, err := a.requireUpdateAPI()
if err != nil {
return nil, err
}
return api.CheckUpdate()
}
// GetCurrentVersion 获取当前版本号
func (a *App) GetCurrentVersion() (map[string]interface{}, error) {
api, err := a.requireUpdateAPI()
if err != nil {
return nil, err
}
return api.GetCurrentVersion()
}
// GetUpdateConfig 获取更新配置
func (a *App) GetUpdateConfig() (map[string]interface{}, error) {
api, err := a.requireUpdateAPI()
if err != nil {
return nil, err
}
return api.GetUpdateConfig()
}
// SetUpdateConfig 设置更新配置
func (a *App) SetUpdateConfig(autoCheckEnabled bool, checkIntervalMinutes int, checkURL string) (map[string]interface{}, error) {
api, err := a.requireUpdateAPI()
if err != nil {
return nil, err
}
return api.SetUpdateConfig(autoCheckEnabled, checkIntervalMinutes, checkURL)
}
// DownloadUpdate 下载更新包
func (a *App) DownloadUpdate(downloadURL string) (map[string]interface{}, error) {
api, err := a.requireUpdateAPI()
if err != nil {
return nil, err
}
return api.DownloadUpdate(downloadURL)
}
// InstallUpdate 安装更新包
func (a *App) InstallUpdate(installerPath string, autoRestart bool) (map[string]interface{}, error) {
api, err := a.requireUpdateAPI()
if err != nil {
return nil, err
}
return api.InstallUpdate(installerPath, autoRestart)
}
// InstallUpdateWithHash 安装更新包(带哈希验证)
func (a *App) InstallUpdateWithHash(installerPath string, autoRestart bool, expectedHash string, hashType string) (map[string]interface{}, error) {
api, err := a.requireUpdateAPI()
if err != nil {
return nil, err
}
return api.InstallUpdateWithHash(installerPath, autoRestart, expectedHash, hashType)
}
// VerifyUpdateFile 验证更新文件哈希值
func (a *App) VerifyUpdateFile(filePath string, expectedHash string, hashType string) (map[string]interface{}, error) {
api, err := a.requireUpdateAPI()
if err != nil {
return nil, err
}
return api.VerifyUpdateFile(filePath, expectedHash, hashType)
}
// startAutoUpdateCheck 启动自动更新检查
func (a *App) startAutoUpdateCheck() {
if a.updateAPI == nil {
return
}
config, err := a.updateAPI.GetUpdateConfig()
if err != nil {
return
}
success, ok := config["success"].(bool)
if !ok || !success {
return
}
configData, ok := config["data"].(map[string]interface{})
if !ok {
return
}
autoCheckEnabled, ok := configData["auto_check_enabled"].(bool)
if !ok || !autoCheckEnabled {
return
}
interval, ok := configData["check_interval_minutes"].(int)
if !ok || interval <= 0 {
interval = 5
}
go a.checkUpdate()
a.updateTicker = time.NewTicker(time.Duration(interval) * time.Minute)
go func() {
for range a.updateTicker.C {
a.checkUpdate()
}
}()
}
// checkUpdate 执行更新检查
func (a *App) checkUpdate() {
defer func() {
if r := recover(); r != nil {
fmt.Printf("[自动检查更新] 发生错误: %v\n", r)
}
}()
a.mu.Lock()
api := a.updateAPI
a.mu.Unlock()
if api == nil {
return
}
result, err := api.CheckUpdate()
if err != nil {
return
}
success, ok := result["success"].(bool)
if !ok || !success {
return
}
data, ok := result["data"].(map[string]interface{})
if !ok {
return
}
hasUpdate, ok := data["has_update"].(bool)
if ok && hasUpdate && a.mainWindow != nil {
a.mainWindow.EmitEvent("update-available", data)
}
}
// ========== 审计日志接口 ==========
// GetAuditLogs 获取审计日志
func (a *App) GetAuditLogs(limit int) ([]map[string]interface{}, error) {
return a.filesystem.GetAuditLogs(limit)
}
// ========== 文件服务器接口 ==========
// GetFileServerURL 获取本地文件服务器的URL
func (a *App) GetFileServerURL() string {
return fmt.Sprintf("http://%s", filesystem.GetLocalFileServerAddr())
}
// DetectFileTypeByContent 通过文件内容检测文件类型
func (a *App) DetectFileTypeByContent(path string) (map[string]interface{}, error) {
return filesystem.DetectFileTypeByContentSimple(path)
}
// ========== 回收站接口 ==========
// GetRecycleBinEntries 获取回收站条目
func (a *App) GetRecycleBinEntries() ([]map[string]interface{}, error) {
return a.filesystem.GetRecycleBinEntries()
}
// RestoreFromRecycleBin 从回收站恢复文件
func (a *App) RestoreFromRecycleBin(recyclePath string) error {
return a.filesystem.RestoreFromRecycleBin(recyclePath)
}
// DeletePermanently 永久删除回收站中的文件
func (a *App) DeletePermanently(recyclePath string) error {
return a.filesystem.DeletePermanently(recyclePath)
}
// EmptyRecycleBin 清空回收站
func (a *App) EmptyRecycleBin() error {
return a.filesystem.EmptyRecycleBin()
}
// ========== 应用配置接口 ==========
// GetAppConfig 获取应用配置
func (a *App) GetAppConfig() (map[string]interface{}, error) {
if a.configAPI == nil {
return nil, fmt.Errorf("配置服务正在初始化中")
}
return a.configAPI.GetAppConfig()
}
// SaveAppConfigRequest 保存应用配置请求
type SaveAppConfigRequest struct {
Tabs []api.AppTabDefinition `json:"tabs"`
VisibleTabs []string `json:"visibleTabs"`
DefaultTab string `json:"defaultTab"`
}
// SaveAppConfig 保存应用配置
func (a *App) SaveAppConfig(req SaveAppConfigRequest) (map[string]interface{}, error) {
if a.configAPI == nil {
return nil, fmt.Errorf("配置服务正在初始化中")
}
oldConfig, _ := a.configAPI.GetAppConfig()
var oldVisibleTabs []string
if success, ok := oldConfig["success"].(bool); ok && success {
if data, ok := oldConfig["data"].(map[string]interface{}); ok {
if vtInterface, ok := data["visibleTabs"].([]interface{}); ok {
oldVisibleTabs = common.InterfaceSliceToStringSlice(vtInterface)
}
}
}
apiReq := api.SaveAppConfigRequest{
Tabs: req.Tabs,
VisibleTabs: req.VisibleTabs,
DefaultTab: req.DefaultTab,
}
result, err := a.configAPI.SaveAppConfig(apiReq)
if err != nil {
return result, err
}
if success, ok := result["success"].(bool); ok && success {
a.handleNewlyEnabledModules(oldVisibleTabs, req.VisibleTabs)
}
return result, nil
}
// handleNewlyEnabledModules 处理新启用的模块
func (a *App) handleNewlyEnabledModules(oldTabs, newTabs []string) {
newlyEnabled := common.Difference(newTabs, oldTabs)
if len(newlyEnabled) == 0 {
return
}
fmt.Printf("[模块] 检测到新启用的模块: %v\n", newlyEnabled)
for _, tab := range newlyEnabled {
switch tab {
case common.TabFileSystem:
a.initFilesystemModule()
case common.TabDevice:
fmt.Println("[模块] 设备测试模块已启用")
}
}
}
// initFilesystemModule 延迟初始化文件系统模块
func (a *App) initFilesystemModule() {
if a.filesystem != nil {
fmt.Println("[模块] 文件系统模块已初始化,跳过")
return
}
fmt.Println("[模块] 延迟初始化文件系统模块...")
fsConfig := filesystem.DefaultConfig()
var err error
a.filesystem, err = filesystem.NewFileSystemService(fsConfig)
if err != nil {
fmt.Printf("[模块] 文件系统模块初始化失败: %v\n", err)
return
}
go a.startFileServer()
fmt.Println("[模块] 文件系统模块初始化完成")
}
// ExportPDF 导出PDF文件
func (a *App) ExportPDF(content string, title string, fileName string, fontSize int, pageWidth int, pageHeight int) (map[string]interface{}, error) {
if a.pdfAPI == nil {
return map[string]interface{}{
"success": false,
"message": "PDF导出功能未初始化",
}, fmt.Errorf("PDF导出功能未初始化")
}
req := api.PdfExportRequest{
Content: content,
Title: title,
FileName: fileName,
FontSize: fontSize,
PageWidth: pageWidth,
PageHeight: pageHeight,
}
result, err := a.pdfAPI.ExportMarkdownToPDF(req)
if err != nil {
return map[string]interface{}{
"success": false,
"message": err.Error(),
}, err
}
return map[string]interface{}{
"success": result.Success,
"message": result.Message,
"path": result.Path,
"size": result.Size,
}, nil
}
// SelectPDFSaveDirectory 选择PDF保存目录
func (a *App) SelectPDFSaveDirectory() (string, error) {
if a.pdfAPI == nil {
return "", fmt.Errorf("PDF导出功能未初始化")
}
return a.pdfAPI.SelectDirectory()
}
// ========== SFTP 接口 ==========
func (a *App) ensureSftpService() *sftp.Service {
a.mu.Lock()
defer a.mu.Unlock()
if a.sftpService == nil {
a.sftpService = sftp.NewService()
}
return a.sftpService
}
// SftpConnectRequest SFTP 连接请求
type SftpConnectRequest struct {
Host string `json:"host"`
Port int `json:"port"`
Username string `json:"username"`
Password string `json:"password"`
KeyPath string `json:"key_path"`
KeyPassphrase string `json:"key_passphrase"`
}
// SftpConnect 建立 SFTP 连接,返回连接标识符 connID
func (a *App) SftpConnect(req SftpConnectRequest) (string, error) {
config := &sftp.Config{
Host: req.Host,
Port: req.Port,
Username: req.Username,
Password: req.Password,
KeyPath: req.KeyPath,
KeyPassphrase: req.KeyPassphrase,
}
if config.Port == 0 {
config.Port = 22
}
if config.Timeout == 0 {
config.Timeout = 15 * time.Second
}
svc := a.ensureSftpService()
_, err := svc.GetManager().Connect(config)
if err != nil {
return "", sftp.ToUserMessage(err)
}
connID := sftp.ConnID(config.Host, config.Port)
return connID, nil
}
// SftpDisconnect 断开 SFTP 连接
func (a *App) SftpDisconnect(connID string) error {
parts := strings.SplitN(connID, ":", 2)
if len(parts) < 2 {
return fmt.Errorf("无效的连接标识符")
}
host := parts[0]
port, err := strconv.Atoi(parts[1])
if err != nil {
return fmt.Errorf("无效的端口号")
}
sftp.GetManager().Disconnect(host, port)
return nil
}
// SftpListDir SFTP 列出目录
func (a *App) SftpListDir(connID string, dirPath string) ([]map[string]interface{}, error) {
return a.ensureSftpService().ListDir(connID, dirPath)
}
// SftpReadFile SFTP 读取文件内容
func (a *App) SftpReadFile(connID string, filePath string) (string, error) {
return a.ensureSftpService().ReadFile(connID, filePath)
}
// SftpWriteFileRequest SFTP 写入请求
type SftpWriteFileRequest struct {
SessionID string `json:"session_id"`
Path string `json:"path"`
Content string `json:"content"`
}
// SftpWriteFile SFTP 写入文件
func (a *App) SftpWriteFile(req SftpWriteFileRequest) error {
return a.ensureSftpService().WriteFile(req.SessionID, req.Path, req.Content)
}
// SftpWriteBase64File SFTP 写入 base64 编码的二进制文件(粘贴图片等)
func (a *App) SftpWriteBase64File(sessionID, filePath, base64Content string) error {
return a.ensureSftpService().WriteBase64File(sessionID, filePath, base64Content)
}
// SftpGetFileInfo SFTP 获取文件信息
func (a *App) SftpGetFileInfo(connID string, filePath string) (map[string]interface{}, error) {
return a.ensureSftpService().GetFileInfo(connID, filePath)
}
// SftpCreateDir SFTP 创建目录
func (a *App) SftpCreateDir(connID string, dirPath string) (*filesystem.FileOperationResult, error) {
return a.ensureSftpService().CreateDir(connID, dirPath)
}
// SftpCreateFile SFTP 创建文件
func (a *App) SftpCreateFile(connID string, filePath string) (*filesystem.FileOperationResult, error) {
return a.ensureSftpService().CreateFile(connID, filePath)
}
// SftpDeletePath SFTP 删除文件或目录
func (a *App) SftpDeletePath(connID string, filePath string) (*filesystem.FileOperationResult, error) {
return a.ensureSftpService().DeletePath(connID, filePath)
}
// SftpRenamePathRequest SFTP 重命名请求
type SftpRenamePathRequest struct {
SessionID string `json:"session_id"`
OldPath string `json:"old_path"`
NewPath string `json:"new_path"`
}
// SftpRenamePath SFTP 重命名文件或目录
func (a *App) SftpRenamePath(req SftpRenamePathRequest) (*filesystem.FileOperationResult, error) {
return a.ensureSftpService().RenamePath(req.SessionID, req.OldPath, req.NewPath)
}
// SftpDownloadToTemp 下载远程文件到本地临时目录(用于预览)
func (a *App) SftpDownloadToTemp(connID string, remotePath string) (string, error) {
return a.ensureSftpService().DownloadToTemp(connID, remotePath)
}
// SftpDownloadSiteForPreview 下载 HTML 及其网站资源到本地临时目录
func (a *App) SftpDownloadSiteForPreview(connID string, remotePath string) (string, error) {
return a.ensureSftpService().DownloadSiteForPreview(connID, remotePath)
}
// SftpDownloadToTempCached 带缓存的 SFTP 下载(命中缓存直接返回本地路径)
func (a *App) SftpDownloadToTempCached(connID string, remotePath string, fileSize int64, modTime string) (string, error) {
return a.ensureSftpService().DownloadToTempCached(connID, remotePath, fileSize, modTime)
}
// SftpGetCommonPaths 获取 SFTP 远程主机常用路径
func (a *App) SftpGetCommonPaths(connID string) (map[string]string, error) {
return a.ensureSftpService().GetCommonPaths(connID)
}
// SftpGetSystemInfo 获取 SFTP 远程主机系统信息CPU/内存/磁盘)
func (a *App) SftpGetSystemInfo(connID string) (map[string]interface{}, error) {
return a.ensureSftpService().GetSystemInfo(connID)
}
// ========== OSS 接口 ==========
func (a *App) ensureOssService() *osssvc.Service {
a.mu.Lock()
defer a.mu.Unlock()
if a.ossService == nil {
a.ossService = osssvc.NewService()
}
return a.ossService
}
type OssConnectRequest struct {
Provider string `json:"provider"`
AccessKey string `json:"access_key"`
SecretKey string `json:"secret_key"`
Endpoint string `json:"endpoint"`
}
func (a *App) OssConnect(req OssConnectRequest) (string, error) {
if err := a.ensureOssService().GetManager().Connect(req.Provider, req.AccessKey, req.SecretKey, req.Endpoint); err != nil {
return "", err
}
return req.Provider, nil
}
// OssDisconnect 断开 OSS 连接
func (a *App) OssDisconnect(connID string) error {
osssvc.GetManager().Disconnect(connID)
return nil
}
// OssListDir OSS 列出目录
func (a *App) OssListDir(connID string, prefix string) ([]map[string]interface{}, error) {
return a.ensureOssService().ListDir(connID, prefix)
}
// OssDownloadToTemp OSS 下载到临时文件
func (a *App) OssDownloadToTemp(connID string, key string) (string, error) {
return a.ensureOssService().DownloadToTemp(connID, key)
}
// OssDownloadSiteForPreview OSS 下载 HTML 及其引用的资源到临时目录
func (a *App) OssDownloadSiteForPreview(connID string, key string) (string, error) {
return a.ensureOssService().DownloadSiteForPreview(connID, key)
}
// OssDownloadToTempCached 带缓存的 OSS 下载(命中缓存直接返回本地路径)
func (a *App) OssDownloadToTempCached(connID string, key string, fileSize int64, modTime string) (string, error) {
return a.ensureOssService().DownloadToTempCached(connID, key, fileSize, modTime)
}
// OssReadFile OSS 读取文件
func (a *App) OssReadFile(connID string, key string) (string, error) {
return a.ensureOssService().ReadFile(connID, key)
}
// OssWriteFile OSS 写入文件
func (a *App) OssWriteFile(connID string, key string, content string) error {
return a.ensureOssService().WriteFile(connID, key, content)
}
// OssWriteBase64File OSS 写入 base64 编码文件
func (a *App) OssWriteBase64File(connID string, key string, base64Content string) error {
return a.ensureOssService().WriteBase64File(connID, key, base64Content)
}
// OssGetFileInfo OSS 获取文件信息
func (a *App) OssGetFileInfo(connID string, key string) (map[string]interface{}, error) {
return a.ensureOssService().GetFileInfo(connID, key)
}
// OssCreateDir OSS 创建目录
func (a *App) OssCreateDir(connID string, dirPath string) (*filesystem.FileOperationResult, error) {
return a.ensureOssService().CreateDir(connID, dirPath)
}
// OssCreateFile OSS 创建文件
func (a *App) OssCreateFile(connID string, filePath string) (*filesystem.FileOperationResult, error) {
return a.ensureOssService().CreateFile(connID, filePath)
}
// OssDeletePath OSS 删除
func (a *App) OssDeletePath(connID string, key string) (*filesystem.FileOperationResult, error) {
return a.ensureOssService().DeletePath(connID, key)
}
// OssRenamePathRequest OSS 重命名请求
type OssRenamePathRequest struct {
ConnID string `json:"conn_id"`
OldPath string `json:"old_path"`
NewPath string `json:"new_path"`
}
// OssRenamePath OSS 重命名
func (a *App) OssRenamePath(req OssRenamePathRequest) (*filesystem.FileOperationResult, error) {
return a.ensureOssService().RenamePath(req.ConnID, req.OldPath, req.NewPath)
}
// OssGetCommonPaths OSS 获取常用路径
func (a *App) OssGetCommonPaths(connID string) (map[string]string, error) {
return a.ensureOssService().GetCommonPaths(connID)
}
// OssGetSignedURL OSS 获取预签名 URL
func (a *App) OssGetSignedURL(connID string, key string) (string, error) {
return a.ensureOssService().GetSignedURL(connID, key)
}
// --- 连接配置 CRUD (SQLite 持久化) ---
type SaveProfileRequest struct {
ID *uint `json:"id"`
Name string `json:"name"`
Host string `json:"host"`
Port int `json:"port"`
Username string `json:"username"`
Password string `json:"password"`
KeyPath string `json:"key_path"`
Type string `json:"type"`
Provider string `json:"provider"`
Token string `json:"token"`
AccessKey string `json:"access_key"`
SecretKey string `json:"secret_key"`
Bucket string `json:"bucket"`
Region string `json:"region"`
Endpoint string `json:"endpoint"`
LastConnected *int64 `json:"last_connected"`
}
func (a *App) ensureProfileSvc() *service.ProfileService {
a.mu.Lock()
defer a.mu.Unlock()
if a.profileSvc == nil {
a.profileSvc = service.NewProfileService()
}
return a.profileSvc
}
func (a *App) LoadConnectionProfiles() ([]map[string]interface{}, error) {
list, err := a.ensureProfileSvc().ListProfiles()
if err != nil {
return nil, err
}
var result []map[string]interface{}
data, _ := json.Marshal(list)
json.Unmarshal(data, &result)
return result, nil
}
func (a *App) SaveConnectionProfile(req SaveProfileRequest) (map[string]interface{}, error) {
p := &models.ConnectionProfile{
Name: req.Name, Host: req.Host, Port: req.Port,
Username: req.Username, Password: req.Password,
KeyPath: req.KeyPath, Type: req.Type, Provider: req.Provider, Token: req.Token,
AccessKey: req.AccessKey, SecretKey: req.SecretKey,
Bucket: req.Bucket, Region: req.Region, Endpoint: req.Endpoint,
}
if req.LastConnected != nil {
t := time.Unix(*req.LastConnected, 0)
p.LastConnected = &t
}
if req.ID != nil {
p.ID = *req.ID
}
if err := a.ensureProfileSvc().SaveProfile(p); err != nil {
return nil, err
}
return map[string]interface{}{"id": float64(p.ID), "success": true}, nil
}
func (a *App) DeleteConnectionProfile(id uint) error {
return a.ensureProfileSvc().DeleteProfile(id)
}
func (a *App) GetLocalSystemInfo() (map[string]interface{}, error) {
info := make(map[string]interface{})
cpuInfo, err := system.GetCPUInfo()
if err == nil && cpuInfo != nil {
if v, ok := cpuInfo["usage"].(string); ok {
info["cpu_usage"] = v
}
}
memInfo, err := system.GetMemoryInfo()
if err == nil && memInfo != nil {
if v, ok := memInfo["usage"].(string); ok {
info["mem_usage"] = v
}
}
diskInfos, err := system.GetDiskInfo()
if err == nil && len(diskInfos) > 0 {
if v, ok := diskInfos[0]["usage"].(string); ok {
info["disk_usage"] = v
}
}
return info, nil
}
// ========== BGM 播放列表持久化 ==========
// BgmPlaylistItem 播放列表条目
type BgmPlaylistItem struct {
Name string `json:"name"`
Path string `json:"path"`
ProfileID string `json:"profile_id"`
}
// BgmGetPlaylist 获取播放列表
func (a *App) BgmGetPlaylist() ([]BgmPlaylistItem, error) {
db := storage.GetDB()
if db == nil {
return nil, fmt.Errorf("数据库未初始化")
}
var rows []models.BgmPlaylist
db.Order("sort ASC").Find(&rows)
items := make([]BgmPlaylistItem, len(rows))
for i, r := range rows {
items[i] = BgmPlaylistItem{Name: r.Name, Path: r.Path, ProfileID: r.ProfileID}
}
return items, nil
}
// BgmSavePlaylist 全量保存播放列表(前端调用时传完整列表)
func (a *App) BgmSavePlaylist(items []BgmPlaylistItem) error {
db := storage.GetDB()
if db == nil {
return fmt.Errorf("数据库未初始化")
}
return db.Transaction(func(tx *gorm.DB) error {
if err := tx.Exec("DELETE FROM bgm_playlist").Error; err != nil {
return err
}
sort := uint(0)
for _, item := range items {
if item.Name == "" || item.Path == "" {
continue
}
tx.Create(&models.BgmPlaylist{Name: item.Name, Path: item.Path, ProfileID: item.ProfileID, Sort: sort})
sort++
}
return nil
})
}