Files
u-desktop/updater.go
绝尘 f5a473c4b8 新增: 自动升级体系 + WorkerW重建检测 + 知识卡片优化
自动升级:
- 版本变量注入 (-ldflags -X main.version)
- 远程 version.json 检查 + 弹窗提示(非强制右下角/强制居中)
- updater.exe 嵌入主程序, 运行时释放到临时目录
- 下载+SHA256校验+备份+替换+回滚 全流程
- 托盘菜单"检查更新"

稳定性:
- WorkerW 重建自动检测(每10s)并重新嵌入
- 左键托盘点击防抖
- 设置窗口已打开时正确前置

优化:
- 知识卡片prompt改为面向开发者的硬核内容
- 配置目录统一到 ~/.u-desktop/
- 设置窗口WebView2数据目录改为configDir下
2026-05-28 19:06:26 +08:00

272 lines
5.7 KiB
Go

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"
}
}