package main import ( "context" "errors" "fmt" "os/exec" "strings" "time" ) const allowScripts = "@deepseek-ai/dsh-subprocess-local,koffi,node-pty,@google/genai,protobufjs" // beginUpdate 进入更新态(防重入,同时让 UI 显示"正在更新");已在更新中返回 false。 func (s *Supervisor) beginUpdate() bool { s.mu.Lock() defer s.mu.Unlock() if s.updating { return false } s.updating = true return true } func (s *Supervisor) endUpdate() { s.mu.Lock() s.updating = false s.mu.Unlock() } // upToDate 判断已安装版本是否已是最新(查询失败时按"非最新"处理)。 func (s *Supervisor) upToDate() (bool, string) { current := s.installedVersion() if current == "" { return false, "" } latest, err := s.LatestVersion() if err != nil { return false, current } return latest == current, current } // UpdateAndRestart 更新流程的唯一入口: // 检查版本 -> 必要时停服务 -> 安装 -> 恢复运行。 // 全程处于"更新中"状态(UI 可见);已是最新则既不安装也不重启。 func (s *Supervisor) UpdateAndRestart() (string, error) { if !s.beginUpdate() { return "", errors.New("更新已在进行中") } defer s.endUpdate() s.log.Printf("更新:检查最新版本…") if fresh, current := s.upToDate(); fresh { s.log.Printf("更新:已是最新(%s),跳过安装与重启", current) return "已是最新版本 " + current, nil } wasDesired := s.isDesired() if wasDesired { s.Stop() } out, err := s.installLatest() if wasDesired { s.Start() } return out, err } // installLatest 执行 npm 全局安装(不做版本检查,由调用方负责)。 func (s *Supervisor) installLatest() (string, error) { exe, args := npmCommandFor("install", "-g", dshPackage+"@latest", "--allow-scripts="+allowScripts) ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute) defer cancel() cmd := exec.CommandContext(ctx, exe, args...) hideWindow(cmd) s.log.Printf("执行更新: %s %s", exe, strings.Join(args, " ")) out, err := cmd.CombinedOutput() text := strings.TrimSpace(string(out)) if text != "" { s.log.Printf("更新输出: %s", text) } if err != nil { return text, fmt.Errorf("更新失败: %w", err) } s.invalidateProbe() return text, nil } // LatestVersion 查询 npm 上最新版本。 func (s *Supervisor) LatestVersion() (string, error) { exe, args := npmCommandFor("view", dshPackage, "version") ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) defer cancel() cmd := exec.CommandContext(ctx, exe, args...) hideWindow(cmd) out, err := cmd.Output() if err != nil { return "", err } return strings.TrimSpace(string(out)), nil } // startAutoUpdate 启动定期更新循环(进程内单例、常驻)。 // 循环不因开关关闭而退出——关闭时仅暂停计时,再次开启立即恢复, // 避免"关掉再打开"后因旧循环退出而导致定期更新永久失效。 func (s *Supervisor) startAutoUpdate() { s.autoMu.Lock() if s.autoLoop { s.autoMu.Unlock() return } s.autoLoop = true s.autoMu.Unlock() go func() { var elapsed time.Duration ticker := time.NewTicker(time.Minute) defer ticker.Stop() for range ticker.C { s.mu.Lock() enabled := s.cfg.AutoUpdate interval := time.Duration(s.cfg.UpdateIntervalH) * time.Hour s.mu.Unlock() if !enabled { elapsed = 0 // 关闭期间不计时 continue } if interval <= 0 { interval = 24 * time.Hour } elapsed += time.Minute if elapsed < interval { continue } elapsed = 0 s.runScheduledUpdate() } }() } // runScheduledUpdate 定期检查并更新(版本比对在 UpdateAndRestart 内完成一次)。 func (s *Supervisor) runScheduledUpdate() { s.log.Printf("定期更新:检查最新版本…") text, err := s.UpdateAndRestart() if err != nil { s.log.Printf("定期更新失败: %v", err) return } s.log.Printf("定期更新完成: %s", firstLine(text)) }