增加git依赖
This commit is contained in:
Binary file not shown.
@@ -6,24 +6,24 @@ import (
|
|||||||
"github.com/charmbracelet/lipgloss"
|
"github.com/charmbracelet/lipgloss"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Colors Palette - Modern & Professional
|
// 配色方案
|
||||||
var (
|
var (
|
||||||
ColorPrimary = lipgloss.Color("#7D56F4") // Purple
|
ColorPrimary = lipgloss.Color("#7D56F4") // 紫色
|
||||||
ColorSecondary = lipgloss.Color("#04B575") // Green
|
ColorSecondary = lipgloss.Color("#04B575") // 绿色
|
||||||
ColorError = lipgloss.Color("#FF3B30") // Red
|
ColorError = lipgloss.Color("#FF3B30") // 红色
|
||||||
ColorWarning = lipgloss.Color("#FFCC00") // Yellow
|
ColorWarning = lipgloss.Color("#FFCC00") // 黄色
|
||||||
ColorSubtle = lipgloss.Color("#666666") // Grey
|
ColorSubtle = lipgloss.Color("#666666") // 灰色
|
||||||
ColorText = lipgloss.Color("#E0E0E0") // White-ish
|
ColorText = lipgloss.Color("#E0E0E0") // 白字
|
||||||
ColorHighlight = lipgloss.Color("#2A2A2A") // Dark Grey
|
ColorHighlight = lipgloss.Color("#2A2A2A") // 深灰
|
||||||
ColorPanel = lipgloss.Color("#1E1E1E") // Panel BG
|
ColorPanel = lipgloss.Color("#1E1E1E") // 面板底色
|
||||||
ColorBorder = lipgloss.Color("#333333") // Border
|
ColorBorder = lipgloss.Color("#333333") // 边框
|
||||||
)
|
)
|
||||||
|
|
||||||
// Base App Style
|
// 基础样式
|
||||||
var AppStyle = lipgloss.NewStyle().
|
var AppStyle = lipgloss.NewStyle().
|
||||||
Padding(1, 2)
|
Padding(1, 2)
|
||||||
|
|
||||||
// Headers
|
// 标题样式
|
||||||
var HeaderStyle = lipgloss.NewStyle().
|
var HeaderStyle = lipgloss.NewStyle().
|
||||||
Foreground(ColorPrimary).
|
Foreground(ColorPrimary).
|
||||||
Bold(true).
|
Bold(true).
|
||||||
@@ -41,13 +41,13 @@ var TitleStyle = lipgloss.NewStyle().
|
|||||||
Border(lipgloss.RoundedBorder()).
|
Border(lipgloss.RoundedBorder()).
|
||||||
BorderForeground(ColorPrimary)
|
BorderForeground(ColorPrimary)
|
||||||
|
|
||||||
// Status Colors
|
// 状态样式
|
||||||
var SuccessStyle = lipgloss.NewStyle().Foreground(ColorSecondary).Bold(true)
|
var SuccessStyle = lipgloss.NewStyle().Foreground(ColorSecondary).Bold(true)
|
||||||
var ErrorStyle = lipgloss.NewStyle().Foreground(ColorError).Bold(true)
|
var ErrorStyle = lipgloss.NewStyle().Foreground(ColorError).Bold(true)
|
||||||
var WarningStyle = lipgloss.NewStyle().Foreground(ColorWarning)
|
var WarningStyle = lipgloss.NewStyle().Foreground(ColorWarning)
|
||||||
var SubtleStyle = lipgloss.NewStyle().Foreground(ColorSubtle)
|
var SubtleStyle = lipgloss.NewStyle().Foreground(ColorSubtle)
|
||||||
|
|
||||||
// Panels
|
// 面板样式
|
||||||
var PanelStyle = lipgloss.NewStyle().
|
var PanelStyle = lipgloss.NewStyle().
|
||||||
Border(lipgloss.RoundedBorder()).
|
Border(lipgloss.RoundedBorder()).
|
||||||
BorderForeground(ColorBorder).
|
BorderForeground(ColorBorder).
|
||||||
@@ -64,7 +64,7 @@ var WizardPanelStyle = lipgloss.NewStyle().
|
|||||||
Padding(1, 4).
|
Padding(1, 4).
|
||||||
Width(80)
|
Width(80)
|
||||||
|
|
||||||
// Menu Styles
|
// 菜单样式
|
||||||
var MenuNormalStyle = lipgloss.NewStyle().
|
var MenuNormalStyle = lipgloss.NewStyle().
|
||||||
Foreground(ColorText).
|
Foreground(ColorText).
|
||||||
PaddingLeft(2)
|
PaddingLeft(2)
|
||||||
@@ -77,7 +77,7 @@ var MenuSelectedStyle = lipgloss.NewStyle().
|
|||||||
Border(lipgloss.NormalBorder(), false, false, false, true).
|
Border(lipgloss.NormalBorder(), false, false, false, true).
|
||||||
BorderForeground(ColorSecondary)
|
BorderForeground(ColorSecondary)
|
||||||
|
|
||||||
// Input & Form Styles
|
// 输入框样式
|
||||||
var KeyStyle = lipgloss.NewStyle().
|
var KeyStyle = lipgloss.NewStyle().
|
||||||
Foreground(ColorPrimary).
|
Foreground(ColorPrimary).
|
||||||
Bold(true)
|
Bold(true)
|
||||||
|
|||||||
@@ -22,12 +22,13 @@ import (
|
|||||||
var (
|
var (
|
||||||
cachedNpmPrefix string
|
cachedNpmPrefix string
|
||||||
cachedNodePath string
|
cachedNodePath string
|
||||||
|
cachedGitPath string
|
||||||
prefixOnce sync.Once
|
prefixOnce sync.Once
|
||||||
nodePathOnce sync.Once
|
nodePathOnce sync.Once
|
||||||
|
gitPathOnce sync.Once
|
||||||
)
|
)
|
||||||
|
|
||||||
// Config Structures
|
// MoltbotConfig 配置结构
|
||||||
|
|
||||||
type MoltbotConfig struct {
|
type MoltbotConfig struct {
|
||||||
Gateway GatewayConfig `json:"gateway"`
|
Gateway GatewayConfig `json:"gateway"`
|
||||||
Env map[string]string `json:"env,omitempty"`
|
Env map[string]string `json:"env,omitempty"`
|
||||||
@@ -108,9 +109,8 @@ type TelegramConfig struct {
|
|||||||
AllowFrom []string `json:"allowFrom"`
|
AllowFrom []string `json:"allowFrom"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetMoltbotPath 尝试解析 moltbot 或 clawdbot 的绝对路径
|
// GetMoltbotPath 获取执行路径
|
||||||
func GetMoltbotPath() (string, error) {
|
func GetMoltbotPath() (string, error) {
|
||||||
// 优先检查 clawdbot
|
|
||||||
if path, err := exec.LookPath("clawdbot"); err == nil {
|
if path, err := exec.LookPath("clawdbot"); err == nil {
|
||||||
return path, nil
|
return path, nil
|
||||||
}
|
}
|
||||||
@@ -123,13 +123,11 @@ func GetMoltbotPath() (string, error) {
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查 clawdbot.cmd
|
|
||||||
possibleClawd := filepath.Join(npmPrefix, "clawdbot.cmd")
|
possibleClawd := filepath.Join(npmPrefix, "clawdbot.cmd")
|
||||||
if _, err := os.Stat(possibleClawd); err == nil {
|
if _, err := os.Stat(possibleClawd); err == nil {
|
||||||
return possibleClawd, nil
|
return possibleClawd, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查 moltbot.cmd
|
|
||||||
possibleMolt := filepath.Join(npmPrefix, "moltbot.cmd")
|
possibleMolt := filepath.Join(npmPrefix, "moltbot.cmd")
|
||||||
if _, err := os.Stat(possibleMolt); err == nil {
|
if _, err := os.Stat(possibleMolt); err == nil {
|
||||||
return possibleMolt, nil
|
return possibleMolt, nil
|
||||||
@@ -138,7 +136,7 @@ func GetMoltbotPath() (string, error) {
|
|||||||
return "", fmt.Errorf("未找到 moltbot 或 clawdbot 可执行文件")
|
return "", fmt.Errorf("未找到 moltbot 或 clawdbot 可执行文件")
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetNodePath 获取 Node.js 可执行文件的绝对路径
|
// GetNodePath 获取 Node 路径
|
||||||
func GetNodePath() (string, error) {
|
func GetNodePath() (string, error) {
|
||||||
var err error
|
var err error
|
||||||
nodePathOnce.Do(func() {
|
nodePathOnce.Do(func() {
|
||||||
@@ -162,8 +160,36 @@ func GetNodePath() (string, error) {
|
|||||||
return "", fmt.Errorf("未找到 Node.js")
|
return "", fmt.Errorf("未找到 Node.js")
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetupNodeEnv 将 Node.js 所在目录添加到当前进程的 PATH 环境变量
|
// GetGitPath 获取 Git 路径
|
||||||
// 这对于刚安装完 Node.js 但未重启终端的情况非常重要
|
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 {
|
func SetupNodeEnv() error {
|
||||||
nodeExe, err := GetNodePath()
|
nodeExe, err := GetNodePath()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -172,14 +198,12 @@ func SetupNodeEnv() error {
|
|||||||
nodeDir := filepath.Dir(nodeExe)
|
nodeDir := filepath.Dir(nodeExe)
|
||||||
|
|
||||||
pathEnv := os.Getenv("PATH")
|
pathEnv := os.Getenv("PATH")
|
||||||
// 简单检查是否已包含 (忽略大小写)
|
|
||||||
if strings.Contains(strings.ToLower(pathEnv), strings.ToLower(nodeDir)) {
|
if strings.Contains(strings.ToLower(pathEnv), strings.ToLower(nodeDir)) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
newPath := nodeDir + string(os.PathListSeparator) + pathEnv
|
newPath := nodeDir + string(os.PathListSeparator) + pathEnv
|
||||||
|
|
||||||
// 同时确保 npm prefix 在 PATH 中
|
|
||||||
if npmPrefix, err := getNpmPrefix(); err == nil {
|
if npmPrefix, err := getNpmPrefix(); err == nil {
|
||||||
if !strings.Contains(strings.ToLower(newPath), strings.ToLower(npmPrefix)) {
|
if !strings.Contains(strings.ToLower(newPath), strings.ToLower(npmPrefix)) {
|
||||||
newPath = npmPrefix + string(os.PathListSeparator) + newPath
|
newPath = npmPrefix + string(os.PathListSeparator) + newPath
|
||||||
@@ -189,10 +213,25 @@ func SetupNodeEnv() error {
|
|||||||
return os.Setenv("PATH", newPath)
|
return os.Setenv("PATH", newPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckMoltbot 检查 Moltbot 是否已安装
|
// 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) {
|
func CheckMoltbot() (string, bool) {
|
||||||
// 确保环境正确
|
|
||||||
SetupNodeEnv()
|
SetupNodeEnv()
|
||||||
|
|
||||||
cmdName, err := GetMoltbotPath()
|
cmdName, err := GetMoltbotPath()
|
||||||
@@ -200,7 +239,6 @@ func CheckMoltbot() (string, bool) {
|
|||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get version
|
|
||||||
cmd := exec.Command("cmd", "/c", cmdName, "--version")
|
cmd := exec.Command("cmd", "/c", cmdName, "--version")
|
||||||
out, err := cmd.Output()
|
out, err := cmd.Output()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -209,9 +247,8 @@ func CheckMoltbot() (string, bool) {
|
|||||||
return strings.TrimSpace(string(out)), true
|
return strings.TrimSpace(string(out)), true
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckNode 检查 Node.js 版本是否 >= 22
|
// CheckNode 检查 Node 版本
|
||||||
func CheckNode() (string, bool) {
|
func CheckNode() (string, bool) {
|
||||||
// Try to find node
|
|
||||||
nodePath, err := GetNodePath()
|
nodePath, err := GetNodePath()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", false
|
return "", false
|
||||||
@@ -223,7 +260,7 @@ func CheckNode() (string, bool) {
|
|||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
|
|
||||||
versionStr := strings.TrimSpace(string(out)) // e.g., "v22.1.0"
|
versionStr := strings.TrimSpace(string(out))
|
||||||
re := regexp.MustCompile(`v(\d+)\.`)
|
re := regexp.MustCompile(`v(\d+)\.`)
|
||||||
matches := re.FindStringSubmatch(versionStr)
|
matches := re.FindStringSubmatch(versionStr)
|
||||||
if len(matches) < 2 {
|
if len(matches) < 2 {
|
||||||
@@ -241,13 +278,28 @@ func CheckNode() (string, bool) {
|
|||||||
return versionStr, false
|
return versionStr, false
|
||||||
}
|
}
|
||||||
|
|
||||||
// getNpmPath 获取 npm 可执行文件路径
|
// 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) {
|
func getNpmPath() (string, error) {
|
||||||
path, err := exec.LookPath("npm")
|
path, err := exec.LookPath("npm")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return path, nil
|
return path, nil
|
||||||
}
|
}
|
||||||
// Try default Windows install path
|
|
||||||
defaultPath := `C:\Program Files\nodejs\npm.cmd`
|
defaultPath := `C:\Program Files\nodejs\npm.cmd`
|
||||||
if _, err := os.Stat(defaultPath); err == nil {
|
if _, err := os.Stat(defaultPath); err == nil {
|
||||||
return defaultPath, nil
|
return defaultPath, nil
|
||||||
@@ -277,13 +329,12 @@ func getNpmPrefix() (string, error) {
|
|||||||
return cachedNpmPrefix, nil
|
return cachedNpmPrefix, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConfigureNpmMirror 设置 npm 淘宝镜像
|
// ConfigureNpmMirror 配置镜像
|
||||||
func ConfigureNpmMirror() error {
|
func ConfigureNpmMirror() error {
|
||||||
npmPath, err := getNpmPath()
|
npmPath, err := getNpmPath()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// 设置 registry 为淘宝镜像
|
|
||||||
cmd := exec.Command(npmPath, "config", "set", "registry", "https://registry.npmmirror.com/")
|
cmd := exec.Command(npmPath, "config", "set", "registry", "https://registry.npmmirror.com/")
|
||||||
if err := cmd.Run(); err != nil {
|
if err := cmd.Run(); err != nil {
|
||||||
return fmt.Errorf("设置 npm 镜像失败: %v", err)
|
return fmt.Errorf("设置 npm 镜像失败: %v", err)
|
||||||
@@ -291,57 +342,52 @@ func ConfigureNpmMirror() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// InstallNode 下载并安装 Node.js MSI
|
// downloadFile 下载文件
|
||||||
func InstallNode() error {
|
func downloadFile(url, dest string) error {
|
||||||
// 0. Check existing installation
|
if info, err := os.Stat(dest); err == nil && info.Size() > 10000000 {
|
||||||
_, ok := CheckNode()
|
return nil
|
||||||
if ok {
|
}
|
||||||
// Version is >= 22, skip install
|
|
||||||
|
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
|
return nil
|
||||||
}
|
}
|
||||||
// If version exists but is old (ok=false, verStr not empty), we update.
|
|
||||||
// If verStr is empty, we install fresh.
|
|
||||||
|
|
||||||
msiUrl := "https://nodejs.org/dist/v24.13.0/node-v24.13.0-x64.msi"
|
msiUrl := "https://nodejs.org/dist/v24.13.0/node-v24.13.0-x64.msi"
|
||||||
tempDir := os.TempDir()
|
tempDir := os.TempDir()
|
||||||
msiPath := filepath.Join(tempDir, "node-v24.13.0-x64.msi")
|
msiPath := filepath.Join(tempDir, "node-v24.13.0-x64.msi")
|
||||||
|
|
||||||
// 1. 下载 MSI
|
if err := downloadFile(msiUrl, msiPath); err != nil {
|
||||||
// Check if file already exists and has size (basic cache check)
|
return err
|
||||||
if info, err := os.Stat(msiPath); err == nil && info.Size() > 10000000 {
|
|
||||||
// Assume valid if > 10MB, skip download
|
|
||||||
} else {
|
|
||||||
fmt.Printf("正在下载 Node.js: %s\n", msiUrl)
|
|
||||||
resp, err := http.Get(msiUrl)
|
|
||||||
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(msiPath)
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 安装 MSI (静默安装)
|
|
||||||
// msiexec /i <file> /qn
|
|
||||||
// Error 1619: This installation package could not be opened.
|
|
||||||
// Error 1603: Fatal error during installation.
|
|
||||||
// Error 1618: Another installation is already in progress.
|
|
||||||
fmt.Println("正在安装 Node.js (可能需要管理员权限)...")
|
fmt.Println("正在安装 Node.js (可能需要管理员权限)...")
|
||||||
|
|
||||||
// Retry loop for 1618 (Another installation in progress)
|
|
||||||
for i := 0; i < 3; i++ {
|
for i := 0; i < 3; i++ {
|
||||||
installCmd := exec.Command("msiexec", "/i", msiPath, "/qn")
|
installCmd := exec.Command("msiexec", "/i", msiPath, "/qn")
|
||||||
output, err := installCmd.CombinedOutput()
|
output, err := installCmd.CombinedOutput()
|
||||||
@@ -355,10 +401,8 @@ func InstallNode() error {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// If error is 1619, maybe file is corrupted, delete and retry download?
|
|
||||||
// For now just return error but with better message
|
|
||||||
if strings.Contains(outStr, "1619") {
|
if strings.Contains(outStr, "1619") {
|
||||||
return fmt.Errorf("安装包损坏或无法打开 (Error 1619). 请尝试手动下载安装: %s", msiUrl)
|
return fmt.Errorf("安装包损坏 (Error 1619). 请尝试手动下载: %s", msiUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
if i == 2 {
|
if i == 2 {
|
||||||
@@ -367,18 +411,48 @@ func InstallNode() error {
|
|||||||
time.Sleep(2 * time.Second)
|
time.Sleep(2 * time.Second)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 刷新环境变量 (当前进程无法立即生效,但后续调用 getNpmPath 会尝试绝对路径)
|
|
||||||
SetupNodeEnv()
|
SetupNodeEnv()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// InstallMoltbotNpm 使用 npm 全局安装
|
// 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 {
|
func InstallMoltbotNpm(tag string) error {
|
||||||
// 确保 Node 环境就绪
|
|
||||||
SetupNodeEnv()
|
SetupNodeEnv()
|
||||||
|
|
||||||
// 强制使用 clawdbot 包,因为用户反馈该包更稳定
|
|
||||||
// 如果之前传入的是 beta,重置为 latest,因为 clawdbot 的版本管理可能不同
|
|
||||||
pkgName := "clawdbot"
|
pkgName := "clawdbot"
|
||||||
if tag == "" || tag == "beta" {
|
if tag == "" || tag == "beta" {
|
||||||
tag = "latest"
|
tag = "latest"
|
||||||
@@ -389,7 +463,6 @@ func InstallMoltbotNpm(tag string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置环境变量以减少 npm 输出
|
|
||||||
os.Setenv("NPM_CONFIG_LOGLEVEL", "error")
|
os.Setenv("NPM_CONFIG_LOGLEVEL", "error")
|
||||||
os.Setenv("NPM_CONFIG_UPDATE_NOTIFIER", "false")
|
os.Setenv("NPM_CONFIG_UPDATE_NOTIFIER", "false")
|
||||||
os.Setenv("NPM_CONFIG_FUND", "false")
|
os.Setenv("NPM_CONFIG_FUND", "false")
|
||||||
@@ -405,14 +478,13 @@ func InstallMoltbotNpm(tag string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// EnsureOnPath 确保 moltbot 或 clawdbot 在 PATH 中
|
// EnsureOnPath 检查并配置 PATH
|
||||||
// 返回值: (需要重启终端, error)
|
|
||||||
func EnsureOnPath() (bool, error) {
|
func EnsureOnPath() (bool, error) {
|
||||||
if _, err := exec.LookPath("clawdbot"); err == nil {
|
if _, err := exec.LookPath("clawdbot"); err == nil {
|
||||||
return false, nil // 已存在
|
return false, nil
|
||||||
}
|
}
|
||||||
if _, err := exec.LookPath("moltbot"); err == nil {
|
if _, err := exec.LookPath("moltbot"); err == nil {
|
||||||
return false, nil // 已存在
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
npmPrefix, err := getNpmPrefix()
|
npmPrefix, err := getNpmPrefix()
|
||||||
@@ -421,19 +493,14 @@ func EnsureOnPath() (bool, error) {
|
|||||||
}
|
}
|
||||||
npmBin := filepath.Join(npmPrefix, "bin")
|
npmBin := filepath.Join(npmPrefix, "bin")
|
||||||
|
|
||||||
// 查找 clawdbot.cmd 或 moltbot.cmd
|
|
||||||
possiblePath := npmPrefix
|
possiblePath := npmPrefix
|
||||||
|
|
||||||
// Check priority: clawdbot -> moltbot
|
|
||||||
if _, err := os.Stat(filepath.Join(npmPrefix, "clawdbot.cmd")); os.IsNotExist(err) {
|
if _, err := os.Stat(filepath.Join(npmPrefix, "clawdbot.cmd")); os.IsNotExist(err) {
|
||||||
if _, err := os.Stat(filepath.Join(npmPrefix, "moltbot.cmd")); os.IsNotExist(err) {
|
if _, err := os.Stat(filepath.Join(npmPrefix, "moltbot.cmd")); os.IsNotExist(err) {
|
||||||
// Check bin subdir
|
|
||||||
possiblePath = npmBin
|
possiblePath = npmBin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加到用户 PATH
|
|
||||||
// 这里我们添加包含 .cmd 文件的目录到 PATH
|
|
||||||
psCmd := fmt.Sprintf(`
|
psCmd := fmt.Sprintf(`
|
||||||
$userPath = [Environment]::GetEnvironmentVariable("Path", "User")
|
$userPath = [Environment]::GetEnvironmentVariable("Path", "User")
|
||||||
if (-not ($userPath -split ";" | Where-Object { $_ -ieq "%s" })) {
|
if (-not ($userPath -split ";" | Where-Object { $_ -ieq "%s" })) {
|
||||||
@@ -443,25 +510,21 @@ func EnsureOnPath() (bool, error) {
|
|||||||
|
|
||||||
exec.Command("powershell", "-Command", psCmd).Run()
|
exec.Command("powershell", "-Command", psCmd).Run()
|
||||||
|
|
||||||
return true, nil // 已添加,需要重启终端
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// RunDoctor 运行迁移
|
// RunDoctor 运行诊断
|
||||||
func RunDoctor() error {
|
func RunDoctor() error {
|
||||||
cmdName, err := GetMoltbotPath()
|
cmdName, err := GetMoltbotPath()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// 尝试直接运行,虽然可能失败
|
|
||||||
cmdName = "moltbot"
|
cmdName = "moltbot"
|
||||||
}
|
}
|
||||||
|
|
||||||
// 在 Windows 上,需要通过 cmd /c 或 powershell 来运行 .cmd 文件
|
|
||||||
// 但 exec.Command 如果指向 .cmd 文件通常可以直接运行
|
|
||||||
// 为了保险,使用 cmd /c
|
|
||||||
cmd := exec.Command("cmd", "/c", cmdName, "doctor", "--non-interactive")
|
cmd := exec.Command("cmd", "/c", cmdName, "doctor", "--non-interactive")
|
||||||
return cmd.Run()
|
return cmd.Run()
|
||||||
}
|
}
|
||||||
|
|
||||||
// RunOnboard 运行引导 (未使用,保留备用)
|
// RunOnboard 运行引导
|
||||||
func RunOnboard() error {
|
func RunOnboard() error {
|
||||||
cmdName, err := GetMoltbotPath()
|
cmdName, err := GetMoltbotPath()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -474,9 +537,9 @@ func RunOnboard() error {
|
|||||||
return cmd.Run()
|
return cmd.Run()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Config Options Struct
|
// ConfigOptions 配置选项
|
||||||
type ConfigOptions struct {
|
type ConfigOptions struct {
|
||||||
ApiType string // "anthropic" or "openai"
|
ApiType string
|
||||||
BotToken string
|
BotToken string
|
||||||
AdminID string
|
AdminID string
|
||||||
AnthropicKey string
|
AnthropicKey string
|
||||||
@@ -485,7 +548,7 @@ type ConfigOptions struct {
|
|||||||
OpenAIModel string
|
OpenAIModel string
|
||||||
}
|
}
|
||||||
|
|
||||||
// GenerateAndWriteConfig 生成并写入配置文件
|
// GenerateAndWriteConfig 生成配置
|
||||||
func GenerateAndWriteConfig(opts ConfigOptions) error {
|
func GenerateAndWriteConfig(opts ConfigOptions) error {
|
||||||
userHome, err := os.UserHomeDir()
|
userHome, err := os.UserHomeDir()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -519,7 +582,6 @@ func GenerateAndWriteConfig(opts ConfigOptions) error {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configure Telegram if token provided
|
|
||||||
if opts.BotToken != "" {
|
if opts.BotToken != "" {
|
||||||
config.Channels.Telegram.Enabled = true
|
config.Channels.Telegram.Enabled = true
|
||||||
config.Channels.Telegram.BotToken = opts.BotToken
|
config.Channels.Telegram.BotToken = opts.BotToken
|
||||||
@@ -541,17 +603,15 @@ func GenerateAndWriteConfig(opts ConfigOptions) error {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
} else if opts.ApiType == "skip" {
|
} else if opts.ApiType == "skip" {
|
||||||
// Minimal config for Web UI setup
|
|
||||||
config.Channels.Telegram.Enabled = false
|
config.Channels.Telegram.Enabled = false
|
||||||
config.Agents = AgentsConfig{
|
config.Agents = AgentsConfig{
|
||||||
Defaults: AgentDefaults{
|
Defaults: AgentDefaults{
|
||||||
Model: ModelRef{
|
Model: ModelRef{
|
||||||
Primary: "anthropic/claude-opus-4-5", // Default placeholder
|
Primary: "anthropic/claude-opus-4-5",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// OpenAI Compatible
|
|
||||||
config.Agents = AgentsConfig{
|
config.Agents = AgentsConfig{
|
||||||
Defaults: AgentDefaults{
|
Defaults: AgentDefaults{
|
||||||
Model: ModelRef{
|
Model: ModelRef{
|
||||||
@@ -577,7 +637,6 @@ func GenerateAndWriteConfig(opts ConfigOptions) error {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
// Add extra exec config for OpenAI mode (as per install.sh)
|
|
||||||
config.Tools.Exec = &ExecConfig{
|
config.Tools.Exec = &ExecConfig{
|
||||||
BackgroundMs: 10000,
|
BackgroundMs: 10000,
|
||||||
TimeoutSec: 1800,
|
TimeoutSec: 1800,
|
||||||
@@ -594,25 +653,23 @@ func GenerateAndWriteConfig(opts ConfigOptions) error {
|
|||||||
return os.WriteFile(configFile, data, 0644)
|
return os.WriteFile(configFile, data, 0644)
|
||||||
}
|
}
|
||||||
|
|
||||||
// StartGateway 在后台启动网关 (隐藏窗口)
|
// StartGateway 启动网关
|
||||||
func StartGateway() error {
|
func StartGateway() error {
|
||||||
cmdName, err := GetMoltbotPath()
|
cmdName, err := GetMoltbotPath()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cmdName = "moltbot"
|
cmdName = "moltbot"
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用 SysProcAttr 隐藏窗口
|
|
||||||
cmd := exec.Command(cmdName, "gateway", "--verbose")
|
cmd := exec.Command(cmdName, "gateway", "--verbose")
|
||||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||||
HideWindow: true,
|
HideWindow: true,
|
||||||
CreationFlags: 0x08000000, // CREATE_NO_WINDOW
|
CreationFlags: 0x08000000,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 不等待,让其在后台运行
|
|
||||||
return cmd.Start()
|
return cmd.Start()
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsGatewayRunning 检查端口 18789 是否被占用
|
// IsGatewayRunning 检查端口
|
||||||
func IsGatewayRunning() bool {
|
func IsGatewayRunning() bool {
|
||||||
conn, err := net.DialTimeout("tcp", "127.0.0.1:18789", 500*time.Millisecond)
|
conn, err := net.DialTimeout("tcp", "127.0.0.1:18789", 500*time.Millisecond)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -622,9 +679,8 @@ func IsGatewayRunning() bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// KillGateway 查找占用 18789 端口的进程并强制结束
|
// KillGateway 停止网关
|
||||||
func KillGateway() error {
|
func KillGateway() error {
|
||||||
// 1. netstat -ano 查找 PID
|
|
||||||
cmd := exec.Command("netstat", "-ano")
|
cmd := exec.Command("netstat", "-ano")
|
||||||
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
|
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
|
||||||
out, err := cmd.Output()
|
out, err := cmd.Output()
|
||||||
@@ -646,38 +702,34 @@ func KillGateway() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if pid == "" {
|
if pid == "" {
|
||||||
return nil // 未找到,可能已停止
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. taskkill /F /PID <pid>
|
|
||||||
killCmd := exec.Command("taskkill", "/F", "/PID", pid)
|
killCmd := exec.Command("taskkill", "/F", "/PID", pid)
|
||||||
killCmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
|
killCmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
|
||||||
return killCmd.Run()
|
return killCmd.Run()
|
||||||
}
|
}
|
||||||
|
|
||||||
// UninstallMoltbot 卸载 Moltbot/Clawdbot 并清理配置
|
// UninstallMoltbot 卸载清理
|
||||||
func UninstallMoltbot() error {
|
func UninstallMoltbot() error {
|
||||||
npmPath, err := getNpmPath()
|
npmPath, err := getNpmPath()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. Uninstall global packages
|
|
||||||
packages := []string{"clawdbot", "moltbot"}
|
packages := []string{"clawdbot", "moltbot"}
|
||||||
for _, pkg := range packages {
|
for _, pkg := range packages {
|
||||||
cmd := exec.Command(npmPath, "uninstall", "-g", pkg)
|
cmd := exec.Command(npmPath, "uninstall", "-g", pkg)
|
||||||
cmd.Stdout = nil
|
cmd.Stdout = nil
|
||||||
cmd.Stderr = nil
|
cmd.Stderr = nil
|
||||||
cmd.Run() // Ignore errors if not installed
|
cmd.Run()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Remove configuration directory
|
|
||||||
userHome, err := os.UserHomeDir()
|
userHome, err := os.UserHomeDir()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
configDir := filepath.Join(userHome, ".clawdbot")
|
configDir := filepath.Join(userHome, ".clawdbot")
|
||||||
os.RemoveAll(configDir)
|
os.RemoveAll(configDir)
|
||||||
|
|
||||||
// Also check for legacy .moltbot if exists
|
|
||||||
legacyDir := filepath.Join(userHome, ".moltbot")
|
legacyDir := filepath.Join(userHome, ".moltbot")
|
||||||
os.RemoveAll(legacyDir)
|
os.RemoveAll(legacyDir)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import (
|
|||||||
"github.com/charmbracelet/lipgloss"
|
"github.com/charmbracelet/lipgloss"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SessionState defines the high-level mode of the application
|
// 会话状态
|
||||||
type SessionState int
|
type SessionState int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -23,7 +23,7 @@ const (
|
|||||||
StateAction
|
StateAction
|
||||||
)
|
)
|
||||||
|
|
||||||
// Sub-states for specific flows
|
// 向导子状态
|
||||||
type WizardStep int
|
type WizardStep int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -42,47 +42,51 @@ const (
|
|||||||
ActionKillGateway
|
ActionKillGateway
|
||||||
)
|
)
|
||||||
|
|
||||||
// Model is the main application state
|
// 主程序模型
|
||||||
type Model struct {
|
type Model struct {
|
||||||
// Global State
|
// 全局状态
|
||||||
state SessionState
|
state SessionState
|
||||||
width int
|
width int
|
||||||
height int
|
height int
|
||||||
|
|
||||||
// Flag to signal main.go (legacy/compatibility)
|
// 启动标志
|
||||||
DidStartGateway bool
|
DidStartGateway bool
|
||||||
|
|
||||||
// Dashboard State
|
// 仪表盘状态
|
||||||
menuIndex int
|
menuIndex int
|
||||||
|
|
||||||
// Wizard State
|
// 向导状态
|
||||||
wizardStep WizardStep
|
wizardStep WizardStep
|
||||||
configOpts sys.ConfigOptions
|
configOpts sys.ConfigOptions
|
||||||
input textinput.Model
|
input textinput.Model
|
||||||
inputStep int // For multi-field steps like API Input
|
inputStep int // 当前输入步骤
|
||||||
|
|
||||||
// Action/Progress State
|
// 动作/进度状态
|
||||||
actionType ActionType
|
actionType ActionType
|
||||||
spinner spinner.Model
|
spinner spinner.Model
|
||||||
progressMsg string
|
progressMsg string
|
||||||
actionErr error
|
actionErr error
|
||||||
actionDone bool
|
actionDone bool
|
||||||
|
|
||||||
// System Status Cache
|
// 系统状态缓存
|
||||||
nodeVer string
|
nodeVer string
|
||||||
nodeOk bool
|
nodeOk bool
|
||||||
moltbotVer string
|
moltbotVer string
|
||||||
moltbotOk bool
|
moltbotOk bool
|
||||||
|
gitVer string
|
||||||
|
gitOk bool
|
||||||
gatewayOk bool
|
gatewayOk bool
|
||||||
checkDone bool
|
checkDone bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Messages
|
// 消息定义
|
||||||
type checkMsg struct {
|
type checkMsg struct {
|
||||||
nodeVer string
|
nodeVer string
|
||||||
nodeOk bool
|
nodeOk bool
|
||||||
moltbotVer string
|
moltbotVer string
|
||||||
moltbotInstalled bool
|
moltbotInstalled bool
|
||||||
|
gitVer string
|
||||||
|
gitOk bool
|
||||||
gatewayRunning bool
|
gatewayRunning bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,15 +133,15 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
if msg.String() == "ctrl+c" {
|
if msg.String() == "ctrl+c" {
|
||||||
return m, tea.Quit
|
return m, tea.Quit
|
||||||
}
|
}
|
||||||
// Global Back handler for Wizard
|
// 向导模式返回
|
||||||
if m.state == StateWizard && msg.String() == "esc" {
|
if m.state == StateWizard && msg.String() == "esc" {
|
||||||
m.state = StateDashboard
|
m.state = StateDashboard
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
// Global Back handler for Action Result
|
// 动作结果确认返回
|
||||||
if m.state == StateAction && m.actionDone && (msg.String() == "enter" || msg.String() == "esc") {
|
if m.state == StateAction && m.actionDone && (msg.String() == "enter" || msg.String() == "esc") {
|
||||||
m.state = StateDashboard
|
m.state = StateDashboard
|
||||||
// Refresh env after action
|
// 刷新环境
|
||||||
return m, checkEnvCmd
|
return m, checkEnvCmd
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,10 +158,12 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
m.nodeOk = msg.nodeOk
|
m.nodeOk = msg.nodeOk
|
||||||
m.moltbotVer = msg.moltbotVer
|
m.moltbotVer = msg.moltbotVer
|
||||||
m.moltbotOk = msg.moltbotInstalled
|
m.moltbotOk = msg.moltbotInstalled
|
||||||
|
m.gitVer = msg.gitVer
|
||||||
|
m.gitOk = msg.gitOk
|
||||||
m.gatewayOk = msg.gatewayRunning
|
m.gatewayOk = msg.gatewayRunning
|
||||||
m.checkDone = true
|
m.checkDone = true
|
||||||
|
|
||||||
// If we are in Action mode checking env, this might be a refresh
|
// 如果在动作模式下检查环境,可能需要切回主菜单
|
||||||
if m.state == StateAction && m.actionType == ActionCheckEnv {
|
if m.state == StateAction && m.actionType == ActionCheckEnv {
|
||||||
m.state = StateDashboard
|
m.state = StateDashboard
|
||||||
}
|
}
|
||||||
@@ -188,14 +194,14 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// State-specific updates
|
// 状态更新
|
||||||
switch m.state {
|
switch m.state {
|
||||||
case StateDashboard:
|
case StateDashboard:
|
||||||
return m.updateDashboard(msg)
|
return m.updateDashboard(msg)
|
||||||
case StateWizard:
|
case StateWizard:
|
||||||
return m.updateWizard(msg)
|
return m.updateWizard(msg)
|
||||||
case StateAction:
|
case StateAction:
|
||||||
// In action state, mostly waiting for msgs, but maybe handle quit
|
// 动作模式下主要等待消息
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,7 +217,7 @@ func (m Model) updateDashboard(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
m.menuIndex--
|
m.menuIndex--
|
||||||
}
|
}
|
||||||
case "down", "j":
|
case "down", "j":
|
||||||
if m.menuIndex < 4 { // 5 items (0-4)
|
if m.menuIndex < 4 { // 5个选项 (0-4)
|
||||||
m.menuIndex++
|
m.menuIndex++
|
||||||
}
|
}
|
||||||
case "enter":
|
case "enter":
|
||||||
@@ -225,7 +231,7 @@ func (m Model) updateDashboard(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
|
|
||||||
func (m Model) handleMenuSelect() (tea.Model, tea.Cmd) {
|
func (m Model) handleMenuSelect() (tea.Model, tea.Cmd) {
|
||||||
switch m.menuIndex {
|
switch m.menuIndex {
|
||||||
case 0: // Start/Restart Gateway
|
case 0: // 启动/重启网关
|
||||||
m.state = StateAction
|
m.state = StateAction
|
||||||
m.actionDone = false
|
m.actionDone = false
|
||||||
m.actionErr = nil
|
m.actionErr = nil
|
||||||
@@ -238,26 +244,26 @@ func (m Model) handleMenuSelect() (tea.Model, tea.Cmd) {
|
|||||||
m.progressMsg = "正在启动网关..."
|
m.progressMsg = "正在启动网关..."
|
||||||
return m, runStartGatewayCmd
|
return m, runStartGatewayCmd
|
||||||
}
|
}
|
||||||
case 1: // Configure
|
case 1: // 配置
|
||||||
m.state = StateWizard
|
m.state = StateWizard
|
||||||
m.wizardStep = StepApiSelect
|
m.wizardStep = StepApiSelect
|
||||||
m.configOpts = sys.ConfigOptions{}
|
m.configOpts = sys.ConfigOptions{}
|
||||||
return m, nil
|
return m, nil
|
||||||
case 2: // Install/Update
|
case 2: // 安装/更新
|
||||||
m.state = StateAction
|
m.state = StateAction
|
||||||
m.actionType = ActionInstall
|
m.actionType = ActionInstall
|
||||||
m.actionDone = false
|
m.actionDone = false
|
||||||
m.actionErr = nil
|
m.actionErr = nil
|
||||||
m.progressMsg = "准备安装..."
|
m.progressMsg = "准备安装..."
|
||||||
return m, runInstallFlowCmd
|
return m, runInstallFlowCmd
|
||||||
case 3: // Uninstall
|
case 3: // 卸载
|
||||||
m.state = StateAction
|
m.state = StateAction
|
||||||
m.actionType = ActionUninstall
|
m.actionType = ActionUninstall
|
||||||
m.actionDone = false
|
m.actionDone = false
|
||||||
m.actionErr = nil
|
m.actionErr = nil
|
||||||
m.progressMsg = "正在卸载..."
|
m.progressMsg = "正在卸载..."
|
||||||
return m, runUninstallCmd
|
return m, runUninstallCmd
|
||||||
case 4: // Exit
|
case 4: // 退出
|
||||||
return m, tea.Quit
|
return m, tea.Quit
|
||||||
}
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
@@ -292,7 +298,7 @@ func (m Model) updateWizard(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
if k.String() == "enter" {
|
if k.String() == "enter" {
|
||||||
val := m.input.Value()
|
val := m.input.Value()
|
||||||
// Logic handles both Anthropic and OpenAI flows
|
// 处理 Anthropic 或 OpenAI 流程
|
||||||
isAnthropic := m.configOpts.ApiType == "anthropic"
|
isAnthropic := m.configOpts.ApiType == "anthropic"
|
||||||
|
|
||||||
if isAnthropic {
|
if isAnthropic {
|
||||||
@@ -350,7 +356,7 @@ func (m Model) updateWizard(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
case StepConfirm:
|
case StepConfirm:
|
||||||
if k, ok := msg.(tea.KeyMsg); ok {
|
if k, ok := msg.(tea.KeyMsg); ok {
|
||||||
if k.String() == "enter" {
|
if k.String() == "enter" {
|
||||||
// Run Save Action
|
// 执行保存
|
||||||
m.state = StateAction
|
m.state = StateAction
|
||||||
m.actionDone = false
|
m.actionDone = false
|
||||||
m.actionErr = nil
|
m.actionErr = nil
|
||||||
@@ -362,11 +368,10 @@ func (m Model) updateWizard(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// VIEW RENDERING
|
// 视图渲染
|
||||||
|
|
||||||
func (m Model) View() string {
|
func (m Model) View() string {
|
||||||
if m.width == 0 {
|
if m.width == 0 {
|
||||||
return "Loading..."
|
return "加载中..."
|
||||||
}
|
}
|
||||||
|
|
||||||
switch m.state {
|
switch m.state {
|
||||||
@@ -381,10 +386,10 @@ func (m Model) View() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) renderDashboard() string {
|
func (m Model) renderDashboard() string {
|
||||||
// 1. Header
|
// 1. 标题
|
||||||
header := style.HeaderStyle.Render("Moltbot Installer")
|
header := style.HeaderStyle.Render("Moltbot Installer")
|
||||||
|
|
||||||
// 2. Status Bar
|
// 2. 状态栏
|
||||||
nodeStatus := style.Badge("检测中...", "info")
|
nodeStatus := style.Badge("检测中...", "info")
|
||||||
if m.checkDone {
|
if m.checkDone {
|
||||||
if m.nodeOk {
|
if m.nodeOk {
|
||||||
@@ -407,6 +412,18 @@ func (m Model) renderDashboard() string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
gitStatus := style.Badge("检测中...", "info")
|
||||||
|
if m.checkDone {
|
||||||
|
if m.gitOk {
|
||||||
|
ver := m.gitVer
|
||||||
|
// 清理版本字符串 "git version 2.x.y..."
|
||||||
|
ver = strings.Replace(ver, "git version ", "", 1)
|
||||||
|
gitStatus = style.Badge(ver, "success")
|
||||||
|
} else {
|
||||||
|
gitStatus = style.Badge("缺失", "warning")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
gwStatus := style.Badge("...", "info")
|
gwStatus := style.Badge("...", "info")
|
||||||
if m.checkDone {
|
if m.checkDone {
|
||||||
if m.gatewayOk {
|
if m.gatewayOk {
|
||||||
@@ -419,11 +436,12 @@ func (m Model) renderDashboard() string {
|
|||||||
statusPanel := style.PanelStyle.Render(lipgloss.JoinVertical(lipgloss.Left,
|
statusPanel := style.PanelStyle.Render(lipgloss.JoinVertical(lipgloss.Left,
|
||||||
style.SubHeaderStyle.Render("系统状态"),
|
style.SubHeaderStyle.Render("系统状态"),
|
||||||
fmt.Sprintf("Node.js 环境: %s", nodeStatus),
|
fmt.Sprintf("Node.js 环境: %s", nodeStatus),
|
||||||
|
fmt.Sprintf("Git 环境: %s", gitStatus),
|
||||||
fmt.Sprintf("Moltbot 核心: %s", moltStatus),
|
fmt.Sprintf("Moltbot 核心: %s", moltStatus),
|
||||||
fmt.Sprintf("网关进程: %s", gwStatus),
|
fmt.Sprintf("网关进程: %s", gwStatus),
|
||||||
))
|
))
|
||||||
|
|
||||||
// 3. Menu
|
// 3. 菜单
|
||||||
menuItems := []struct{ title, desc string }{
|
menuItems := []struct{ title, desc string }{
|
||||||
{"启动/重启服务", "管理后台网关进程"},
|
{"启动/重启服务", "管理后台网关进程"},
|
||||||
{"配置向导", "设置 API 密钥与机器人参数"},
|
{"配置向导", "设置 API 密钥与机器人参数"},
|
||||||
@@ -432,7 +450,7 @@ func (m Model) renderDashboard() string {
|
|||||||
{"退出", "关闭控制台"},
|
{"退出", "关闭控制台"},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dynamic text for toggle
|
// 动态切换文本
|
||||||
if m.gatewayOk {
|
if m.gatewayOk {
|
||||||
menuItems[0].title = "重启服务"
|
menuItems[0].title = "重启服务"
|
||||||
menuItems[0].desc = "停止当前进程并重新启动"
|
menuItems[0].desc = "停止当前进程并重新启动"
|
||||||
@@ -457,9 +475,9 @@ func (m Model) renderDashboard() string {
|
|||||||
style.SubtleStyle.Render("使用 ↑/↓ 选择,Enter 确认"),
|
style.SubtleStyle.Render("使用 ↑/↓ 选择,Enter 确认"),
|
||||||
))
|
))
|
||||||
|
|
||||||
// Layout
|
// 4. 布局
|
||||||
if m.width > 100 {
|
if m.width > 100 {
|
||||||
// Side by Side
|
// 并排
|
||||||
return style.AppStyle.Render(lipgloss.JoinVertical(lipgloss.Left,
|
return style.AppStyle.Render(lipgloss.JoinVertical(lipgloss.Left,
|
||||||
header,
|
header,
|
||||||
"",
|
"",
|
||||||
@@ -467,7 +485,7 @@ func (m Model) renderDashboard() string {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vertical Stack
|
// 垂直堆叠
|
||||||
return style.AppStyle.Render(lipgloss.JoinVertical(lipgloss.Left,
|
return style.AppStyle.Render(lipgloss.JoinVertical(lipgloss.Left,
|
||||||
header,
|
header,
|
||||||
"",
|
"",
|
||||||
@@ -581,24 +599,27 @@ func (m Model) renderAction() string {
|
|||||||
return style.AppStyle.Render(style.PanelStyle.Render(content))
|
return style.AppStyle.Render(style.PanelStyle.Render(content))
|
||||||
}
|
}
|
||||||
|
|
||||||
// COMMANDS
|
// 指令处理
|
||||||
|
|
||||||
func checkEnvCmd() tea.Msg {
|
func checkEnvCmd() tea.Msg {
|
||||||
nodeVer, nodeOk := sys.CheckNode()
|
nodeVer, nodeOk := sys.CheckNode()
|
||||||
moltVer, moltOk := sys.CheckMoltbot()
|
moltVer, moltOk := sys.CheckMoltbot()
|
||||||
|
gitVer, gitOk := sys.CheckGit()
|
||||||
gwRun := sys.IsGatewayRunning()
|
gwRun := sys.IsGatewayRunning()
|
||||||
return checkMsg{
|
return checkMsg{
|
||||||
nodeVer: nodeVer,
|
nodeVer: nodeVer,
|
||||||
nodeOk: nodeOk,
|
nodeOk: nodeOk,
|
||||||
moltbotVer: moltVer,
|
moltbotVer: moltVer,
|
||||||
moltbotInstalled: moltOk,
|
moltbotInstalled: moltOk,
|
||||||
|
gitVer: gitVer,
|
||||||
|
gitOk: gitOk,
|
||||||
gatewayRunning: gwRun,
|
gatewayRunning: gwRun,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func runStartGatewayCmd() tea.Msg {
|
func runStartGatewayCmd() tea.Msg {
|
||||||
sys.StartGateway()
|
sys.StartGateway()
|
||||||
time.Sleep(1 * time.Second) // Wait for startup
|
time.Sleep(1 * time.Second) // 等待启动
|
||||||
return actionResultMsg{err: nil}
|
return actionResultMsg{err: nil}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -609,11 +630,11 @@ func runKillGatewayCmd() tea.Msg {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func runUninstallCmd() tea.Msg {
|
func runUninstallCmd() tea.Msg {
|
||||||
// 1. Try to stop gateway if running
|
// 1. 尝试停止网关
|
||||||
_ = sys.KillGateway()
|
_ = sys.KillGateway()
|
||||||
time.Sleep(1 * time.Second)
|
time.Sleep(1 * time.Second)
|
||||||
|
|
||||||
// 2. Uninstall files
|
// 2. 卸载文件
|
||||||
err := sys.UninstallMoltbot()
|
err := sys.UninstallMoltbot()
|
||||||
return actionResultMsg{err: err}
|
return actionResultMsg{err: err}
|
||||||
}
|
}
|
||||||
@@ -626,16 +647,19 @@ func runSaveConfigCmd(opts sys.ConfigOptions) tea.Cmd {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func runInstallFlowCmd() tea.Msg {
|
func runInstallFlowCmd() tea.Msg {
|
||||||
// Linear flow: Check Node -> Install Node -> Config NPM -> Install Moltbot -> Config System
|
// 线性流程: 检查Node -> 安装Node -> 检查Git -> 安装Git -> 配置NPM -> 安装Moltbot -> 配置系统
|
||||||
// Simplified to a blocking chain for "Action" state simplicity,
|
// 为简化状态,使用阻塞执行
|
||||||
// or we can use tea.Sequence if we want granular updates.
|
|
||||||
// For now, we do a blocking sequence in a goroutine wrapper.
|
|
||||||
|
|
||||||
err := sys.InstallNode()
|
err := sys.InstallNode()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return actionResultMsg{err: fmt.Errorf("node.js 安装失败: %v", err)}
|
return actionResultMsg{err: fmt.Errorf("node.js 安装失败: %v", err)}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = sys.InstallGit()
|
||||||
|
if err != nil {
|
||||||
|
return actionResultMsg{err: fmt.Errorf("git 安装失败: %v", err)}
|
||||||
|
}
|
||||||
|
|
||||||
err = sys.ConfigureNpmMirror()
|
err = sys.ConfigureNpmMirror()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return actionResultMsg{err: fmt.Errorf("npm 配置失败: %v", err)}
|
return actionResultMsg{err: fmt.Errorf("npm 配置失败: %v", err)}
|
||||||
@@ -648,7 +672,7 @@ func runInstallFlowCmd() tea.Msg {
|
|||||||
|
|
||||||
_, err = sys.EnsureOnPath()
|
_, err = sys.EnsureOnPath()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Non-fatal
|
// 非致命错误
|
||||||
}
|
}
|
||||||
|
|
||||||
sys.RunDoctor()
|
sys.RunDoctor()
|
||||||
|
|||||||
@@ -13,13 +13,13 @@ func main() {
|
|||||||
p := tea.NewProgram(ui.InitialModel())
|
p := tea.NewProgram(ui.InitialModel())
|
||||||
m, err := p.Run()
|
m, err := p.Run()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Error starting installer: %v\n", err)
|
fmt.Printf("启动失败: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if model, ok := m.(ui.Model); ok {
|
if model, ok := m.(ui.Model); ok {
|
||||||
if model.DidStartGateway {
|
if model.DidStartGateway {
|
||||||
fmt.Println("Web Console: http://127.0.0.1:18789/")
|
fmt.Println("Web 控制台: http://127.0.0.1:18789/")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user