package main import ( "context" "encoding/json" "errors" "fmt" "os" "os/exec" "path/filepath" "strings" "time" ) const ( dshPackage = "@deepseek-ai/dsh" allowScripts = "@deepseek-ai/dsh-subprocess-local,koffi,node-pty,@google/genai,protobufjs" ) // npmGlobalRoot 执行 npm root -g(隐藏窗口),返回全局 node_modules 路径。 func npmGlobalRoot() string { exe, args := npmCommandFor("root", "-g") cmd := exec.Command(exe, args...) hideWindow(cmd) out, err := cmd.Output() if err != nil { return "" } return strings.TrimSpace(string(out)) } // npmCommandFor 组装 npm 调用:优先 node + npm-cli.js,回退 cmd /c npm。 func npmCommandFor(args ...string) (string, []string) { if node, err := exec.LookPath("node"); err == nil { if cli := detectNpmCLI(node); cli != "" { return node, append([]string{cli}, args...) } } return "cmd.exe", append([]string{"/c", "npm"}, args...) } func detectNpmCLI(node string) string { base := filepath.Dir(node) candidates := []string{ filepath.Join(base, "node_modules", "npm", "bin", "npm-cli.js"), filepath.Join(base, "..", "lib", "node_modules", "npm", "bin", "npm-cli.js"), } for _, c := range candidates { if fileExists(c) { return c } } return "" } // readPackageVersion 读取 package.json 的 version。 func readPackageVersion(path string) string { data, err := os.ReadFile(path) if err != nil { return "" } var v struct { Version string `json:"version"` } if err := json.Unmarshal(data, &v); err != nil { return "" } return v.Version } // Update 执行全局更新:npm install -g @deepseek-ai/dsh@latest --allow-scripts=... 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() }() 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.mu.Lock() s.binCache = "" s.mu.Unlock() 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() { go func() { for { s.mu.Lock() enabled := s.cfg.AutoUpdate hours := s.cfg.UpdateIntervalH s.mu.Unlock() if !enabled { s.log.Printf("定期更新:已关闭") return } if hours <= 0 { hours = 24 } time.Sleep(time.Duration(hours) * time.Hour) s.mu.Lock() enabled = s.cfg.AutoUpdate s.mu.Unlock() if !enabled { return } latest, err := s.LatestVersion() if err != nil { s.log.Printf("定期更新:查询最新版本失败: %v", err) continue } current := s.installedVersion() if latest == "" || current == latest { s.log.Printf("定期更新:已是最新(%s)", current) continue } s.log.Printf("定期更新:发现新版本 %s(当前 %s),开始更新", latest, current) if _, err := s.Update(); err != nil { s.log.Printf("定期更新失败: %v", err) continue } s.Restart() } }() }