新增: 自动升级体系 + WorkerW重建检测 + 知识卡片优化
自动升级: - 版本变量注入 (-ldflags -X main.version) - 远程 version.json 检查 + 弹窗提示(非强制右下角/强制居中) - updater.exe 嵌入主程序, 运行时释放到临时目录 - 下载+SHA256校验+备份+替换+回滚 全流程 - 托盘菜单"检查更新" 稳定性: - WorkerW 重建自动检测(每10s)并重新嵌入 - 左键托盘点击防抖 - 设置窗口已打开时正确前置 优化: - 知识卡片prompt改为面向开发者的硬核内容 - 配置目录统一到 ~/.u-desktop/ - 设置窗口WebView2数据目录改为configDir下
This commit is contained in:
271
updater.go
Normal file
271
updater.go
Normal file
@@ -0,0 +1,271 @@
|
||||
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"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user