Files
shell/moltbot/installer/internal/sys/sys.go
T
2026-01-30 01:04:12 +08:00

1093 lines
24 KiB
Go

package sys
import (
"bufio"
"bytes"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"os"
"os/exec"
"path/filepath"
"regexp"
"strconv"
"strings"
"sync"
"sync/atomic"
"syscall"
"time"
)
var (
cachedNpmPrefix string
cachedNodePath string
cachedGitPath string
prefixOnce sync.Once
nodePathOnce sync.Once
gitPathOnce sync.Once
)
const (
downloadConcurrentThreshold int64 = 20 * 1024 * 1024
downloadConcurrentParts = 4
)
const gitProxyEnv = "GIT_PROXY"
const gitProxyDefault = "https://g.blfrp.cn/"
func gitProxy() string {
proxy := strings.TrimSpace(os.Getenv(gitProxyEnv))
if proxy == "" {
proxy = gitProxyDefault
}
if !strings.HasSuffix(proxy, "/") {
proxy += "/"
}
return proxy
}
// SHA256 来源 https://nodejs.org/dist/v24.13.0/SHASUMS256.txt.asc
const nodeMsiSHA256 = "1a5f0cd914386f3be2fbaf03ad9fff808a588ce50d2e155f338fad5530575f18"
// SHA256 来源 https://github.com/git-for-windows/git/releases/tag/v2.52.0.windows.1
const gitExeSHA256 = "d8de7a3152266c8bb13577eab850ea1df6dccf8c2aa48be5b4a1c58b7190d62c"
// 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
}
func ResetPathCache() {
cachedNpmPrefix = ""
cachedNodePath = ""
cachedGitPath = ""
prefixOnce = sync.Once{}
nodePathOnce = sync.Once{}
gitPathOnce = sync.Once{}
}
// 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
}
func ConfigureGitProxy() error {
gitPath, err := GetGitPath()
if err != nil {
return err
}
proxy := gitProxy()
key := fmt.Sprintf("url.%shttps://github.com/.insteadOf", proxy)
cmd := exec.Command(gitPath, "config", "--global", key, "https://github.com/")
if err := cmd.Run(); err != nil {
return fmt.Errorf("设置 git 代理失败: %v", err)
}
return nil
}
// downloadFile 下载文件
func downloadFile(url, dest, expectedSHA256 string) error {
if ok, err := verifyFileSHA256(dest, expectedSHA256); err == nil && ok {
return nil
}
partPath := dest + ".part"
if ok, err := verifyFileSHA256(partPath, expectedSHA256); err == nil && ok {
_ = os.Remove(dest)
return os.Rename(partPath, dest)
}
_ = os.Remove(dest)
size, acceptRanges, err := probeRemoteFile(url)
if err != nil {
return err
}
if err := downloadWithResume(url, partPath, size, acceptRanges); err != nil {
return err
}
if ok, err := verifyFileSHA256(partPath, expectedSHA256); err != nil || !ok {
_ = os.Remove(partPath)
if err != nil {
return err
}
return fmt.Errorf("下载文件校验失败")
}
_ = os.Remove(dest)
return os.Rename(partPath, dest)
}
func downloadWithResume(url, dest string, size int64, acceptRanges bool) error {
if size > 0 && acceptRanges {
if info, err := os.Stat(dest); err == nil && info.Size() > 0 && info.Size() < size {
return downloadRange(url, dest, info.Size(), size-1, size)
}
if size >= downloadConcurrentThreshold {
return downloadConcurrent(url, dest, size, downloadConcurrentParts)
}
}
return downloadRange(url, dest, 0, -1, size)
}
func downloadRange(url, dest string, start, end, total int64) error {
out, err := os.OpenFile(dest, os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return fmt.Errorf("创建文件失败: %v", err)
}
defer out.Close()
if start > 0 {
if _, err := out.Seek(start, 0); err != nil {
return fmt.Errorf("定位文件失败: %v", err)
}
}
client := &http.Client{Timeout: 30 * time.Minute}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return fmt.Errorf("创建请求失败: %v", err)
}
if start > 0 || end >= 0 {
if end >= start && end >= 0 {
req.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", start, end))
} else {
req.Header.Set("Range", fmt.Sprintf("bytes=%d-", start))
}
}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("下载失败: %v", err)
}
defer resp.Body.Close()
if start > 0 && resp.StatusCode != http.StatusPartialContent {
return fmt.Errorf("不支持断点续传,状态码: %d", resp.StatusCode)
}
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusPartialContent {
return fmt.Errorf("下载失败,状态码: %d", resp.StatusCode)
}
if total <= 0 && resp.ContentLength > 0 {
total = start + resp.ContentLength
}
progress := newProgressReporter(total, start)
progress.Start()
reader := &countingReader{r: resp.Body, written: progress.written}
if _, err = io.Copy(out, reader); err != nil {
progress.Stop()
return fmt.Errorf("写入文件失败: %v", err)
}
progress.Stop()
return nil
}
func downloadConcurrent(url, dest string, size int64, parts int) error {
if parts < 2 {
return downloadRange(url, dest, 0, -1, size)
}
out, err := os.OpenFile(dest, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
if err != nil {
return fmt.Errorf("创建文件失败: %v", err)
}
if err := out.Truncate(size); err != nil {
out.Close()
return fmt.Errorf("预分配文件失败: %v", err)
}
var wg sync.WaitGroup
errCh := make(chan error, parts)
progress := newProgressReporter(size, 0)
progress.Start()
partSize := size / int64(parts)
for i := 0; i < parts; i++ {
start := int64(i) * partSize
end := start + partSize - 1
if i == parts-1 {
end = size - 1
}
wg.Add(1)
go func(s, e int64) {
defer wg.Done()
client := &http.Client{Timeout: 30 * time.Minute}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
errCh <- fmt.Errorf("创建请求失败: %v", err)
return
}
req.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", s, e))
resp, err := client.Do(req)
if err != nil {
errCh <- fmt.Errorf("下载失败: %v", err)
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusPartialContent {
errCh <- fmt.Errorf("分段下载失败,状态码: %d", resp.StatusCode)
return
}
writer := &writeAtWriter{file: out, offset: s, written: progress.written}
if _, err := io.Copy(writer, resp.Body); err != nil {
errCh <- fmt.Errorf("写入文件失败: %v", err)
return
}
}(start, end)
}
wg.Wait()
close(errCh)
out.Close()
progress.Stop()
for err := range errCh {
if err != nil {
return err
}
}
return nil
}
type writeAtWriter struct {
file *os.File
offset int64
written *int64
}
func (w *writeAtWriter) Write(p []byte) (int, error) {
n, err := w.file.WriteAt(p, w.offset)
w.offset += int64(n)
if w.written != nil && n > 0 {
atomic.AddInt64(w.written, int64(n))
}
return n, err
}
type countingReader struct {
r io.Reader
written *int64
}
func (c *countingReader) Read(p []byte) (int, error) {
n, err := c.r.Read(p)
if n > 0 && c.written != nil {
atomic.AddInt64(c.written, int64(n))
}
return n, err
}
type progressReporter struct {
total int64
written *int64
done chan struct{}
once sync.Once
}
func newProgressReporter(total, initial int64) *progressReporter {
current := initial
return &progressReporter{
total: total,
written: &current,
done: make(chan struct{}),
}
}
func (p *progressReporter) Start() {
if p == nil || p.total <= 0 {
return
}
p.print()
go func() {
ticker := time.NewTicker(200 * time.Millisecond)
defer ticker.Stop()
for {
select {
case <-ticker.C:
p.print()
case <-p.done:
p.print()
fmt.Print("\n")
return
}
}
}()
}
func (p *progressReporter) Stop() {
if p == nil || p.total <= 0 {
return
}
p.once.Do(func() {
close(p.done)
})
}
func (p *progressReporter) print() {
current := atomic.LoadInt64(p.written)
if current < 0 {
current = 0
}
if current > p.total {
current = p.total
}
percent := float64(current) * 100 / float64(p.total)
fmt.Printf("\r下载进度: %.2f%%", percent)
}
func probeRemoteFile(url string) (int64, bool, error) {
client := &http.Client{Timeout: 30 * time.Second}
req, err := http.NewRequest("HEAD", url, nil)
if err == nil {
resp, err := client.Do(req)
if err == nil {
resp.Body.Close()
size := resp.ContentLength
acceptRanges := strings.Contains(strings.ToLower(resp.Header.Get("Accept-Ranges")), "bytes")
if size > 0 && acceptRanges {
return size, acceptRanges, nil
}
}
}
req, err = http.NewRequest("GET", url, nil)
if err != nil {
return 0, false, fmt.Errorf("创建请求失败: %v", err)
}
req.Header.Set("Range", "bytes=0-0")
resp, err := client.Do(req)
if err != nil {
return 0, false, fmt.Errorf("探测下载失败: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusPartialContent {
return -1, false, nil
}
total := parseContentRangeTotal(resp.Header.Get("Content-Range"))
return total, true, nil
}
func parseContentRangeTotal(value string) int64 {
parts := strings.Split(value, "/")
if len(parts) != 2 {
return -1
}
totalStr := strings.TrimSpace(parts[1])
if totalStr == "*" {
return -1
}
total, err := strconv.ParseInt(totalStr, 10, 64)
if err != nil {
return -1
}
return total
}
func verifyFileSHA256(path, expected string) (bool, error) {
if expected == "" {
return true, nil
}
info, err := os.Stat(path)
if err != nil || info.Size() == 0 {
return false, err
}
sum, err := fileSHA256(path)
if err != nil {
return false, err
}
return strings.EqualFold(sum, expected), nil
}
func fileSHA256(path string) (string, error) {
f, err := os.Open(path)
if err != nil {
return "", fmt.Errorf("打开文件失败: %v", err)
}
defer f.Close()
hasher := sha256.New()
if _, err := io.Copy(hasher, f); err != nil {
return "", fmt.Errorf("读取文件失败: %v", err)
}
return hex.EncodeToString(hasher.Sum(nil)), 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, nodeMsiSHA256); 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 := fmt.Sprintf("%sgithub.com/git-for-windows/git/releases/download/v2.52.0.windows.1/Git-2.52.0-64-bit.exe", gitProxy())
tempDir := os.TempDir()
exePath := filepath.Join(tempDir, "Git-2.52.0-64-bit.exe")
fmt.Println("正在下载 Git...")
if err := downloadFile(gitUrl, exePath, gitExeSHA256); 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
}