updater.go 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  1. package main
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "os/exec"
  7. "strings"
  8. "time"
  9. )
  10. const allowScripts = "@deepseek-ai/dsh-subprocess-local,koffi,node-pty,@google/genai,protobufjs"
  11. // upToDate 判断已安装版本是否已是最新(查询失败时按"非最新"处理)。
  12. func (s *Supervisor) upToDate() (bool, string) {
  13. current := s.installedVersion()
  14. if current == "" {
  15. return false, ""
  16. }
  17. latest, err := s.LatestVersion()
  18. if err != nil {
  19. return false, current
  20. }
  21. return latest == current, current
  22. }
  23. // Update 执行全局更新:npm install -g @deepseek-ai/dsh@latest --allow-scripts=...
  24. // 已是最新版本时直接跳过——避免版本相同时仍整包重装(实测曾耗时 5 分钟)。
  25. func (s *Supervisor) Update() (string, error) {
  26. s.mu.Lock()
  27. if s.updating {
  28. s.mu.Unlock()
  29. return "", errors.New("更新已在进行中")
  30. }
  31. s.updating = true
  32. s.mu.Unlock()
  33. defer func() {
  34. s.mu.Lock()
  35. s.updating = false
  36. s.mu.Unlock()
  37. }()
  38. if fresh, current := s.upToDate(); fresh {
  39. s.log.Printf("更新:已是最新(%s),跳过安装", current)
  40. return "已是最新版本 " + current, nil
  41. }
  42. exe, args := npmCommandFor("install", "-g", dshPackage+"@latest", "--allow-scripts="+allowScripts)
  43. ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute)
  44. defer cancel()
  45. cmd := exec.CommandContext(ctx, exe, args...)
  46. hideWindow(cmd)
  47. s.log.Printf("执行更新: %s %s", exe, strings.Join(args, " "))
  48. out, err := cmd.CombinedOutput()
  49. text := strings.TrimSpace(string(out))
  50. if text != "" {
  51. s.log.Printf("更新输出: %s", text)
  52. }
  53. if err != nil {
  54. return text, fmt.Errorf("更新失败: %w", err)
  55. }
  56. s.invalidateProbe()
  57. return text, nil
  58. }
  59. // LatestVersion 查询 npm 上最新版本。
  60. func (s *Supervisor) LatestVersion() (string, error) {
  61. exe, args := npmCommandFor("view", dshPackage, "version")
  62. ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
  63. defer cancel()
  64. cmd := exec.CommandContext(ctx, exe, args...)
  65. hideWindow(cmd)
  66. out, err := cmd.Output()
  67. if err != nil {
  68. return "", err
  69. }
  70. return strings.TrimSpace(string(out)), nil
  71. }
  72. // startAutoUpdate 启动定期更新循环(进程内单例、常驻)。
  73. // 循环不因开关关闭而退出——关闭时仅暂停计时,再次开启立即恢复,
  74. // 避免"关掉再打开"后因旧循环退出而导致定期更新永久失效。
  75. func (s *Supervisor) startAutoUpdate() {
  76. s.autoMu.Lock()
  77. if s.autoLoop {
  78. s.autoMu.Unlock()
  79. return
  80. }
  81. s.autoLoop = true
  82. s.autoMu.Unlock()
  83. go func() {
  84. var elapsed time.Duration
  85. ticker := time.NewTicker(time.Minute)
  86. defer ticker.Stop()
  87. for range ticker.C {
  88. s.mu.Lock()
  89. enabled := s.cfg.AutoUpdate
  90. interval := time.Duration(s.cfg.UpdateIntervalH) * time.Hour
  91. s.mu.Unlock()
  92. if !enabled {
  93. elapsed = 0 // 关闭期间不计时
  94. continue
  95. }
  96. if interval <= 0 {
  97. interval = 24 * time.Hour
  98. }
  99. elapsed += time.Minute
  100. if elapsed < interval {
  101. continue
  102. }
  103. elapsed = 0
  104. s.runScheduledUpdate()
  105. }
  106. }()
  107. }
  108. // runScheduledUpdate 查询最新版本,有更新才安装并重启。
  109. func (s *Supervisor) runScheduledUpdate() {
  110. latest, err := s.LatestVersion()
  111. if err != nil {
  112. s.log.Printf("定期更新:查询最新版本失败: %v", err)
  113. return
  114. }
  115. current := s.installedVersion()
  116. if latest == "" || current == latest {
  117. s.log.Printf("定期更新:已是最新(%s)", current)
  118. return
  119. }
  120. s.log.Printf("定期更新:发现新版本 %s(当前 %s),开始更新", latest, current)
  121. if _, err := s.UpdateAndRestart(); err != nil {
  122. s.log.Printf("定期更新失败: %v", err)
  123. return
  124. }
  125. }