feat: release v0.83.2 with full pending changes

This commit is contained in:
coso
2026-03-10 19:17:54 +08:00
parent e5d67a0d15
commit bfacf764b7
49 changed files with 2496 additions and 237 deletions
+3 -4
View File
@@ -6,7 +6,7 @@ use crate::models::app_type::AppType;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
use tauri::{AppHandle, Manager};
use tauri::AppHandle;
use tauri_plugin_autostart::ManagerExt;
#[cfg(target_os = "windows")]
@@ -1119,9 +1119,8 @@ fn get_download_directory(app_handle: &AppHandle) -> Result<PathBuf, String> {
}
// 回退到应用数据目录
let app_data_dir = app_handle
.path()
.app_data_dir()
let _ = app_handle;
let app_data_dir = proxycast_core::app_paths::preferred_data_dir()
.map_err(|e| format!("无法获取应用数据目录: {e}"))?;
let download_dir = app_data_dir.join("downloads");
+25 -38
View File
@@ -12,12 +12,12 @@ use crate::services::heartbeat_service::{
CycleResult, HeartbeatServiceState, HeartbeatStatus, HeartbeatTaskPreview,
};
use crate::AppState;
use proxycast_core::app_paths;
use proxycast_core::config::{DeliveryConfig, HeartbeatSecurityConfig, TaskSchedule};
use proxycast_core::database::dao::heartbeat::HeartbeatExecution;
use proxycast_websocket::handlers::{RpcHandler, RpcHandlerState};
use proxycast_websocket::protocol::{CronHealthResult, GatewayRpcRequest, RpcMethod};
use serde::{Deserialize, Serialize};
use tauri::Manager;
use uuid::Uuid;
// ========== 配置响应类型 ==========
@@ -135,9 +135,7 @@ pub async fn update_heartbeat_config(
// 处理启停逻辑
if config.enabled && !was_enabled {
service.set_app_handle(app.clone());
let app_data_dir = app
.path()
.app_data_dir()
let app_data_dir = app_paths::preferred_data_dir()
.map_err(|e| format!("无法获取应用数据目录: {e}"))?;
let self_ref = hb_state.0.clone();
service.start(app_data_dir, self_ref).await?;
@@ -162,12 +160,10 @@ pub async fn get_heartbeat_status(
#[tauri::command]
pub async fn get_heartbeat_tasks(
hb_state: tauri::State<'_, HeartbeatServiceState>,
app: tauri::AppHandle,
_app: tauri::AppHandle,
) -> Result<Vec<HeartbeatTaskPreview>, String> {
let app_data_dir = app
.path()
.app_data_dir()
.map_err(|e| format!("无法获取应用数据目录: {e}"))?;
let app_data_dir =
app_paths::preferred_data_dir().map_err(|e| format!("无法获取应用数据目录: {e}"))?;
let service = hb_state.0.read().await;
service.preview_tasks(&app_data_dir)
}
@@ -303,11 +299,12 @@ pub async fn get_task_templates() -> Result<Vec<TaskTemplate>, String> {
}
#[tauri::command]
pub async fn apply_task_template(template_id: String, app: tauri::AppHandle) -> Result<(), String> {
let app_data_dir = app
.path()
.app_data_dir()
.map_err(|e| format!("无法获取应用数据目录: {e}"))?;
pub async fn apply_task_template(
template_id: String,
_app: tauri::AppHandle,
) -> Result<(), String> {
let app_data_dir =
app_paths::preferred_data_dir().map_err(|e| format!("无法获取应用数据目录: {e}"))?;
let template = TaskTemplateRegistry::get_template_by_id(&template_id)
.ok_or_else(|| format!("模板不存在: {}", template_id))?;
@@ -320,17 +317,15 @@ pub async fn apply_task_template(template_id: String, app: tauri::AppHandle) ->
#[tauri::command]
pub async fn add_heartbeat_task(
hb_state: tauri::State<'_, HeartbeatServiceState>,
app: tauri::AppHandle,
_app: tauri::AppHandle,
description: String,
priority: Option<u8>,
timeout_secs: Option<u64>,
once: Option<bool>,
model: Option<String>,
) -> Result<(), String> {
let app_data_dir = app
.path()
.app_data_dir()
.map_err(|e| format!("无法获取应用数据目录: {e}"))?;
let app_data_dir =
app_paths::preferred_data_dir().map_err(|e| format!("无法获取应用数据目录: {e}"))?;
let service = hb_state.0.read().await;
service.add_task(
&app_data_dir,
@@ -345,13 +340,11 @@ pub async fn add_heartbeat_task(
#[tauri::command]
pub async fn delete_heartbeat_task(
hb_state: tauri::State<'_, HeartbeatServiceState>,
app: tauri::AppHandle,
_app: tauri::AppHandle,
index: usize,
) -> Result<(), String> {
let app_data_dir = app
.path()
.app_data_dir()
.map_err(|e| format!("无法获取应用数据目录: {e}"))?;
let app_data_dir =
app_paths::preferred_data_dir().map_err(|e| format!("无法获取应用数据目录: {e}"))?;
let service = hb_state.0.read().await;
service.delete_task(&app_data_dir, index)
}
@@ -359,7 +352,7 @@ pub async fn delete_heartbeat_task(
#[tauri::command]
pub async fn update_heartbeat_task(
hb_state: tauri::State<'_, HeartbeatServiceState>,
app: tauri::AppHandle,
_app: tauri::AppHandle,
index: usize,
description: String,
priority: Option<u8>,
@@ -367,10 +360,8 @@ pub async fn update_heartbeat_task(
once: Option<bool>,
model: Option<String>,
) -> Result<(), String> {
let app_data_dir = app
.path()
.app_data_dir()
.map_err(|e| format!("无法获取应用数据目录: {e}"))?;
let app_data_dir =
app_paths::preferred_data_dir().map_err(|e| format!("无法获取应用数据目录: {e}"))?;
let service = hb_state.0.read().await;
service.update_task(
&app_data_dir,
@@ -388,7 +379,7 @@ pub async fn update_heartbeat_task(
#[tauri::command]
pub async fn generate_content_creator_tasks(
state: tauri::State<'_, AppState>,
app: tauri::AppHandle,
_app: tauri::AppHandle,
) -> Result<usize, String> {
use crate::services::heartbeat_service::templates::ContentCreatorTaskGenerator;
@@ -401,10 +392,8 @@ pub async fn generate_content_creator_tasks(
let count = tasks.len();
if count > 0 {
let app_data_dir = app
.path()
.app_data_dir()
.map_err(|e| format!("无法获取应用数据目录: {e}"))?;
let app_data_dir =
app_paths::preferred_data_dir().map_err(|e| format!("无法获取应用数据目录: {e}"))?;
ContentCreatorTaskGenerator::append_to_heartbeat(tasks, &app_data_dir)?;
}
@@ -418,10 +407,8 @@ pub async fn trigger_heartbeat_now(
hb_state: tauri::State<'_, HeartbeatServiceState>,
app: tauri::AppHandle,
) -> Result<CycleResult, String> {
let app_data_dir = app
.path()
.app_data_dir()
.map_err(|e| format!("无法获取应用数据目录: {e}"))?;
let app_data_dir =
app_paths::preferred_data_dir().map_err(|e| format!("无法获取应用数据目录: {e}"))?;
let result = {
let service = hb_state.0.read().await;
+4 -6
View File
@@ -1698,17 +1698,15 @@ fn resolve_profile_data_dir_from_base(base_dir: &Path, profile_key: &str) -> Pat
}
fn resolve_profile_data_dir(app: &AppHandle, profile_key: &str) -> Result<PathBuf, String> {
let base_dir = app
.path()
.app_data_dir()
let _ = app;
let base_dir = proxycast_core::app_paths::preferred_data_dir()
.map_err(|e| format!("获取应用数据目录失败: {e}"))?;
Ok(resolve_profile_data_dir_from_base(&base_dir, profile_key))
}
fn resolve_chrome_profile_data_dir(app: &AppHandle, profile_key: &str) -> Result<PathBuf, String> {
let base_dir = app
.path()
.app_data_dir()
let _ = app;
let base_dir = proxycast_core::app_paths::preferred_data_dir()
.map_err(|e| format!("获取应用数据目录失败: {e}"))?;
Ok(base_dir
.join("chrome_profiles")
+45 -10
View File
@@ -10,6 +10,8 @@ use std::path::PathBuf;
#[cfg(target_os = "windows")]
use std::io::Write;
#[cfg(target_os = "windows")]
use std::os::windows::process::CommandExt;
#[cfg(target_os = "windows")]
use std::path::Path;
#[cfg(target_os = "windows")]
use std::process::Command;
@@ -18,6 +20,9 @@ use tauri_plugin_dialog::{DialogExt, MessageDialogButtons, MessageDialogKind};
#[cfg(target_os = "windows")]
use winreg::{enums::*, RegKey};
#[cfg(target_os = "windows")]
const CREATE_NO_WINDOW: u32 = 0x08000000;
#[derive(Debug, Clone, Serialize)]
pub struct WindowsStartupCheck {
pub key: String,
@@ -129,7 +134,7 @@ pub fn collect_windows_startup_diagnostics(app: &AppHandle) -> WindowsStartupDia
let mut errors = Vec::new();
let mut warnings = Vec::new();
let app_data_dir: Option<PathBuf> = app.path().app_data_dir().ok();
let app_data_dir: Option<PathBuf> = proxycast_core::app_paths::preferred_data_dir().ok();
let home_dir = dirs::home_dir();
let legacy_proxycast_dir = home_dir.clone().map(|home| home.join(".proxycast"));
let db_path = crate::database::get_db_path().ok();
@@ -171,23 +176,27 @@ pub fn collect_windows_startup_diagnostics(app: &AppHandle) -> WindowsStartupDia
}
match &legacy_proxycast_dir {
Some(path) => match ensure_dir_writable(path) {
Some(path) if path.exists() => match ensure_existing_dir_writable(path) {
Ok(()) => checks.push(ok_check(
"legacy_proxycast_dir",
format!("用户目录数据根可写: {}", path.display()),
format!("检测到旧版数据目录且可访问: {}", path.display()),
)),
Err(error) => {
errors.push(format!("用户目录数据根不可写: {}", path.display()));
checks.push(error_check(
warnings.push(format!("旧版数据目录不可访问: {}", path.display()));
checks.push(warn_check(
"legacy_proxycast_dir",
format!("用户目录数据根不可写: {}", path.display()),
format!("旧版数据目录不可访问: {}", path.display()),
Some(error),
));
}
},
Some(path) => checks.push(ok_check(
"legacy_proxycast_dir",
format!("未检测到旧版数据目录: {}", path.display()),
)),
None => {
errors.push("无法解析用户 Home 目录".to_string());
checks.push(error_check(
warnings.push("无法解析用户 Home 目录".to_string());
checks.push(warn_check(
"legacy_proxycast_dir",
"无法解析用户 Home 目录".to_string(),
None,
@@ -439,6 +448,25 @@ fn ensure_dir_writable(path: &Path) -> Result<(), String> {
Ok(())
}
#[cfg(target_os = "windows")]
fn ensure_existing_dir_writable(path: &Path) -> Result<(), String> {
if !path.exists() {
return Ok(());
}
let probe = path.join("proxycast-write-test.tmp");
let mut file = std::fs::File::create(&probe)
.map_err(|e| format!("创建测试文件失败 {}: {e}", probe.display()))?;
file.write_all(b"proxycast")
.map_err(|e| format!("写入测试文件失败 {}: {e}", probe.display()))?;
file.sync_all()
.map_err(|e| format!("刷新测试文件失败 {}: {e}", probe.display()))?;
std::fs::remove_file(&probe)
.map_err(|e| format!("删除测试文件失败 {}: {e}", probe.display()))?;
Ok(())
}
#[cfg(target_os = "windows")]
fn check_database_file(path: &Path) -> Result<(), String> {
if let Some(parent) = path.parent() {
@@ -447,11 +475,16 @@ fn check_database_file(path: &Path) -> Result<(), String> {
let conn = rusqlite::Connection::open(path)
.map_err(|e| format!("打开数据库失败 {}: {e}", path.display()))?;
conn.execute("PRAGMA user_version", [])
conn.query_row("PRAGMA user_version", [], |_| Ok(()))
.map_err(|e| format!("执行数据库探测失败 {}: {e}", path.display()))?;
Ok(())
}
#[cfg(target_os = "windows")]
fn apply_windows_no_window(command: &mut Command) {
command.creation_flags(CREATE_NO_WINDOW);
}
#[cfg(target_os = "windows")]
fn detect_webview2_runtime_version() -> Option<String> {
const VALUE_NAME: &str = "pv";
@@ -579,7 +612,9 @@ fn detect_shell_availability() -> Option<String> {
}
}
let pwsh_check = Command::new("pwsh").args(["-v"]).output();
let mut pwsh_check = Command::new("pwsh");
apply_windows_no_window(&mut pwsh_check);
let pwsh_check = pwsh_check.args(["-v"]).output();
if pwsh_check
.map(|output| output.status.success())
.unwrap_or(false)