package main import ( "context" "errors" "fmt" "os/exec" "strings" "time" ) const allowScripts = "@deepseek-ai/dsh-subprocess-local,koffi,node-pty,@google/genai,protobufjs" // 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 } // Update 执行全局更新:npm install -g @deepseek-ai/dsh@latest --allow-scripts=... // 已是最新版本时直接跳过——避免版本相同时仍整包重装(实测曾耗时 5 分钟)。 func (s *Supervisor) Update() (string, error) { s.mu.Lock() if s.updating { s.mu.Unlock() return "", errors.New("更新已在进行中") } s.updating = true s.mu.Unlock() defer func() { s.mu.Lock() s.updating = false s.mu.Unlock() }() if fresh, current := s.upToDate(); fresh { s.log.Printf("更新:已是最新(%s),跳过安装", current) return "已是最新版本 " + current, nil } 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 查询最新版本,有更新才安装并重启。 func (s *Supervisor) runScheduledUpdate() { latest, err := s.LatestVersion() if err != nil { s.log.Printf("定期更新:查询最新版本失败: %v", err) return } current := s.installedVersion() if latest == "" || current == latest { s.log.Printf("定期更新:已是最新(%s)", current) return } s.log.Printf("定期更新:发现新版本 %s(当前 %s),开始更新", latest, current) if _, err := s.UpdateAndRestart(); err != nil { s.log.Printf("定期更新失败: %v", err) return } }