mirror of
https://github.com/MrFadiAi/openclaw-manager.git
synced 2026-08-14 09:02:25 +00:00
auto check env
This commit is contained in:
@@ -0,0 +1,638 @@
|
||||
use crate::utils::{platform, shell};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::command;
|
||||
|
||||
/// 环境检查结果
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct EnvironmentStatus {
|
||||
/// Node.js 是否安装
|
||||
pub node_installed: bool,
|
||||
/// Node.js 版本
|
||||
pub node_version: Option<String>,
|
||||
/// Node.js 版本是否满足要求 (>=22)
|
||||
pub node_version_ok: bool,
|
||||
/// OpenClaw 是否安装
|
||||
pub openclaw_installed: bool,
|
||||
/// OpenClaw 版本
|
||||
pub openclaw_version: Option<String>,
|
||||
/// 配置目录是否存在
|
||||
pub config_dir_exists: bool,
|
||||
/// 是否全部就绪
|
||||
pub ready: bool,
|
||||
/// 操作系统
|
||||
pub os: String,
|
||||
}
|
||||
|
||||
/// 安装进度
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct InstallProgress {
|
||||
pub step: String,
|
||||
pub progress: u8,
|
||||
pub message: String,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
/// 安装结果
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct InstallResult {
|
||||
pub success: bool,
|
||||
pub message: String,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
/// 检查环境状态
|
||||
#[command]
|
||||
pub async fn check_environment() -> Result<EnvironmentStatus, String> {
|
||||
let os = platform::get_os();
|
||||
|
||||
// 检查 Node.js
|
||||
let node_version = get_node_version();
|
||||
let node_installed = node_version.is_some();
|
||||
let node_version_ok = check_node_version_requirement(&node_version);
|
||||
|
||||
// 检查 OpenClaw
|
||||
let openclaw_version = get_openclaw_version();
|
||||
let openclaw_installed = openclaw_version.is_some();
|
||||
|
||||
// 检查配置目录
|
||||
let config_dir = platform::get_config_dir();
|
||||
let config_dir_exists = std::path::Path::new(&config_dir).exists();
|
||||
|
||||
let ready = node_installed && node_version_ok && openclaw_installed;
|
||||
|
||||
Ok(EnvironmentStatus {
|
||||
node_installed,
|
||||
node_version,
|
||||
node_version_ok,
|
||||
openclaw_installed,
|
||||
openclaw_version,
|
||||
config_dir_exists,
|
||||
ready,
|
||||
os,
|
||||
})
|
||||
}
|
||||
|
||||
/// 获取 Node.js 版本
|
||||
fn get_node_version() -> Option<String> {
|
||||
if platform::is_windows() {
|
||||
shell::run_powershell_output("node --version")
|
||||
.ok()
|
||||
.map(|v| v.trim().to_string())
|
||||
} else {
|
||||
shell::run_command_output("node", &["--version"])
|
||||
.ok()
|
||||
.map(|v| v.trim().to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取 OpenClaw 版本
|
||||
fn get_openclaw_version() -> Option<String> {
|
||||
if platform::is_windows() {
|
||||
shell::run_powershell_output("openclaw --version 2>$null")
|
||||
.ok()
|
||||
.map(|v| v.trim().to_string())
|
||||
} else {
|
||||
shell::run_command_output("openclaw", &["--version"])
|
||||
.ok()
|
||||
.map(|v| v.trim().to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// 检查 Node.js 版本是否 >= 22
|
||||
fn check_node_version_requirement(version: &Option<String>) -> bool {
|
||||
if let Some(v) = version {
|
||||
// 解析版本号 "v22.1.0" -> 22
|
||||
let major = v.trim_start_matches('v')
|
||||
.split('.')
|
||||
.next()
|
||||
.and_then(|s| s.parse::<u32>().ok())
|
||||
.unwrap_or(0);
|
||||
major >= 22
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// 安装 Node.js
|
||||
#[command]
|
||||
pub async fn install_nodejs() -> Result<InstallResult, String> {
|
||||
let os = platform::get_os();
|
||||
|
||||
match os.as_str() {
|
||||
"windows" => install_nodejs_windows().await,
|
||||
"macos" => install_nodejs_macos().await,
|
||||
"linux" => install_nodejs_linux().await,
|
||||
_ => Ok(InstallResult {
|
||||
success: false,
|
||||
message: "不支持的操作系统".to_string(),
|
||||
error: Some(format!("不支持的操作系统: {}", os)),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Windows 安装 Node.js
|
||||
async fn install_nodejs_windows() -> Result<InstallResult, String> {
|
||||
// 使用 winget 安装 Node.js(Windows 10/11 自带)
|
||||
let script = r#"
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
# 检查是否已安装
|
||||
$nodeVersion = node --version 2>$null
|
||||
if ($nodeVersion) {
|
||||
Write-Host "Node.js 已安装: $nodeVersion"
|
||||
exit 0
|
||||
}
|
||||
|
||||
# 优先使用 winget
|
||||
$hasWinget = Get-Command winget -ErrorAction SilentlyContinue
|
||||
if ($hasWinget) {
|
||||
Write-Host "使用 winget 安装 Node.js..."
|
||||
winget install --id OpenJS.NodeJS.LTS --accept-source-agreements --accept-package-agreements
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Host "Node.js 安装成功!"
|
||||
exit 0
|
||||
}
|
||||
}
|
||||
|
||||
# 备用方案:使用 fnm (Fast Node Manager)
|
||||
Write-Host "尝试使用 fnm 安装 Node.js..."
|
||||
$fnmInstallScript = "irm https://fnm.vercel.app/install.ps1 | iex"
|
||||
Invoke-Expression $fnmInstallScript
|
||||
|
||||
# 配置 fnm 环境
|
||||
$env:FNM_DIR = "$env:USERPROFILE\.fnm"
|
||||
$env:Path = "$env:FNM_DIR;$env:Path"
|
||||
|
||||
# 安装 Node.js 22
|
||||
fnm install 22
|
||||
fnm default 22
|
||||
fnm use 22
|
||||
|
||||
# 验证安装
|
||||
$nodeVersion = node --version 2>$null
|
||||
if ($nodeVersion) {
|
||||
Write-Host "Node.js 安装成功: $nodeVersion"
|
||||
exit 0
|
||||
} else {
|
||||
Write-Host "Node.js 安装失败"
|
||||
exit 1
|
||||
}
|
||||
"#;
|
||||
|
||||
match shell::run_powershell_output(script) {
|
||||
Ok(output) => {
|
||||
// 验证安装
|
||||
if get_node_version().is_some() {
|
||||
Ok(InstallResult {
|
||||
success: true,
|
||||
message: "Node.js 安装成功!请重启应用以使环境变量生效。".to_string(),
|
||||
error: None,
|
||||
})
|
||||
} else {
|
||||
Ok(InstallResult {
|
||||
success: false,
|
||||
message: "安装后需要重启应用".to_string(),
|
||||
error: Some(output),
|
||||
})
|
||||
}
|
||||
}
|
||||
Err(e) => Ok(InstallResult {
|
||||
success: false,
|
||||
message: "Node.js 安装失败".to_string(),
|
||||
error: Some(e),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// macOS 安装 Node.js
|
||||
async fn install_nodejs_macos() -> Result<InstallResult, String> {
|
||||
// 使用 Homebrew 安装
|
||||
let script = r#"
|
||||
# 检查 Homebrew
|
||||
if ! command -v brew &> /dev/null; then
|
||||
echo "安装 Homebrew..."
|
||||
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
|
||||
|
||||
# 配置 PATH
|
||||
if [[ -f /opt/homebrew/bin/brew ]]; then
|
||||
eval "$(/opt/homebrew/bin/brew shellenv)"
|
||||
elif [[ -f /usr/local/bin/brew ]]; then
|
||||
eval "$(/usr/local/bin/brew shellenv)"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "安装 Node.js 22..."
|
||||
brew install node@22
|
||||
brew link --overwrite node@22
|
||||
|
||||
# 验证安装
|
||||
node --version
|
||||
"#;
|
||||
|
||||
match shell::run_bash_output(script) {
|
||||
Ok(output) => Ok(InstallResult {
|
||||
success: true,
|
||||
message: format!("Node.js 安装成功!{}", output),
|
||||
error: None,
|
||||
}),
|
||||
Err(e) => Ok(InstallResult {
|
||||
success: false,
|
||||
message: "Node.js 安装失败".to_string(),
|
||||
error: Some(e),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Linux 安装 Node.js
|
||||
async fn install_nodejs_linux() -> Result<InstallResult, String> {
|
||||
// 使用 NodeSource 仓库安装
|
||||
let script = r#"
|
||||
# 检测包管理器
|
||||
if command -v apt-get &> /dev/null; then
|
||||
echo "检测到 apt,使用 NodeSource 仓库..."
|
||||
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
|
||||
sudo apt-get install -y nodejs
|
||||
elif command -v dnf &> /dev/null; then
|
||||
echo "检测到 dnf,使用 NodeSource 仓库..."
|
||||
curl -fsSL https://rpm.nodesource.com/setup_22.x | sudo bash -
|
||||
sudo dnf install -y nodejs
|
||||
elif command -v yum &> /dev/null; then
|
||||
echo "检测到 yum,使用 NodeSource 仓库..."
|
||||
curl -fsSL https://rpm.nodesource.com/setup_22.x | sudo bash -
|
||||
sudo yum install -y nodejs
|
||||
elif command -v pacman &> /dev/null; then
|
||||
echo "检测到 pacman..."
|
||||
sudo pacman -S nodejs npm --noconfirm
|
||||
else
|
||||
echo "无法检测到支持的包管理器"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 验证安装
|
||||
node --version
|
||||
"#;
|
||||
|
||||
match shell::run_bash_output(script) {
|
||||
Ok(output) => Ok(InstallResult {
|
||||
success: true,
|
||||
message: format!("Node.js 安装成功!{}", output),
|
||||
error: None,
|
||||
}),
|
||||
Err(e) => Ok(InstallResult {
|
||||
success: false,
|
||||
message: "Node.js 安装失败".to_string(),
|
||||
error: Some(e),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// 安装 OpenClaw
|
||||
#[command]
|
||||
pub async fn install_openclaw() -> Result<InstallResult, String> {
|
||||
let os = platform::get_os();
|
||||
|
||||
match os.as_str() {
|
||||
"windows" => install_openclaw_windows().await,
|
||||
_ => install_openclaw_unix().await,
|
||||
}
|
||||
}
|
||||
|
||||
/// Windows 安装 OpenClaw
|
||||
async fn install_openclaw_windows() -> Result<InstallResult, String> {
|
||||
let script = r#"
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
# 检查 Node.js
|
||||
$nodeVersion = node --version 2>$null
|
||||
if (-not $nodeVersion) {
|
||||
Write-Host "错误:请先安装 Node.js"
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "使用 npm 安装 OpenClaw..."
|
||||
npm install -g openclaw@latest
|
||||
|
||||
# 验证安装
|
||||
$openclawVersion = openclaw --version 2>$null
|
||||
if ($openclawVersion) {
|
||||
Write-Host "OpenClaw 安装成功: $openclawVersion"
|
||||
exit 0
|
||||
} else {
|
||||
Write-Host "OpenClaw 安装失败"
|
||||
exit 1
|
||||
}
|
||||
"#;
|
||||
|
||||
match shell::run_powershell_output(script) {
|
||||
Ok(output) => {
|
||||
if get_openclaw_version().is_some() {
|
||||
Ok(InstallResult {
|
||||
success: true,
|
||||
message: "OpenClaw 安装成功!".to_string(),
|
||||
error: None,
|
||||
})
|
||||
} else {
|
||||
Ok(InstallResult {
|
||||
success: false,
|
||||
message: "安装后需要重启应用".to_string(),
|
||||
error: Some(output),
|
||||
})
|
||||
}
|
||||
}
|
||||
Err(e) => Ok(InstallResult {
|
||||
success: false,
|
||||
message: "OpenClaw 安装失败".to_string(),
|
||||
error: Some(e),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Unix 系统安装 OpenClaw
|
||||
async fn install_openclaw_unix() -> Result<InstallResult, String> {
|
||||
let script = r#"
|
||||
# 检查 Node.js
|
||||
if ! command -v node &> /dev/null; then
|
||||
echo "错误:请先安装 Node.js"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "使用 npm 安装 OpenClaw..."
|
||||
npm install -g openclaw@latest
|
||||
|
||||
# 验证安装
|
||||
openclaw --version
|
||||
"#;
|
||||
|
||||
match shell::run_bash_output(script) {
|
||||
Ok(output) => Ok(InstallResult {
|
||||
success: true,
|
||||
message: format!("OpenClaw 安装成功!{}", output),
|
||||
error: None,
|
||||
}),
|
||||
Err(e) => Ok(InstallResult {
|
||||
success: false,
|
||||
message: "OpenClaw 安装失败".to_string(),
|
||||
error: Some(e),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// 初始化 OpenClaw 配置
|
||||
#[command]
|
||||
pub async fn init_openclaw_config() -> Result<InstallResult, String> {
|
||||
let config_dir = platform::get_config_dir();
|
||||
|
||||
// 创建配置目录
|
||||
if let Err(e) = std::fs::create_dir_all(&config_dir) {
|
||||
return Ok(InstallResult {
|
||||
success: false,
|
||||
message: "创建配置目录失败".to_string(),
|
||||
error: Some(e.to_string()),
|
||||
});
|
||||
}
|
||||
|
||||
// 创建子目录
|
||||
let subdirs = ["agents/main/sessions", "agents/main/agent", "credentials"];
|
||||
for subdir in subdirs {
|
||||
let path = format!("{}/{}", config_dir, subdir);
|
||||
if let Err(e) = std::fs::create_dir_all(&path) {
|
||||
return Ok(InstallResult {
|
||||
success: false,
|
||||
message: format!("创建目录失败: {}", subdir),
|
||||
error: Some(e.to_string()),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 设置 gateway mode 为 local
|
||||
let result = if platform::is_windows() {
|
||||
shell::run_powershell_output("openclaw config set gateway.mode local 2>$null")
|
||||
} else {
|
||||
shell::run_bash_output("openclaw config set gateway.mode local 2>/dev/null")
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok(_) => Ok(InstallResult {
|
||||
success: true,
|
||||
message: "配置初始化成功!".to_string(),
|
||||
error: None,
|
||||
}),
|
||||
Err(e) => Ok(InstallResult {
|
||||
success: false,
|
||||
message: "配置初始化失败".to_string(),
|
||||
error: Some(e),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// 打开终端执行安装脚本(用于需要管理员权限的场景)
|
||||
#[command]
|
||||
pub async fn open_install_terminal(install_type: String) -> Result<String, String> {
|
||||
match install_type.as_str() {
|
||||
"nodejs" => open_nodejs_install_terminal().await,
|
||||
"openclaw" => open_openclaw_install_terminal().await,
|
||||
_ => Err(format!("未知的安装类型: {}", install_type)),
|
||||
}
|
||||
}
|
||||
|
||||
/// 打开终端安装 Node.js
|
||||
async fn open_nodejs_install_terminal() -> Result<String, String> {
|
||||
if platform::is_windows() {
|
||||
// Windows: 打开 PowerShell 执行安装
|
||||
let script = r#"
|
||||
Start-Process powershell -ArgumentList '-NoExit', '-Command', '
|
||||
Write-Host "========================================" -ForegroundColor Cyan
|
||||
Write-Host " Node.js 安装向导" -ForegroundColor White
|
||||
Write-Host "========================================" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
|
||||
# 检查 winget
|
||||
$hasWinget = Get-Command winget -ErrorAction SilentlyContinue
|
||||
if ($hasWinget) {
|
||||
Write-Host "正在使用 winget 安装 Node.js 22..." -ForegroundColor Yellow
|
||||
winget install --id OpenJS.NodeJS.LTS --accept-source-agreements --accept-package-agreements
|
||||
} else {
|
||||
Write-Host "请从以下地址下载安装 Node.js:" -ForegroundColor Yellow
|
||||
Write-Host "https://nodejs.org/en/download" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
Start-Process "https://nodejs.org/en/download"
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "安装完成后请重启 OpenClaw Manager" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
Read-Host "按回车键关闭此窗口"
|
||||
' -Verb RunAs
|
||||
"#;
|
||||
shell::run_powershell_output(script)?;
|
||||
Ok("已打开安装终端".to_string())
|
||||
} else if platform::is_macos() {
|
||||
// macOS: 打开 Terminal.app
|
||||
let script_content = r#"#!/bin/bash
|
||||
clear
|
||||
echo "========================================"
|
||||
echo " Node.js 安装向导"
|
||||
echo "========================================"
|
||||
echo ""
|
||||
|
||||
# 检查 Homebrew
|
||||
if ! command -v brew &> /dev/null; then
|
||||
echo "正在安装 Homebrew..."
|
||||
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
|
||||
|
||||
if [[ -f /opt/homebrew/bin/brew ]]; then
|
||||
eval "$(/opt/homebrew/bin/brew shellenv)"
|
||||
elif [[ -f /usr/local/bin/brew ]]; then
|
||||
eval "$(/usr/local/bin/brew shellenv)"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "正在安装 Node.js 22..."
|
||||
brew install node@22
|
||||
brew link --overwrite node@22
|
||||
|
||||
echo ""
|
||||
echo "安装完成!"
|
||||
node --version
|
||||
echo ""
|
||||
read -p "按回车键关闭此窗口..."
|
||||
"#;
|
||||
|
||||
let script_path = "/tmp/openclaw_install_nodejs.command";
|
||||
std::fs::write(script_path, script_content)
|
||||
.map_err(|e| format!("创建脚本失败: {}", e))?;
|
||||
|
||||
std::process::Command::new("chmod")
|
||||
.args(["+x", script_path])
|
||||
.output()
|
||||
.map_err(|e| format!("设置权限失败: {}", e))?;
|
||||
|
||||
std::process::Command::new("open")
|
||||
.arg(script_path)
|
||||
.spawn()
|
||||
.map_err(|e| format!("启动终端失败: {}", e))?;
|
||||
|
||||
Ok("已打开安装终端".to_string())
|
||||
} else {
|
||||
Err("请手动安装 Node.js: https://nodejs.org/".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// 打开终端安装 OpenClaw
|
||||
async fn open_openclaw_install_terminal() -> Result<String, String> {
|
||||
if platform::is_windows() {
|
||||
let script = r#"
|
||||
Start-Process powershell -ArgumentList '-NoExit', '-Command', '
|
||||
Write-Host "========================================" -ForegroundColor Cyan
|
||||
Write-Host " OpenClaw 安装向导" -ForegroundColor White
|
||||
Write-Host "========================================" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
|
||||
Write-Host "正在安装 OpenClaw..." -ForegroundColor Yellow
|
||||
npm install -g openclaw@latest
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "初始化配置..."
|
||||
openclaw config set gateway.mode local
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "安装完成!" -ForegroundColor Green
|
||||
openclaw --version
|
||||
Write-Host ""
|
||||
Read-Host "按回车键关闭此窗口"
|
||||
'
|
||||
"#;
|
||||
shell::run_powershell_output(script)?;
|
||||
Ok("已打开安装终端".to_string())
|
||||
} else if platform::is_macos() {
|
||||
let script_content = r#"#!/bin/bash
|
||||
clear
|
||||
echo "========================================"
|
||||
echo " OpenClaw 安装向导"
|
||||
echo "========================================"
|
||||
echo ""
|
||||
|
||||
echo "正在安装 OpenClaw..."
|
||||
npm install -g openclaw@latest
|
||||
|
||||
echo ""
|
||||
echo "初始化配置..."
|
||||
openclaw config set gateway.mode local 2>/dev/null || true
|
||||
|
||||
mkdir -p ~/.openclaw/agents/main/sessions
|
||||
mkdir -p ~/.openclaw/agents/main/agent
|
||||
mkdir -p ~/.openclaw/credentials
|
||||
|
||||
echo ""
|
||||
echo "安装完成!"
|
||||
openclaw --version
|
||||
echo ""
|
||||
read -p "按回车键关闭此窗口..."
|
||||
"#;
|
||||
|
||||
let script_path = "/tmp/openclaw_install_openclaw.command";
|
||||
std::fs::write(script_path, script_content)
|
||||
.map_err(|e| format!("创建脚本失败: {}", e))?;
|
||||
|
||||
std::process::Command::new("chmod")
|
||||
.args(["+x", script_path])
|
||||
.output()
|
||||
.map_err(|e| format!("设置权限失败: {}", e))?;
|
||||
|
||||
std::process::Command::new("open")
|
||||
.arg(script_path)
|
||||
.spawn()
|
||||
.map_err(|e| format!("启动终端失败: {}", e))?;
|
||||
|
||||
Ok("已打开安装终端".to_string())
|
||||
} else {
|
||||
// Linux
|
||||
let script_content = r#"#!/bin/bash
|
||||
clear
|
||||
echo "========================================"
|
||||
echo " OpenClaw 安装向导"
|
||||
echo "========================================"
|
||||
echo ""
|
||||
|
||||
echo "正在安装 OpenClaw..."
|
||||
npm install -g openclaw@latest
|
||||
|
||||
echo ""
|
||||
echo "初始化配置..."
|
||||
openclaw config set gateway.mode local 2>/dev/null || true
|
||||
|
||||
mkdir -p ~/.openclaw/agents/main/sessions
|
||||
mkdir -p ~/.openclaw/agents/main/agent
|
||||
mkdir -p ~/.openclaw/credentials
|
||||
|
||||
echo ""
|
||||
echo "安装完成!"
|
||||
openclaw --version
|
||||
echo ""
|
||||
read -p "按回车键关闭..."
|
||||
"#;
|
||||
|
||||
let script_path = "/tmp/openclaw_install_openclaw.sh";
|
||||
std::fs::write(script_path, script_content)
|
||||
.map_err(|e| format!("创建脚本失败: {}", e))?;
|
||||
|
||||
std::process::Command::new("chmod")
|
||||
.args(["+x", script_path])
|
||||
.output()
|
||||
.map_err(|e| format!("设置权限失败: {}", e))?;
|
||||
|
||||
// 尝试不同的终端
|
||||
let terminals = ["gnome-terminal", "xfce4-terminal", "konsole", "xterm"];
|
||||
for term in terminals {
|
||||
if std::process::Command::new(term)
|
||||
.args(["--", script_path])
|
||||
.spawn()
|
||||
.is_ok()
|
||||
{
|
||||
return Ok("已打开安装终端".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
Err("无法启动终端,请手动运行: npm install -g openclaw".to_string())
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod config;
|
||||
pub mod diagnostics;
|
||||
pub mod installer;
|
||||
pub mod process;
|
||||
pub mod service;
|
||||
|
||||
@@ -2,7 +2,7 @@ mod commands;
|
||||
mod models;
|
||||
mod utils;
|
||||
|
||||
use commands::{config, diagnostics, process, service};
|
||||
use commands::{config, diagnostics, installer, process, service};
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
@@ -35,6 +35,13 @@ pub fn run() {
|
||||
diagnostics::test_ai_connection,
|
||||
diagnostics::test_channel,
|
||||
diagnostics::get_system_info,
|
||||
diagnostics::start_channel_login,
|
||||
// 安装器
|
||||
installer::check_environment,
|
||||
installer::install_nodejs,
|
||||
installer::install_openclaw,
|
||||
installer::init_openclaw_config,
|
||||
installer::open_install_terminal,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("运行 Tauri 应用时发生错误");
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use std::process::{Command, Output};
|
||||
use std::io;
|
||||
use crate::utils::platform;
|
||||
|
||||
/// 执行 Shell 命令
|
||||
pub fn run_command(cmd: &str, args: &[&str]) -> io::Result<Output> {
|
||||
@@ -49,20 +50,76 @@ pub fn run_bash_output(script: &str) -> Result<String, String> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 执行 PowerShell 命令(Windows)
|
||||
pub fn run_powershell(script: &str) -> io::Result<Output> {
|
||||
Command::new("powershell")
|
||||
.args(["-NoProfile", "-NonInteractive", "-Command", script])
|
||||
.output()
|
||||
}
|
||||
|
||||
/// 执行 PowerShell 命令并获取输出(Windows)
|
||||
pub fn run_powershell_output(script: &str) -> Result<String, String> {
|
||||
match run_powershell(script) {
|
||||
Ok(output) => {
|
||||
if output.status.success() {
|
||||
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
|
||||
} else {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
|
||||
if stderr.is_empty() {
|
||||
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||
if stdout.is_empty() {
|
||||
Err(format!("Command failed with exit code: {:?}", output.status.code()))
|
||||
} else {
|
||||
Err(stdout)
|
||||
}
|
||||
} else {
|
||||
Err(stderr)
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// 跨平台执行脚本命令
|
||||
pub fn run_script_output(script: &str) -> Result<String, String> {
|
||||
if platform::is_windows() {
|
||||
run_powershell_output(script)
|
||||
} else {
|
||||
run_bash_output(script)
|
||||
}
|
||||
}
|
||||
|
||||
/// 后台执行命令(不等待结果)
|
||||
pub fn spawn_background(script: &str) -> io::Result<()> {
|
||||
Command::new("bash")
|
||||
.arg("-c")
|
||||
.arg(script)
|
||||
.spawn()?;
|
||||
if platform::is_windows() {
|
||||
Command::new("powershell")
|
||||
.args(["-NoProfile", "-Command", script])
|
||||
.spawn()?;
|
||||
} else {
|
||||
Command::new("bash")
|
||||
.arg("-c")
|
||||
.arg(script)
|
||||
.spawn()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 检查命令是否存在
|
||||
pub fn command_exists(cmd: &str) -> bool {
|
||||
Command::new("which")
|
||||
.arg(cmd)
|
||||
.output()
|
||||
.map(|o| o.status.success())
|
||||
.unwrap_or(false)
|
||||
if platform::is_windows() {
|
||||
// Windows: 使用 where 命令
|
||||
Command::new("where")
|
||||
.arg(cmd)
|
||||
.output()
|
||||
.map(|o| o.status.success())
|
||||
.unwrap_or(false)
|
||||
} else {
|
||||
// Unix: 使用 which 命令
|
||||
Command::new("which")
|
||||
.arg(cmd)
|
||||
.output()
|
||||
.map(|o| o.status.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
}
|
||||
|
||||
+59
-1
@@ -1,5 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { Sidebar } from './components/Layout/Sidebar';
|
||||
import { Header } from './components/Layout/Header';
|
||||
import { Dashboard } from './components/Dashboard';
|
||||
@@ -8,11 +9,47 @@ import { Channels } from './components/Channels';
|
||||
import { ServiceManager } from './components/Service';
|
||||
import { Settings } from './components/Settings';
|
||||
import { Testing } from './components/Testing';
|
||||
import { Setup } from './components/Setup';
|
||||
|
||||
export type PageType = 'dashboard' | 'ai' | 'channels' | 'service' | 'testing' | 'settings';
|
||||
|
||||
interface EnvironmentStatus {
|
||||
node_installed: boolean;
|
||||
node_version: string | null;
|
||||
node_version_ok: boolean;
|
||||
openclaw_installed: boolean;
|
||||
openclaw_version: string | null;
|
||||
config_dir_exists: boolean;
|
||||
ready: boolean;
|
||||
os: string;
|
||||
}
|
||||
|
||||
function App() {
|
||||
const [currentPage, setCurrentPage] = useState<PageType>('dashboard');
|
||||
const [isReady, setIsReady] = useState<boolean | null>(null);
|
||||
const [showSetup, setShowSetup] = useState(false);
|
||||
|
||||
// 检查环境
|
||||
useEffect(() => {
|
||||
const checkEnv = async () => {
|
||||
try {
|
||||
const status = await invoke<EnvironmentStatus>('check_environment');
|
||||
setIsReady(status.ready);
|
||||
setShowSetup(!status.ready);
|
||||
} catch (e) {
|
||||
console.error('环境检查失败:', e);
|
||||
// 如果检查失败,尝试继续运行(可能是旧版本没有这个命令)
|
||||
setIsReady(true);
|
||||
setShowSetup(false);
|
||||
}
|
||||
};
|
||||
checkEnv();
|
||||
}, []);
|
||||
|
||||
const handleSetupComplete = () => {
|
||||
setIsReady(true);
|
||||
setShowSetup(false);
|
||||
};
|
||||
|
||||
const renderPage = () => {
|
||||
const pageVariants = {
|
||||
@@ -47,6 +84,27 @@ function App() {
|
||||
);
|
||||
};
|
||||
|
||||
// 正在检查环境
|
||||
if (isReady === null) {
|
||||
return (
|
||||
<div className="flex h-screen bg-dark-900 items-center justify-center">
|
||||
<div className="fixed inset-0 bg-gradient-radial pointer-events-none" />
|
||||
<div className="relative z-10 text-center">
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 rounded-xl bg-gradient-to-br from-brand-500 to-purple-600 mb-4 animate-pulse">
|
||||
<span className="text-3xl">🦞</span>
|
||||
</div>
|
||||
<p className="text-dark-400">正在启动...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 显示安装向导
|
||||
if (showSetup) {
|
||||
return <Setup onComplete={handleSetupComplete} />;
|
||||
}
|
||||
|
||||
// 正常界面
|
||||
return (
|
||||
<div className="flex h-screen bg-dark-900 overflow-hidden">
|
||||
{/* 背景装饰 */}
|
||||
|
||||
@@ -0,0 +1,381 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import {
|
||||
CheckCircle2,
|
||||
XCircle,
|
||||
Loader2,
|
||||
Download,
|
||||
Terminal,
|
||||
ArrowRight,
|
||||
RefreshCw,
|
||||
ExternalLink,
|
||||
Cpu,
|
||||
Package
|
||||
} from 'lucide-react';
|
||||
|
||||
interface EnvironmentStatus {
|
||||
node_installed: boolean;
|
||||
node_version: string | null;
|
||||
node_version_ok: boolean;
|
||||
openclaw_installed: boolean;
|
||||
openclaw_version: string | null;
|
||||
config_dir_exists: boolean;
|
||||
ready: boolean;
|
||||
os: string;
|
||||
}
|
||||
|
||||
interface InstallResult {
|
||||
success: boolean;
|
||||
message: string;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
interface SetupProps {
|
||||
onComplete: () => void;
|
||||
}
|
||||
|
||||
export function Setup({ onComplete }: SetupProps) {
|
||||
const [envStatus, setEnvStatus] = useState<EnvironmentStatus | null>(null);
|
||||
const [checking, setChecking] = useState(true);
|
||||
const [installing, setInstalling] = useState<'nodejs' | 'openclaw' | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [step, setStep] = useState<'check' | 'install' | 'complete'>('check');
|
||||
|
||||
const checkEnvironment = async () => {
|
||||
setChecking(true);
|
||||
setError(null);
|
||||
try {
|
||||
const status = await invoke<EnvironmentStatus>('check_environment');
|
||||
setEnvStatus(status);
|
||||
|
||||
if (status.ready) {
|
||||
setStep('complete');
|
||||
// 延迟一下再跳转,让用户看到成功状态
|
||||
setTimeout(() => onComplete(), 1500);
|
||||
} else {
|
||||
setStep('install');
|
||||
}
|
||||
} catch (e) {
|
||||
setError(`检查环境失败: ${e}`);
|
||||
} finally {
|
||||
setChecking(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
checkEnvironment();
|
||||
}, []);
|
||||
|
||||
const handleInstallNodejs = async () => {
|
||||
setInstalling('nodejs');
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
// 先尝试直接安装
|
||||
const result = await invoke<InstallResult>('install_nodejs');
|
||||
|
||||
if (result.success) {
|
||||
// 重新检查环境
|
||||
await checkEnvironment();
|
||||
} else if (result.message.includes('重启')) {
|
||||
// 需要重启应用
|
||||
setError('Node.js 安装完成,请重启应用以使环境变量生效');
|
||||
} else {
|
||||
// 打开终端手动安装
|
||||
await invoke<string>('open_install_terminal', { installType: 'nodejs' });
|
||||
setError('已打开安装终端,请在终端中完成安装后点击"重新检查"');
|
||||
}
|
||||
} catch (e) {
|
||||
// 如果自动安装失败,打开终端
|
||||
try {
|
||||
await invoke<string>('open_install_terminal', { installType: 'nodejs' });
|
||||
setError('已打开安装终端,请在终端中完成安装后点击"重新检查"');
|
||||
} catch (termErr) {
|
||||
setError(`安装失败: ${e}。${termErr}`);
|
||||
}
|
||||
} finally {
|
||||
setInstalling(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleInstallOpenclaw = async () => {
|
||||
setInstalling('openclaw');
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const result = await invoke<InstallResult>('install_openclaw');
|
||||
|
||||
if (result.success) {
|
||||
// 初始化配置
|
||||
await invoke<InstallResult>('init_openclaw_config');
|
||||
// 重新检查环境
|
||||
await checkEnvironment();
|
||||
} else {
|
||||
// 打开终端手动安装
|
||||
await invoke<string>('open_install_terminal', { installType: 'openclaw' });
|
||||
setError('已打开安装终端,请在终端中完成安装后点击"重新检查"');
|
||||
}
|
||||
} catch (e) {
|
||||
try {
|
||||
await invoke<string>('open_install_terminal', { installType: 'openclaw' });
|
||||
setError('已打开安装终端,请在终端中完成安装后点击"重新检查"');
|
||||
} catch (termErr) {
|
||||
setError(`安装失败: ${e}。${termErr}`);
|
||||
}
|
||||
} finally {
|
||||
setInstalling(null);
|
||||
}
|
||||
};
|
||||
|
||||
const getOsName = (os: string) => {
|
||||
switch (os) {
|
||||
case 'windows': return 'Windows';
|
||||
case 'macos': return 'macOS';
|
||||
case 'linux': return 'Linux';
|
||||
default: return os;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-dark-900 flex items-center justify-center p-8">
|
||||
{/* 背景装饰 */}
|
||||
<div className="fixed inset-0 bg-gradient-radial pointer-events-none" />
|
||||
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
||||
<div className="absolute -top-40 -right-40 w-80 h-80 bg-brand-500/10 rounded-full blur-3xl" />
|
||||
<div className="absolute -bottom-40 -left-40 w-80 h-80 bg-purple-500/10 rounded-full blur-3xl" />
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="relative z-10 w-full max-w-lg"
|
||||
>
|
||||
{/* Logo 和标题 */}
|
||||
<div className="text-center mb-8">
|
||||
<motion.div
|
||||
initial={{ scale: 0.8 }}
|
||||
animate={{ scale: 1 }}
|
||||
transition={{ type: 'spring', damping: 15 }}
|
||||
className="inline-flex items-center justify-center w-20 h-20 rounded-2xl bg-gradient-to-br from-brand-500 to-purple-600 mb-4 shadow-lg shadow-brand-500/25"
|
||||
>
|
||||
<span className="text-4xl">🦞</span>
|
||||
</motion.div>
|
||||
<h1 className="text-2xl font-bold text-white mb-2">OpenClaw Manager</h1>
|
||||
<p className="text-dark-400">环境检测与安装向导</p>
|
||||
</div>
|
||||
|
||||
{/* 主卡片 */}
|
||||
<motion.div
|
||||
layout
|
||||
className="glass-card rounded-2xl p-6 shadow-xl"
|
||||
>
|
||||
<AnimatePresence mode="wait">
|
||||
{/* 检查中状态 */}
|
||||
{checking && (
|
||||
<motion.div
|
||||
key="checking"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="text-center py-8"
|
||||
>
|
||||
<Loader2 className="w-12 h-12 text-brand-500 animate-spin mx-auto mb-4" />
|
||||
<p className="text-dark-300">正在检测系统环境...</p>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{/* 安装步骤 */}
|
||||
{!checking && step === 'install' && envStatus && (
|
||||
<motion.div
|
||||
key="install"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="space-y-6"
|
||||
>
|
||||
{/* 系统信息 */}
|
||||
<div className="flex items-center justify-between text-sm text-dark-400 pb-4 border-b border-dark-700">
|
||||
<span>操作系统</span>
|
||||
<span className="text-dark-200">{getOsName(envStatus.os)}</span>
|
||||
</div>
|
||||
|
||||
{/* Node.js 状态 */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`p-2 rounded-lg ${
|
||||
envStatus.node_installed && envStatus.node_version_ok
|
||||
? 'bg-green-500/20 text-green-400'
|
||||
: 'bg-red-500/20 text-red-400'
|
||||
}`}>
|
||||
<Cpu className="w-5 h-5" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-white font-medium">Node.js</p>
|
||||
<p className="text-sm text-dark-400">
|
||||
{envStatus.node_version
|
||||
? `${envStatus.node_version} ${envStatus.node_version_ok ? '✓' : '(需要 v22+)'}`
|
||||
: '未安装'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{envStatus.node_installed && envStatus.node_version_ok ? (
|
||||
<CheckCircle2 className="w-6 h-6 text-green-400" />
|
||||
) : (
|
||||
<button
|
||||
onClick={handleInstallNodejs}
|
||||
disabled={installing !== null}
|
||||
className="btn-primary text-sm px-4 py-2 flex items-center gap-2"
|
||||
>
|
||||
{installing === 'nodejs' ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
安装中...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Download className="w-4 h-4" />
|
||||
安装
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* OpenClaw 状态 */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`p-2 rounded-lg ${
|
||||
envStatus.openclaw_installed
|
||||
? 'bg-green-500/20 text-green-400'
|
||||
: 'bg-red-500/20 text-red-400'
|
||||
}`}>
|
||||
<Package className="w-5 h-5" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-white font-medium">OpenClaw</p>
|
||||
<p className="text-sm text-dark-400">
|
||||
{envStatus.openclaw_version || '未安装'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{envStatus.openclaw_installed ? (
|
||||
<CheckCircle2 className="w-6 h-6 text-green-400" />
|
||||
) : (
|
||||
<button
|
||||
onClick={handleInstallOpenclaw}
|
||||
disabled={installing !== null || !envStatus.node_version_ok}
|
||||
className={`btn-primary text-sm px-4 py-2 flex items-center gap-2 ${
|
||||
!envStatus.node_version_ok ? 'opacity-50 cursor-not-allowed' : ''
|
||||
}`}
|
||||
title={!envStatus.node_version_ok ? '请先安装 Node.js' : ''}
|
||||
>
|
||||
{installing === 'openclaw' ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
安装中...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Download className="w-4 h-4" />
|
||||
安装
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 错误信息 */}
|
||||
{error && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="p-4 bg-yellow-500/10 border border-yellow-500/30 rounded-lg"
|
||||
>
|
||||
<p className="text-yellow-400 text-sm">{error}</p>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<div className="flex gap-3 pt-4 border-t border-dark-700">
|
||||
<button
|
||||
onClick={checkEnvironment}
|
||||
disabled={checking || installing !== null}
|
||||
className="flex-1 btn-secondary py-3 flex items-center justify-center gap-2"
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 ${checking ? 'animate-spin' : ''}`} />
|
||||
重新检查
|
||||
</button>
|
||||
|
||||
{envStatus.ready && (
|
||||
<button
|
||||
onClick={onComplete}
|
||||
className="flex-1 btn-primary py-3 flex items-center justify-center gap-2"
|
||||
>
|
||||
开始使用
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 帮助链接 */}
|
||||
<div className="text-center pt-2">
|
||||
<a
|
||||
href="https://nodejs.org/en/download"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm text-dark-400 hover:text-brand-400 transition-colors inline-flex items-center gap-1"
|
||||
>
|
||||
手动下载 Node.js
|
||||
<ExternalLink className="w-3 h-3" />
|
||||
</a>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{/* 完成状态 */}
|
||||
{!checking && step === 'complete' && (
|
||||
<motion.div
|
||||
key="complete"
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
className="text-center py-8"
|
||||
>
|
||||
<motion.div
|
||||
initial={{ scale: 0 }}
|
||||
animate={{ scale: 1 }}
|
||||
transition={{ type: 'spring', damping: 10, delay: 0.1 }}
|
||||
>
|
||||
<CheckCircle2 className="w-16 h-16 text-green-400 mx-auto mb-4" />
|
||||
</motion.div>
|
||||
<h3 className="text-xl font-bold text-white mb-2">环境就绪!</h3>
|
||||
<p className="text-dark-400 mb-6">
|
||||
Node.js 和 OpenClaw 已正确安装
|
||||
</p>
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.3 }}
|
||||
>
|
||||
<Loader2 className="w-5 h-5 text-brand-500 animate-spin mx-auto" />
|
||||
<p className="text-sm text-dark-500 mt-2">正在进入主界面...</p>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
|
||||
{/* 版本信息 */}
|
||||
<p className="text-center text-dark-500 text-xs mt-6">
|
||||
OpenClaw Manager v1.0.0
|
||||
</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user