package main import ( _ "embed" "encoding/json" "fmt" "log" "os" "os/exec" "path/filepath" "runtime" "strconv" "sync" "time" "github.com/jchv/go-webview2" ) //go:embed web/update.html var updateHTML string //go:embed updater.exe var updaterExe []byte type PackageInfo struct { URL string `json:"url"` Size int64 `json:"size"` SHA256 string `json:"sha256"` } type UpdateInfo struct { Version string `json:"version"` Force bool `json:"force"` ReleaseNotes string `json:"release_notes"` Package PackageInfo `json:"package"` } var ( updateMu sync.Mutex updateOpen bool updateInfo *UpdateInfo ) const defaultUpdateURL = "https://c.1216.top/u-desktop/version.json" func getUpdateURL() string { cfg := loadConfig() if cfg.UpdateURL != "" { return cfg.UpdateURL } return defaultUpdateURL } func checkUpdate() (*UpdateInfo, error) { resp, err := httpClient.Get(getUpdateURL()) if err != nil { return nil, fmt.Errorf("请求 version.json 失败: %w", err) } defer resp.Body.Close() if resp.StatusCode != 200 { return nil, fmt.Errorf("version.json 返回状态 %d", resp.StatusCode) } var info UpdateInfo if err := json.NewDecoder(resp.Body).Decode(&info); err != nil { return nil, fmt.Errorf("解析 version.json 失败: %w", err) } if info.Version == "" || info.Version == version || info.Version == "dev" { return nil, nil } // 用户跳过过此版本且非强制 cfg := loadConfig() if !info.Force && cfg.SkipVersion == info.Version { return nil, nil } return &info, nil } func runAutoUpdateCheck() { cfg := loadConfig() if !cfg.AutoCheckUpdate { return } time.Sleep(3 * time.Second) info, err := checkUpdate() if err != nil { log.Println("自动更新检查失败:", err) return } if info != nil { showUpdateWindow(info) } } func showUpdateWindow(info *UpdateInfo) { updateMu.Lock() if updateOpen { updateMu.Unlock() return } updateOpen = true updateInfo = info updateMu.Unlock() go func() { runtime.LockOSThread() defer func() { updateMu.Lock() updateOpen = false updateInfo = nil updateMu.Unlock() runtime.UnlockOSThread() }() winW := 340 winH := 220 if info.Force { winW = 420 winH = 260 } dataDir := filepath.Join(configDir(), "update-data") os.MkdirAll(dataDir, 0755) w := webview2.NewWithOptions(webview2.WebViewOptions{ AutoFocus: true, DataPath: dataDir, WindowOptions: webview2.WindowOptions{ Title: "更新提示", Width: uint(winW), Height: uint(winH), }, }) if w == nil { log.Println("更新窗口: 创建失败") return } hwnd := uintptr(w.Window()) // 无边框 style, _, _ := procGetWindowLongPtrW.Call(hwnd, gwlStyle) procSetWindowLongPtrW.Call(hwnd, gwlStyle, style & ^uintptr(wsSizebox|wsMaxbox|0x00C00000)) // 置顶 + 工具窗口 procSetWindowLongPtrW.Call(hwnd, gwlExStyle, wsExToolwindow|0x00000008) // 定位窗口 screenW, screenH := getScreenSize() if info.Force { x := (int(screenW) - winW) / 2 y := (int(screenH) - winH) / 2 procMoveWindow.Call(hwnd, uintptr(x), uintptr(y), uintptr(winW), uintptr(winH), 1) } else { x := int(screenW) - winW - 20 y := int(screenH) - winH - 60 procMoveWindow.Call(hwnd, uintptr(x), uintptr(y), uintptr(winW), uintptr(winH), 1) } w.Bind("getUpdateInfo", func() string { if updateInfo == nil { return "{}" } data, _ := json.Marshal(map[string]interface{}{ "version": updateInfo.Version, "force": updateInfo.Force, "releaseNotes": updateInfo.ReleaseNotes, "size": updateInfo.Package.Size, "currentVersion": version, }) return string(data) }) w.Bind("skipUpdate", func() string { if updateInfo != nil && !updateInfo.Force { cfg := loadConfig() cfg.SkipVersion = updateInfo.Version saveConfig(cfg) } return "" }) w.Bind("startUpdate", func() string { if updateInfo == nil { return "" } go doStartUpdate(updateInfo) return "" }) w.SetHtml(updateHTML) log.Println("更新窗口已打开") w.Run() log.Println("更新窗口已关闭") }() } func doStartUpdate(info *UpdateInfo) { exePath, err := os.Executable() if err != nil { log.Println("获取自身路径失败:", err) return } exeDir := filepath.Dir(exePath) // 释放嵌入的 updater.exe 到临时目录 updaterPath := filepath.Join(os.TempDir(), "u-desktop-updater.exe") if err := os.WriteFile(updaterPath, updaterExe, 0755); err != nil { log.Println("释放 updater.exe 失败:", err) showError("释放升级器失败: " + err.Error()) return } backupDir := filepath.Join(exeDir, "backup") os.MkdirAll(backupDir, 0755) args := []string{ "--package-url", info.Package.URL, "--sha256", info.Package.SHA256, "--target", exePath, "--version", info.Version, "--backup-dir", backupDir, } if info.Force { args = append(args, "--force") } cmd := exec.Command(updaterPath, args...) cmd.Stdout = nil cmd.Stderr = nil if err := cmd.Start(); err != nil { log.Println("启动 updater 失败:", err) return } log.Println("updater 已启动, 主程序退出") os.Exit(0) } func manualCheckUpdate() { info, err := checkUpdate() if err != nil { log.Println("手动更新检查失败:", err) showError("检查更新失败: " + err.Error()) return } if info == nil { showError("当前已是最新版本 (" + version + ")") return } showUpdateWindow(info) } func formatSize(size int64) string { const ( KB = 1024 MB = KB * 1024 ) switch { case size >= MB: return strconv.FormatFloat(float64(size)/float64(MB), 'f', 1, 64) + " MB" case size >= KB: return strconv.FormatFloat(float64(size)/float64(KB), 'f', 1, 64) + " KB" default: return strconv.FormatInt(size, 10) + " B" } }