fix: resolve Windows compilation errors and warnings

- Add Manager trait import for windows_startup_cmd.rs
- Add explicit type annotations for PathBuf in windows_startup_cmd.rs
- Fix unused import warning in screenshot_capture_service.rs
- Prefix unused variables with underscore in runner.rs and aster_agent_cmd.rs
- Update package.json with Node.js engine requirement
This commit is contained in:
coso
2026-03-10 09:20:40 +08:00
parent 276e2064c2
commit 2adf8c2bc2
33 changed files with 1484 additions and 305 deletions
+302 -61
View File
@@ -5,13 +5,13 @@ use rand::{distributions::Alphanumeric, Rng};
use regex::Regex;
use serde::{Deserialize, Serialize};
use serde_json::{json, Map, Value};
use std::collections::HashSet;
use std::collections::{HashSet, VecDeque};
use std::path::{Path, PathBuf};
use std::process::Stdio;
use std::sync::OnceLock;
use std::time::SystemTime;
use tauri::{AppHandle, Emitter, Manager};
use tokio::io::{AsyncBufReadExt, BufReader};
use tokio::io::{AsyncBufReadExt, AsyncRead, AsyncReadExt, BufReader};
use tokio::net::TcpStream;
use tokio::process::{Child, Command};
use tokio::sync::Mutex;
@@ -24,6 +24,7 @@ const OPENCLAW_CN_PACKAGE: &str = "@qingchencloud/openclaw-zh@latest";
const OPENCLAW_DEFAULT_PACKAGE: &str = "openclaw@latest";
const NPM_MIRROR_CN: &str = "https://registry.npmmirror.com";
const NODE_MIN_VERSION: (u64, u64, u64) = (22, 0, 0);
const OPENCLAW_PROGRESS_LOG_LIMIT: usize = 400;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
@@ -54,6 +55,13 @@ pub struct ActionResult {
pub message: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CommandPreview {
pub title: String,
pub command: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GatewayStatusInfo {
@@ -110,6 +118,7 @@ pub struct OpenClawService {
gateway_port: u16,
gateway_auth_token: String,
gateway_started_at: Option<SystemTime>,
progress_logs: VecDeque<InstallProgressEvent>,
}
impl Default for OpenClawService {
@@ -120,6 +129,7 @@ impl Default for OpenClawService {
gateway_port: DEFAULT_GATEWAY_PORT,
gateway_auth_token: String::new(),
gateway_started_at: None,
progress_logs: VecDeque::new(),
}
}
}
@@ -133,6 +143,38 @@ impl Default for OpenClawServiceState {
}
impl OpenClawService {
pub fn clear_progress_logs(&mut self) {
self.progress_logs.clear();
}
pub fn get_progress_logs(&self) -> Vec<InstallProgressEvent> {
self.progress_logs.iter().cloned().collect()
}
fn push_progress_log(&mut self, message: String, level: String) {
if self.progress_logs.len() >= OPENCLAW_PROGRESS_LOG_LIMIT {
self.progress_logs.pop_front();
}
self.progress_logs
.push_back(InstallProgressEvent { message, level });
}
pub async fn get_command_preview(
&mut self,
app: &AppHandle,
operation: &str,
port: Option<u16>,
) -> Result<CommandPreview, String> {
match operation {
"install" => self.build_install_command_preview(app).await,
"uninstall" => self.build_uninstall_command_preview().await,
"restart" => self.build_restart_command_preview(port).await,
"start" => self.build_start_command_preview(port).await,
"stop" => self.build_stop_command_preview(port).await,
_ => Err(format!("不支持的 OpenClaw 操作预览: {operation}")),
}
}
pub async fn check_installed(&self) -> Result<BinaryInstallStatus, String> {
let path = find_command_in_shell("openclaw").await?;
Ok(BinaryInstallStatus {
@@ -215,29 +257,8 @@ impl OpenClawService {
}
pub async fn install(&self, app: &AppHandle) -> Result<ActionResult, String> {
let npm_path = find_command_in_shell("npm")
.await?
.ok_or_else(|| "未检测到 npm,可先安装或修复 Node.js 环境。".to_string())?;
let npm_prefix = detect_npm_global_prefix(&npm_path).await;
let package = if should_use_china_package(app) {
OPENCLAW_CN_PACKAGE
} else {
OPENCLAW_DEFAULT_PACKAGE
};
let prefix_env = npm_prefix
.as_deref()
.map(shell_env_assignment)
.unwrap_or_default();
let npm_cmd = shell_escape(&npm_path);
let cleanup_command = format!(
"{prefix_env}{npm_cmd} uninstall -g openclaw @qingchencloud/openclaw-zh || true"
);
let install_command = if should_use_china_package(app) {
format!("{prefix_env}{npm_cmd} install -g {package} --registry={NPM_MIRROR_CN}")
} else {
format!("{prefix_env}{npm_cmd} install -g {package}")
};
let (_, npm_path, npm_prefix, cleanup_command, install_command) =
self.resolve_install_commands(app).await?;
let command = format!("{cleanup_command}\n{install_command}");
emit_install_progress(app, &format!("使用 npm: {npm_path}"), "info");
@@ -255,19 +276,7 @@ impl OpenClawService {
let _ = self.stop_gateway(None).await;
}
let npm_path = find_command_in_shell("npm")
.await?
.ok_or_else(|| "未检测到 npm,可先安装或修复 Node.js 环境。".to_string())?;
let npm_prefix = detect_npm_global_prefix(&npm_path).await;
let prefix_env = npm_prefix
.as_deref()
.map(shell_env_assignment)
.unwrap_or_default();
let command = format!(
"{}{} uninstall -g openclaw @qingchencloud/openclaw-zh",
prefix_env,
shell_escape(&npm_path)
);
let (npm_path, npm_prefix, command) = self.resolve_uninstall_command().await?;
emit_install_progress(app, &format!("使用 npm: {npm_path}"), "info");
if let Some(prefix) = npm_prefix {
@@ -902,6 +911,148 @@ impl OpenClawService {
std::fs::write(proxycast_config_path, content).map_err(|e| format!("写入配置失败: {e}"))?;
Ok(())
}
async fn resolve_install_commands(
&self,
app: &AppHandle,
) -> Result<(String, String, Option<String>, String, String), String> {
let npm_path = find_command_in_shell("npm")
.await?
.ok_or_else(|| "未检测到 npm,可先安装或修复 Node.js 环境。".to_string())?;
let npm_prefix = detect_npm_global_prefix(&npm_path).await;
let package = if should_use_china_package(app).await {
OPENCLAW_CN_PACKAGE
} else {
OPENCLAW_DEFAULT_PACKAGE
};
let prefix_env = npm_prefix
.as_deref()
.map(shell_env_assignment)
.unwrap_or_default();
let npm_cmd = shell_escape(&npm_path);
let cleanup_command = format!(
"{prefix_env}{npm_cmd} uninstall -g openclaw @qingchencloud/openclaw-zh || true"
);
let install_command = if should_use_china_package(app).await {
format!("{prefix_env}{npm_cmd} install -g {package} --registry={NPM_MIRROR_CN}")
} else {
format!("{prefix_env}{npm_cmd} install -g {package}")
};
Ok((
package.to_string(),
npm_path,
npm_prefix,
cleanup_command,
install_command,
))
}
async fn resolve_uninstall_command(&self) -> Result<(String, Option<String>, String), String> {
let npm_path = find_command_in_shell("npm")
.await?
.ok_or_else(|| "未检测到 npm,可先安装或修复 Node.js 环境。".to_string())?;
let npm_prefix = detect_npm_global_prefix(&npm_path).await;
let prefix_env = npm_prefix
.as_deref()
.map(shell_env_assignment)
.unwrap_or_default();
let command = format!(
"{}{} uninstall -g openclaw @qingchencloud/openclaw-zh",
prefix_env,
shell_escape(&npm_path)
);
Ok((npm_path, npm_prefix, command))
}
async fn build_install_command_preview(
&self,
app: &AppHandle,
) -> Result<CommandPreview, String> {
let (package, npm_path, npm_prefix, cleanup_command, install_command) =
self.resolve_install_commands(app).await?;
let prefix_note = npm_prefix
.map(|prefix| format!("npm: {npm_path}\nprefix: {prefix}\n"))
.unwrap_or_else(|| format!("npm: {npm_path}\n"));
Ok(CommandPreview {
title: format!("安装 {package}"),
command: format!("{prefix_note}{cleanup_command}\n{install_command}"),
})
}
async fn build_uninstall_command_preview(&self) -> Result<CommandPreview, String> {
let (npm_path, npm_prefix, command) = self.resolve_uninstall_command().await?;
let prefix_note = npm_prefix
.map(|prefix| format!("npm: {npm_path}\nprefix: {prefix}\n"))
.unwrap_or_else(|| format!("npm: {npm_path}\n"));
Ok(CommandPreview {
title: "卸载 OpenClaw".to_string(),
command: format!("{prefix_note}{command}"),
})
}
async fn build_start_command_preview(
&mut self,
port: Option<u16>,
) -> Result<CommandPreview, String> {
if let Some(next_port) = port {
self.gateway_port = next_port.max(1);
}
self.restore_auth_token_from_config();
let binary = find_command_in_shell("openclaw")
.await?
.ok_or_else(|| "未检测到 OpenClaw 可执行文件,请先安装。".to_string())?;
let config_path = openclaw_proxycast_config_path();
Ok(CommandPreview {
title: "启动 Gateway".to_string(),
command: format!(
"{}OPENCLAW_CONFIG_PATH={} {} gateway --port {}",
if cfg!(target_os = "windows") {
"set "
} else {
""
},
shell_escape(config_path.to_string_lossy().as_ref()),
shell_escape(&binary),
self.gateway_port
),
})
}
async fn build_stop_command_preview(
&mut self,
port: Option<u16>,
) -> Result<CommandPreview, String> {
if let Some(next_port) = port {
self.gateway_port = next_port.max(1);
}
self.restore_auth_token_from_config();
let binary = find_command_in_shell("openclaw")
.await?
.ok_or_else(|| "未检测到 OpenClaw 可执行文件,请先安装。".to_string())?;
let config_path = openclaw_proxycast_config_path();
Ok(CommandPreview {
title: "停止 Gateway".to_string(),
command: format!(
"OPENCLAW_CONFIG_PATH={} {} gateway stop --url {} --token {}",
shell_escape(config_path.to_string_lossy().as_ref()),
shell_escape(&binary),
self.gateway_ws_url(),
shell_escape(&self.gateway_auth_token)
),
})
}
async fn build_restart_command_preview(
&mut self,
port: Option<u16>,
) -> Result<CommandPreview, String> {
let stop = self.build_stop_command_preview(port).await?;
let start = self.build_start_command_preview(port).await?;
Ok(CommandPreview {
title: "重启 Gateway".to_string(),
command: format!("{}\n{}", stop.command, start.command),
})
}
}
pub fn openclaw_install_event_name() -> &'static str {
@@ -1091,12 +1242,12 @@ fn generate_auth_token() -> String {
.collect()
}
fn should_use_china_package(app: &AppHandle) -> bool {
async fn should_use_china_package(app: &AppHandle) -> bool {
if let Some(app_state) = app.try_state::<AppState>() {
let language = tauri::async_runtime::block_on(async {
let language = {
let state = app_state.read().await;
state.config.language.clone()
});
};
if language.starts_with("zh") {
return true;
@@ -1273,31 +1424,14 @@ async fn run_shell_command_with_progress(
let stdout_task = child.stdout.take().map(|stdout| {
let app = app.clone();
tokio::spawn(async move {
let mut lines = BufReader::new(stdout).lines();
while let Ok(Some(line)) = lines.next_line().await {
let trimmed = line.trim();
if !trimmed.is_empty() {
emit_install_progress(&app, trimmed, "info");
}
}
stream_reader_to_progress(app, stdout, "info").await;
})
});
let stderr_task = child.stderr.take().map(|stderr| {
let app = app.clone();
tokio::spawn(async move {
let mut lines = BufReader::new(stderr).lines();
while let Ok(Some(line)) = lines.next_line().await {
let trimmed = line.trim();
if !trimmed.is_empty() {
let level = if trimmed.to_ascii_lowercase().contains("warn") {
"warn"
} else {
"error"
};
emit_install_progress(&app, trimmed, level);
}
}
stream_reader_to_progress(app, stderr, "error").await;
})
});
@@ -1337,6 +1471,15 @@ fn spawn_shell_command(command_line: &str) -> Result<Child, String> {
let mut cmd = Command::new("cmd");
cmd.arg("/C").arg(command_line);
cmd
} else if cfg!(target_os = "macos") {
let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/zsh".to_string());
let mut cmd = Command::new("script");
cmd.arg("-q")
.arg("/dev/null")
.arg(shell)
.arg("-lc")
.arg(command_line);
cmd
} else {
let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/sh".to_string());
let mut cmd = Command::new(shell);
@@ -1344,11 +1487,109 @@ fn spawn_shell_command(command_line: &str) -> Result<Child, String> {
cmd
};
command.stdout(Stdio::piped()).stderr(Stdio::piped());
command
.env("NO_COLOR", "1")
.env("CLICOLOR", "0")
.env("FORCE_COLOR", "0")
.env("npm_config_color", "false")
.stdout(Stdio::piped())
.stderr(Stdio::piped());
command.spawn().map_err(|e| format!("启动命令失败: {e}"))
}
async fn stream_reader_to_progress<R>(app: AppHandle, mut reader: R, default_level: &'static str)
where
R: AsyncRead + Unpin,
{
let mut buffer = [0_u8; 2048];
let mut pending = String::new();
loop {
match reader.read(&mut buffer).await {
Ok(0) => break,
Ok(size) => {
pending.push_str(&String::from_utf8_lossy(&buffer[..size]));
flush_progress_chunks(&app, &mut pending, default_level);
}
Err(error) => {
emit_install_progress(&app, &format!("读取命令输出失败: {error}"), "warn");
break;
}
}
}
let tail = pending.trim();
if !tail.is_empty() {
emit_install_progress(&app, tail, classify_progress_level(tail, default_level));
}
}
fn flush_progress_chunks(app: &AppHandle, pending: &mut String, default_level: &'static str) {
loop {
let next_break = pending.find(['\n', '\r']);
let Some(index) = next_break else {
break;
};
let mut line = pending[..index].trim().to_string();
let mut consume_len = index + 1;
while pending
.get(consume_len..consume_len + 1)
.is_some_and(|ch| ch == "\n" || ch == "\r")
{
consume_len += 1;
}
pending.drain(..consume_len);
if line.is_empty() {
continue;
}
line = sanitize_progress_line(&line);
if line.is_empty() {
continue;
}
emit_install_progress(app, &line, classify_progress_level(&line, default_level));
}
if pending.len() > 4096 {
let line = sanitize_progress_line(pending.trim());
if !line.is_empty() {
emit_install_progress(app, &line, classify_progress_level(&line, default_level));
}
pending.clear();
}
}
fn sanitize_progress_line(value: &str) -> String {
value
.replace('\u{1b}', "")
.replace("[?25h", "")
.replace("[?25l", "")
.trim()
.to_string()
}
fn classify_progress_level(message: &str, default_level: &'static str) -> &'static str {
let lower = message.to_ascii_lowercase();
if lower.contains("error") || lower.contains("fatal") {
"error"
} else if lower.contains("warn") || lower.contains("warning") {
"warn"
} else {
default_level
}
}
fn emit_install_progress(app: &AppHandle, message: &str, level: &str) {
if let Some(service_state) = app.try_state::<OpenClawServiceState>() {
if let Ok(mut service) = service_state.0.try_lock() {
service.push_progress_log(message.to_string(), level.to_string());
}
}
let payload = InstallProgressEvent {
message: message.to_string(),
level: level.to_string(),