Files
shell/moltbot/installer/internal/sys/sys.go
2026-01-29 19:09:53 +08:00

739 lines
16 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package sys
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"os"
"os/exec"
"path/filepath"
"regexp"
"strconv"
"strings"
"sync"
"syscall"
"time"
)
var (
cachedNpmPrefix string
cachedNodePath string
cachedGitPath string
prefixOnce sync.Once
nodePathOnce sync.Once
gitPathOnce sync.Once
)
// MoltbotConfig 配置结构
type MoltbotConfig struct {
Gateway GatewayConfig `json:"gateway"`
Env map[string]string `json:"env,omitempty"`
Agents AgentsConfig `json:"agents"`
Models *ModelsConfig `json:"models,omitempty"`
Tools ToolsConfig `json:"tools"`
Channels ChannelsConfig `json:"channels"`
}
type GatewayConfig struct {
Mode string `json:"mode"`
Bind string `json:"bind"`
Port int `json:"port"`
}
type AgentsConfig struct {
Defaults AgentDefaults `json:"defaults"`
}
type AgentDefaults struct {
Model ModelRef `json:"model"`
ElevatedDefault string `json:"elevatedDefault,omitempty"`
Compaction *CompactionConfig `json:"compaction,omitempty"`
MaxConcurrent int `json:"maxConcurrent,omitempty"`
}
type ModelRef struct {
Primary string `json:"primary"`
}
type CompactionConfig struct {
Mode string `json:"mode"`
}
type ModelsConfig struct {
Mode string `json:"mode"`
Providers map[string]ProviderConfig `json:"providers"`
}
type ProviderConfig struct {
BaseURL string `json:"baseUrl"`
APIKey string `json:"apiKey"`
API string `json:"api"`
Models []ModelEntry `json:"models"`
}
type ModelEntry struct {
ID string `json:"id"`
Name string `json:"name"`
}
type ToolsConfig struct {
Exec *ExecConfig `json:"exec,omitempty"`
Elevated ElevatedConfig `json:"elevated"`
Allow []string `json:"allow"`
}
type ExecConfig struct {
BackgroundMs int `json:"backgroundMs"`
TimeoutSec int `json:"timeoutSec"`
CleanupMs int `json:"cleanupMs"`
NotifyOnExit bool `json:"notifyOnExit"`
}
type ElevatedConfig struct {
Enabled bool `json:"enabled"`
AllowFrom map[string][]string `json:"allowFrom"`
}
type ChannelsConfig struct {
Telegram TelegramConfig `json:"telegram"`
}
type TelegramConfig struct {
Enabled bool `json:"enabled"`
BotToken string `json:"botToken"`
DMPolicy string `json:"dmPolicy"`
AllowFrom []string `json:"allowFrom"`
}
// GetMoltbotPath 获取执行路径
func GetMoltbotPath() (string, error) {
if path, err := exec.LookPath("clawdbot"); err == nil {
return path, nil
}
if path, err := exec.LookPath("moltbot"); err == nil {
return path, nil
}
npmPrefix, err := getNpmPrefix()
if err != nil {
return "", err
}
possibleClawd := filepath.Join(npmPrefix, "clawdbot.cmd")
if _, err := os.Stat(possibleClawd); err == nil {
return possibleClawd, nil
}
possibleMolt := filepath.Join(npmPrefix, "moltbot.cmd")
if _, err := os.Stat(possibleMolt); err == nil {
return possibleMolt, nil
}
return "", fmt.Errorf("未找到 moltbot 或 clawdbot 可执行文件")
}
// GetNodePath 获取 Node 路径
func GetNodePath() (string, error) {
var err error
nodePathOnce.Do(func() {
if path, e := exec.LookPath("node"); e == nil {
cachedNodePath = path
return
}
defaultPath := `C:\Program Files\nodejs\node.exe`
if _, e := os.Stat(defaultPath); e == nil {
cachedNodePath = defaultPath
return
}
err = fmt.Errorf("未找到 Node.js")
})
if err != nil {
return "", err
}
if cachedNodePath != "" {
return cachedNodePath, nil
}
return "", fmt.Errorf("未找到 Node.js")
}
// GetGitPath 获取 Git 路径
func GetGitPath() (string, error) {
var err error
gitPathOnce.Do(func() {
if path, e := exec.LookPath("git"); e == nil {
cachedGitPath = path
return
}
defaultPaths := []string{
`C:\Program Files\Git\cmd\git.exe`,
`C:\Program Files\Git\bin\git.exe`,
}
for _, p := range defaultPaths {
if _, e := os.Stat(p); e == nil {
cachedGitPath = p
return
}
}
err = fmt.Errorf("未找到 Git")
})
if err != nil {
return "", err
}
if cachedGitPath != "" {
return cachedGitPath, nil
}
return "", fmt.Errorf("未找到 Git")
}
// SetupNodeEnv 配置 Node 环境变量
func SetupNodeEnv() error {
nodeExe, err := GetNodePath()
if err != nil {
return err
}
nodeDir := filepath.Dir(nodeExe)
pathEnv := os.Getenv("PATH")
if strings.Contains(strings.ToLower(pathEnv), strings.ToLower(nodeDir)) {
return nil
}
newPath := nodeDir + string(os.PathListSeparator) + pathEnv
if npmPrefix, err := getNpmPrefix(); err == nil {
if !strings.Contains(strings.ToLower(newPath), strings.ToLower(npmPrefix)) {
newPath = npmPrefix + string(os.PathListSeparator) + newPath
}
}
return os.Setenv("PATH", newPath)
}
// SetupGitEnv 配置 Git 环境变量
func SetupGitEnv() error {
gitExe, err := GetGitPath()
if err != nil {
return err
}
gitDir := filepath.Dir(gitExe)
pathEnv := os.Getenv("PATH")
if strings.Contains(strings.ToLower(pathEnv), strings.ToLower(gitDir)) {
return nil
}
newPath := gitDir + string(os.PathListSeparator) + pathEnv
return os.Setenv("PATH", newPath)
}
// CheckMoltbot 检查安装状态
func CheckMoltbot() (string, bool) {
SetupNodeEnv()
cmdName, err := GetMoltbotPath()
if err != nil {
return "", false
}
cmd := exec.Command("cmd", "/c", cmdName, "--version")
out, err := cmd.Output()
if err != nil {
return "", false
}
return strings.TrimSpace(string(out)), true
}
// CheckNode 检查 Node 版本
func CheckNode() (string, bool) {
nodePath, err := GetNodePath()
if err != nil {
return "", false
}
cmd := exec.Command(nodePath, "-v")
out, err := cmd.Output()
if err != nil {
return "", false
}
versionStr := strings.TrimSpace(string(out))
re := regexp.MustCompile(`v(\d+)\.`)
matches := re.FindStringSubmatch(versionStr)
if len(matches) < 2 {
return versionStr, false
}
majorVer, err := strconv.Atoi(matches[1])
if err != nil {
return versionStr, false
}
if majorVer >= 22 {
return versionStr, true
}
return versionStr, false
}
// CheckGit 检查 Git 状态
func CheckGit() (string, bool) {
gitPath, err := GetGitPath()
if err != nil {
return "", false
}
cmd := exec.Command(gitPath, "--version")
out, err := cmd.Output()
if err != nil {
return "", false
}
return strings.TrimSpace(string(out)), true
}
// getNpmPath 获取 npm
func getNpmPath() (string, error) {
path, err := exec.LookPath("npm")
if err == nil {
return path, nil
}
defaultPath := `C:\Program Files\nodejs\npm.cmd`
if _, err := os.Stat(defaultPath); err == nil {
return defaultPath, nil
}
return "", fmt.Errorf("未找到 npm请确认 Node.js 安装成功")
}
func getNpmPrefix() (string, error) {
var err error
prefixOnce.Do(func() {
npmPath, e := getNpmPath()
if e != nil {
err = fmt.Errorf("无法定位 npm: %v", e)
return
}
cmd := exec.Command(npmPath, "config", "get", "prefix")
out, e := cmd.Output()
if e != nil {
err = fmt.Errorf("无法获取 npm prefix: %v", e)
return
}
cachedNpmPrefix = strings.TrimSpace(string(out))
})
if err != nil {
return "", err
}
return cachedNpmPrefix, nil
}
// ConfigureNpmMirror 配置镜像
func ConfigureNpmMirror() error {
npmPath, err := getNpmPath()
if err != nil {
return err
}
cmd := exec.Command(npmPath, "config", "set", "registry", "https://registry.npmmirror.com/")
if err := cmd.Run(); err != nil {
return fmt.Errorf("设置 npm 镜像失败: %v", err)
}
return nil
}
// downloadFile 下载文件
func downloadFile(url, dest string) error {
if info, err := os.Stat(dest); err == nil && info.Size() > 10000000 {
return nil
}
fmt.Printf("正在下载: %s\n", url)
resp, err := http.Get(url)
if err != nil {
return fmt.Errorf("下载失败: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("下载失败,状态码: %d", resp.StatusCode)
}
out, err := os.Create(dest)
if err != nil {
return fmt.Errorf("创建文件失败: %v", err)
}
defer out.Close()
_, err = io.Copy(out, resp.Body)
if err != nil {
return fmt.Errorf("写入文件失败: %v", err)
}
return nil
}
// InstallNode 安装 Node.js
func InstallNode() error {
if _, ok := CheckNode(); ok {
return nil
}
msiUrl := "https://nodejs.org/dist/v24.13.0/node-v24.13.0-x64.msi"
tempDir := os.TempDir()
msiPath := filepath.Join(tempDir, "node-v24.13.0-x64.msi")
if err := downloadFile(msiUrl, msiPath); err != nil {
return err
}
fmt.Println("正在安装 Node.js (可能需要管理员权限)...")
for i := 0; i < 3; i++ {
installCmd := exec.Command("msiexec", "/i", msiPath, "/qn")
output, err := installCmd.CombinedOutput()
if err == nil {
break
}
outStr := string(output)
if strings.Contains(outStr, "1618") {
time.Sleep(5 * time.Second)
continue
}
if strings.Contains(outStr, "1619") {
return fmt.Errorf("安装包损坏 (Error 1619). 请尝试手动下载: %s", msiUrl)
}
if i == 2 {
return fmt.Errorf("安装失败: %v, Output: %s", err, outStr)
}
time.Sleep(2 * time.Second)
}
SetupNodeEnv()
return nil
}
// InstallGit 安装 Git
func InstallGit() error {
if _, ok := CheckGit(); ok {
return nil
}
gitUrl := "https://github.com/git-for-windows/git/releases/download/v2.52.0.windows.1/Git-2.52.0-64-bit.exe"
tempDir := os.TempDir()
exePath := filepath.Join(tempDir, "Git-2.52.0-64-bit.exe")
fmt.Println("正在下载 Git...")
if err := downloadFile(gitUrl, exePath); err != nil {
return fmt.Errorf("git 下载失败: %v", err)
}
fmt.Println("正在安装 Git (可能需要管理员权限)...")
installCmd := exec.Command(exePath,
"/VERYSILENT",
"/NORESTART",
"/NOCANCEL",
"/SP-",
"/CLOSEAPPLICATIONS",
"/RESTARTAPPLICATIONS",
"/o:PathOption=Cmd",
)
if out, err := installCmd.CombinedOutput(); err != nil {
return fmt.Errorf("git 安装失败: %v, Output: %s", err, string(out))
}
SetupGitEnv()
return nil
}
// InstallMoltbotNpm 安装包
func InstallMoltbotNpm(tag string) error {
SetupNodeEnv()
pkgName := "clawdbot"
if tag == "" || tag == "beta" {
tag = "latest"
}
npmPath, err := getNpmPath()
if err != nil {
return err
}
os.Setenv("NPM_CONFIG_LOGLEVEL", "error")
os.Setenv("NPM_CONFIG_UPDATE_NOTIFIER", "false")
os.Setenv("NPM_CONFIG_FUND", "false")
os.Setenv("NPM_CONFIG_AUDIT", "false")
cmd := exec.Command(npmPath, "install", "-g", fmt.Sprintf("%s@%s", pkgName, tag))
cmd.Stdout = nil
cmd.Stderr = nil
if err := cmd.Run(); err != nil {
return err
}
return nil
}
// EnsureOnPath 检查并配置 PATH
func EnsureOnPath() (bool, error) {
if _, err := exec.LookPath("clawdbot"); err == nil {
return false, nil
}
if _, err := exec.LookPath("moltbot"); err == nil {
return false, nil
}
npmPrefix, err := getNpmPrefix()
if err != nil {
return false, err
}
npmBin := filepath.Join(npmPrefix, "bin")
possiblePath := npmPrefix
if _, err := os.Stat(filepath.Join(npmPrefix, "clawdbot.cmd")); os.IsNotExist(err) {
if _, err := os.Stat(filepath.Join(npmPrefix, "moltbot.cmd")); os.IsNotExist(err) {
possiblePath = npmBin
}
}
psCmd := fmt.Sprintf(`
$userPath = [Environment]::GetEnvironmentVariable("Path", "User")
if (-not ($userPath -split ";" | Where-Object { $_ -ieq "%s" })) {
[Environment]::SetEnvironmentVariable("Path", "$userPath;%s", "User")
}
`, possiblePath, possiblePath)
exec.Command("powershell", "-Command", psCmd).Run()
return true, nil
}
// RunDoctor 运行诊断
func RunDoctor() error {
cmdName, err := GetMoltbotPath()
if err != nil {
cmdName = "moltbot"
}
cmd := exec.Command("cmd", "/c", cmdName, "doctor", "--non-interactive")
return cmd.Run()
}
// RunOnboard 运行引导
func RunOnboard() error {
cmdName, err := GetMoltbotPath()
if err != nil {
cmdName = "moltbot"
}
cmd := exec.Command("cmd", "/c", cmdName, "onboard")
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
// ConfigOptions 配置选项
type ConfigOptions struct {
ApiType string
BotToken string
AdminID string
AnthropicKey string
OpenAIBaseURL string
OpenAIKey string
OpenAIModel string
}
// GenerateAndWriteConfig 生成配置
func GenerateAndWriteConfig(opts ConfigOptions) error {
userHome, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("无法获取用户目录: %v", err)
}
configDir := filepath.Join(userHome, ".clawdbot")
if err := os.MkdirAll(configDir, 0755); err != nil {
return fmt.Errorf("创建配置目录失败: %v", err)
}
configFile := filepath.Join(configDir, "clawdbot.json")
config := MoltbotConfig{
Gateway: GatewayConfig{
Mode: "local",
Bind: "loopback",
Port: 18789,
},
Tools: ToolsConfig{
Elevated: ElevatedConfig{
Enabled: true,
AllowFrom: map[string][]string{},
},
Allow: []string{"exec", "process", "read", "write", "edit", "web_search", "web_fetch", "cron"},
},
Channels: ChannelsConfig{
Telegram: TelegramConfig{
Enabled: false,
DMPolicy: "pairing",
AllowFrom: []string{},
},
},
}
if opts.BotToken != "" {
config.Channels.Telegram.Enabled = true
config.Channels.Telegram.BotToken = opts.BotToken
if opts.AdminID != "" {
config.Channels.Telegram.AllowFrom = []string{opts.AdminID}
config.Tools.Elevated.AllowFrom["telegram"] = []string{opts.AdminID}
}
}
if opts.ApiType == "anthropic" {
config.Env = map[string]string{
"ANTHROPIC_API_KEY": opts.AnthropicKey,
}
config.Agents = AgentsConfig{
Defaults: AgentDefaults{
Model: ModelRef{
Primary: "anthropic/claude-opus-4-5",
},
},
}
} else if opts.ApiType == "skip" {
config.Channels.Telegram.Enabled = false
config.Agents = AgentsConfig{
Defaults: AgentDefaults{
Model: ModelRef{
Primary: "anthropic/claude-opus-4-5",
},
},
}
} else {
config.Agents = AgentsConfig{
Defaults: AgentDefaults{
Model: ModelRef{
Primary: fmt.Sprintf("openai-compat/%s", opts.OpenAIModel),
},
ElevatedDefault: "full",
Compaction: &CompactionConfig{
Mode: "safeguard",
},
MaxConcurrent: 4,
},
}
config.Models = &ModelsConfig{
Mode: "merge",
Providers: map[string]ProviderConfig{
"openai-compat": {
BaseURL: opts.OpenAIBaseURL,
APIKey: opts.OpenAIKey,
API: "openai-completions",
Models: []ModelEntry{
{ID: opts.OpenAIModel, Name: opts.OpenAIModel},
},
},
},
}
config.Tools.Exec = &ExecConfig{
BackgroundMs: 10000,
TimeoutSec: 1800,
CleanupMs: 1800000,
NotifyOnExit: true,
}
}
data, err := json.MarshalIndent(config, "", " ")
if err != nil {
return fmt.Errorf("序列化配置失败: %v", err)
}
return os.WriteFile(configFile, data, 0644)
}
// StartGateway 启动网关
func StartGateway() error {
cmdName, err := GetMoltbotPath()
if err != nil {
cmdName = "moltbot"
}
cmd := exec.Command(cmdName, "gateway", "--verbose")
cmd.SysProcAttr = &syscall.SysProcAttr{
HideWindow: true,
CreationFlags: 0x08000000,
}
return cmd.Start()
}
// IsGatewayRunning 检查端口
func IsGatewayRunning() bool {
conn, err := net.DialTimeout("tcp", "127.0.0.1:18789", 500*time.Millisecond)
if err == nil {
conn.Close()
return true
}
return false
}
// KillGateway 停止网关
func KillGateway() error {
cmd := exec.Command("netstat", "-ano")
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
out, err := cmd.Output()
if err != nil {
return err
}
scanner := bufio.NewScanner(bytes.NewReader(out))
var pid string
for scanner.Scan() {
line := scanner.Text()
if strings.Contains(line, ":18789") && strings.Contains(line, "LISTENING") {
fields := strings.Fields(line)
if len(fields) > 0 {
pid = fields[len(fields)-1]
break
}
}
}
if pid == "" {
return nil
}
killCmd := exec.Command("taskkill", "/F", "/PID", pid)
killCmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
return killCmd.Run()
}
// UninstallMoltbot 卸载清理
func UninstallMoltbot() error {
npmPath, err := getNpmPath()
if err != nil {
return err
}
packages := []string{"clawdbot", "moltbot"}
for _, pkg := range packages {
cmd := exec.Command(npmPath, "uninstall", "-g", pkg)
cmd.Stdout = nil
cmd.Stderr = nil
cmd.Run()
}
userHome, err := os.UserHomeDir()
if err == nil {
configDir := filepath.Join(userHome, ".clawdbot")
os.RemoveAll(configDir)
legacyDir := filepath.Join(userHome, ".moltbot")
os.RemoveAll(legacyDir)
}
return nil
}