auto check env

This commit is contained in:
miao
2026-02-01 15:56:25 +08:00
parent 1f0e937ed6
commit 2eda2dbac3
6 changed files with 1153 additions and 11 deletions
+638
View File
@@ -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.jsWindows 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
View File
@@ -1,4 +1,5 @@
pub mod config;
pub mod diagnostics;
pub mod installer;
pub mod process;
pub mod service;
+8 -1
View File
@@ -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 应用时发生错误");
+66 -9
View File
@@ -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
View File
@@ -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">
{/* 背景装饰 */}
+381
View File
@@ -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>
);
}