111
This commit is contained in:
95
src-tauri/Cargo.lock
generated
95
src-tauri/Cargo.lock
generated
@@ -4093,7 +4093,7 @@ dependencies = [
|
||||
"tao-macros",
|
||||
"unicode-segmentation",
|
||||
"url",
|
||||
"windows",
|
||||
"windows 0.61.3",
|
||||
"windows-core 0.61.2",
|
||||
"windows-version",
|
||||
"x11-dl",
|
||||
@@ -4166,12 +4166,12 @@ dependencies = [
|
||||
"webkit2gtk",
|
||||
"webview2-com",
|
||||
"window-vibrancy",
|
||||
"windows",
|
||||
"windows 0.61.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-app"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"async-trait",
|
||||
@@ -4206,6 +4206,7 @@ dependencies = [
|
||||
"tokio",
|
||||
"urlencoding",
|
||||
"uuid",
|
||||
"windows 0.58.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4346,7 +4347,7 @@ dependencies = [
|
||||
"tauri-plugin",
|
||||
"thiserror 2.0.18",
|
||||
"url",
|
||||
"windows",
|
||||
"windows 0.61.3",
|
||||
"zbus",
|
||||
]
|
||||
|
||||
@@ -4387,7 +4388,7 @@ dependencies = [
|
||||
"url",
|
||||
"webkit2gtk",
|
||||
"webview2-com",
|
||||
"windows",
|
||||
"windows 0.61.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4412,7 +4413,7 @@ dependencies = [
|
||||
"url",
|
||||
"webkit2gtk",
|
||||
"webview2-com",
|
||||
"windows",
|
||||
"windows 0.61.3",
|
||||
"wry",
|
||||
]
|
||||
|
||||
@@ -5318,10 +5319,10 @@ checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a"
|
||||
dependencies = [
|
||||
"webview2-com-macros",
|
||||
"webview2-com-sys",
|
||||
"windows",
|
||||
"windows 0.61.3",
|
||||
"windows-core 0.61.2",
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-implement 0.60.2",
|
||||
"windows-interface 0.59.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5342,7 +5343,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c"
|
||||
dependencies = [
|
||||
"thiserror 2.0.18",
|
||||
"windows",
|
||||
"windows 0.61.3",
|
||||
"windows-core 0.61.2",
|
||||
]
|
||||
|
||||
@@ -5392,6 +5393,16 @@ dependencies = [
|
||||
"windows-version",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.58.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6"
|
||||
dependencies = [
|
||||
"windows-core 0.58.0",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.61.3"
|
||||
@@ -5414,14 +5425,27 @@ dependencies = [
|
||||
"windows-core 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.58.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99"
|
||||
dependencies = [
|
||||
"windows-implement 0.58.0",
|
||||
"windows-interface 0.58.0",
|
||||
"windows-result 0.2.0",
|
||||
"windows-strings 0.1.0",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
|
||||
dependencies = [
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-implement 0.60.2",
|
||||
"windows-interface 0.59.3",
|
||||
"windows-link 0.1.3",
|
||||
"windows-result 0.3.4",
|
||||
"windows-strings 0.4.2",
|
||||
@@ -5433,8 +5457,8 @@ version = "0.62.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
|
||||
dependencies = [
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-implement 0.60.2",
|
||||
"windows-interface 0.59.3",
|
||||
"windows-link 0.2.1",
|
||||
"windows-result 0.4.1",
|
||||
"windows-strings 0.5.1",
|
||||
@@ -5451,6 +5475,17 @@ dependencies = [
|
||||
"windows-threading",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.58.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.60.2"
|
||||
@@ -5462,6 +5497,17 @@ dependencies = [
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-interface"
|
||||
version = "0.58.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-interface"
|
||||
version = "0.59.3"
|
||||
@@ -5495,6 +5541,15 @@ dependencies = [
|
||||
"windows-link 0.1.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.3.4"
|
||||
@@ -5513,6 +5568,16 @@ dependencies = [
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10"
|
||||
dependencies = [
|
||||
"windows-result 0.2.0",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.4.2"
|
||||
@@ -6018,7 +6083,7 @@ dependencies = [
|
||||
"webkit2gtk",
|
||||
"webkit2gtk-sys",
|
||||
"webview2-com",
|
||||
"windows",
|
||||
"windows 0.61.3",
|
||||
"windows-core 0.61.2",
|
||||
"windows-version",
|
||||
"x11-dl",
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
[package]
|
||||
name = "tauri-app"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
description = "A Tauri App"
|
||||
authors = ["you"]
|
||||
edition = "2021"
|
||||
default-run = "tauri-app"
|
||||
|
||||
[lib]
|
||||
name = "tauri_app_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[[bin]]
|
||||
name = "updater"
|
||||
path = "src/bin/updater/main.rs"
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
@@ -86,3 +91,9 @@ mac_address = "1.1"
|
||||
|
||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||
tauri-plugin-single-instance = "2"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows = { version = "0.58", features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_Storage_FileSystem",
|
||||
] }
|
||||
|
||||
56
src-tauri/scripts/bump-version.cjs
Normal file
56
src-tauri/scripts/bump-version.cjs
Normal file
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* 构建前将版本号 patch 段 +1,并同步写入 tauri.conf.json / Cargo.toml / package.json。
|
||||
*/
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
|
||||
const tauriDir = path.join(__dirname, "..");
|
||||
const rootDir = path.join(tauriDir, "..");
|
||||
|
||||
const tauriConfPath = path.join(tauriDir, "tauri.conf.json");
|
||||
const cargoTomlPath = path.join(tauriDir, "Cargo.toml");
|
||||
const packageJsonPath = path.join(rootDir, "package.json");
|
||||
|
||||
function bumpPatch(version) {
|
||||
const parts = String(version)
|
||||
.trim()
|
||||
.split(".")
|
||||
.map((p) => Number.parseInt(p, 10));
|
||||
if (parts.length === 0 || parts.some(Number.isNaN)) {
|
||||
throw new Error(`无法解析版本号: ${version}`);
|
||||
}
|
||||
parts[parts.length - 1] += 1;
|
||||
return parts.join(".");
|
||||
}
|
||||
|
||||
function updateJsonVersion(filePath, newVersion) {
|
||||
const data = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
||||
const oldVersion = data.version;
|
||||
data.version = newVersion;
|
||||
fs.writeFileSync(filePath, `${JSON.stringify(data, null, 2)}\n`, "utf8");
|
||||
return oldVersion;
|
||||
}
|
||||
|
||||
function updateCargoVersion(filePath, newVersion) {
|
||||
const content = fs.readFileSync(filePath, "utf8");
|
||||
const match = content.match(/^version\s*=\s*"([^"]+)"/m);
|
||||
if (!match) {
|
||||
throw new Error(`Cargo.toml 中未找到 version 字段: ${filePath}`);
|
||||
}
|
||||
const oldVersion = match[1];
|
||||
const next = content.replace(
|
||||
/^version\s*=\s*"[^"]+"/m,
|
||||
`version = "${newVersion}"`,
|
||||
);
|
||||
fs.writeFileSync(filePath, next, "utf8");
|
||||
return oldVersion;
|
||||
}
|
||||
|
||||
const conf = JSON.parse(fs.readFileSync(tauriConfPath, "utf8"));
|
||||
const newVersion = bumpPatch(conf.version);
|
||||
|
||||
updateJsonVersion(tauriConfPath, newVersion);
|
||||
updateJsonVersion(packageJsonPath, newVersion);
|
||||
updateCargoVersion(cargoTomlPath, newVersion);
|
||||
|
||||
console.log(`版本号已更新: ${conf.version} -> ${newVersion}`);
|
||||
@@ -6,6 +6,31 @@ const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const { execSync } = require("node:child_process");
|
||||
|
||||
const RELEASE_VERSION_URL =
|
||||
"http://81.71.163.140:8001/api/setreleaseversion";
|
||||
|
||||
async function notifyReleaseVersion(version) {
|
||||
const res = await fetch(RELEASE_VERSION_URL, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ version }),
|
||||
});
|
||||
const body = await res.text();
|
||||
if (!res.ok) {
|
||||
throw new Error(`HTTP ${res.status}: ${body}`);
|
||||
}
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(body);
|
||||
} catch {
|
||||
throw new Error(`响应不是 JSON: ${body}`);
|
||||
}
|
||||
if (parsed.ok === false) {
|
||||
throw new Error(parsed.message || "写入 releaseversion 失败");
|
||||
}
|
||||
console.log(`已同步 releaseversion 到服务器: ${version}`);
|
||||
}
|
||||
|
||||
const tauriDir = path.join(__dirname, "..");
|
||||
const conf = JSON.parse(
|
||||
fs.readFileSync(path.join(tauriDir, "tauri.conf.json"), "utf8"),
|
||||
@@ -45,6 +70,11 @@ const stagedExe =
|
||||
process.platform === "win32" ? `${productName}.exe` : productName;
|
||||
fs.copyFileSync(exePath, path.join(stageDir, stagedExe));
|
||||
|
||||
const updaterPath = path.join(releaseDir, "updater.exe");
|
||||
if (process.platform === "win32" && fs.existsSync(updaterPath)) {
|
||||
fs.copyFileSync(updaterPath, path.join(stageDir, "updater.exe"));
|
||||
}
|
||||
|
||||
for (const dir of ["resources", "binaries"]) {
|
||||
const src = path.join(releaseDir, dir);
|
||||
if (fs.existsSync(src)) {
|
||||
@@ -78,3 +108,8 @@ for (const dir of ["msi", "nsis"]) {
|
||||
}
|
||||
|
||||
console.log(`便携压缩包已生成: ${zipPath}`);
|
||||
|
||||
notifyReleaseVersion(version).catch((err) => {
|
||||
console.error(`同步 releaseversion 失败: ${err.message}`);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
282
src-tauri/src/bin/updater/engine.rs
Normal file
282
src-tauri/src/bin/updater/engine.rs
Normal file
@@ -0,0 +1,282 @@
|
||||
//! updater.exe 独立更新逻辑(不依赖 Tauri 主库)。
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
const ENV_FROM_UPDATER: &str = "AICLIENT_FROM_UPDATER";
|
||||
const MAIN_EXE: &str = "aiclient.exe";
|
||||
const CHECK_UPDATE_URL: &str = "http://81.71.163.140:8001/api/checkupdate";
|
||||
const DOWNLOAD_URL: &str = "http://81.71.163.140:80/images/mainfile/main.exe";
|
||||
|
||||
fn exe_dir() -> Result<PathBuf, String> {
|
||||
std::env::current_exe()
|
||||
.map_err(|e| format!("无法获取当前可执行文件路径: {e}"))
|
||||
.and_then(|p| {
|
||||
p.parent()
|
||||
.map(Path::to_path_buf)
|
||||
.ok_or_else(|| "无法获取可执行文件所在目录".to_string())
|
||||
})
|
||||
}
|
||||
|
||||
fn normalize_version(raw: &str) -> String {
|
||||
let trimmed = raw.trim().trim_start_matches(['v', 'V']);
|
||||
let mut parts: Vec<u32> = trimmed
|
||||
.split('.')
|
||||
.filter_map(|p| p.parse().ok())
|
||||
.collect();
|
||||
if parts.is_empty() {
|
||||
return trimmed.to_string();
|
||||
}
|
||||
while parts.len() > 1 && *parts.last().unwrap_or(&0) == 0 {
|
||||
parts.pop();
|
||||
}
|
||||
parts
|
||||
.iter()
|
||||
.map(|n| n.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(".")
|
||||
}
|
||||
|
||||
fn versions_match(local: &str, remote: &str) -> bool {
|
||||
normalize_version(local) == normalize_version(remote)
|
||||
}
|
||||
|
||||
fn parse_remote_version(body: &str) -> Result<String, String> {
|
||||
let trimmed = body.trim();
|
||||
if trimmed.is_empty() {
|
||||
return Err("更新接口返回空内容".to_string());
|
||||
}
|
||||
|
||||
if let Ok(value) = serde_json::from_str::<Value>(trimmed) {
|
||||
for key in ["version", "latest_version", "latestVersion", "app_version"] {
|
||||
if let Some(v) = value.get(key).and_then(|x| x.as_str()) {
|
||||
if !v.trim().is_empty() {
|
||||
return Ok(v.trim().to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(data) = value.get("data") {
|
||||
for key in ["version", "latest_version", "latestVersion", "app_version"] {
|
||||
if let Some(v) = data.get(key).and_then(|x| x.as_str()) {
|
||||
if !v.trim().is_empty() {
|
||||
return Ok(v.trim().to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if trimmed.chars().all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-') {
|
||||
return Ok(trimmed.to_string());
|
||||
}
|
||||
|
||||
Err(format!("无法解析更新接口返回的版本号: {trimmed}"))
|
||||
}
|
||||
|
||||
async fn fetch_latest_version(client: &reqwest::Client) -> Result<String, String> {
|
||||
let response = client
|
||||
.get(CHECK_UPDATE_URL)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("请求更新接口失败: {e}"))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(format!("更新接口 HTTP {}", response.status()));
|
||||
}
|
||||
|
||||
let body = response
|
||||
.text()
|
||||
.await
|
||||
.map_err(|e| format!("读取更新接口响应失败: {e}"))?;
|
||||
parse_remote_version(&body)
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn read_exe_version(path: &Path) -> Result<String, String> {
|
||||
use std::ffi::c_void;
|
||||
use std::os::windows::ffi::OsStrExt;
|
||||
|
||||
use windows::core::PCWSTR;
|
||||
use windows::Win32::Storage::FileSystem::{
|
||||
GetFileVersionInfoSizeW, GetFileVersionInfoW, VerQueryValueW,
|
||||
};
|
||||
|
||||
let wide: Vec<u16> = path
|
||||
.as_os_str()
|
||||
.encode_wide()
|
||||
.chain(std::iter::once(0))
|
||||
.collect();
|
||||
|
||||
let size = unsafe { GetFileVersionInfoSizeW(PCWSTR(wide.as_ptr()), None) };
|
||||
if size == 0 {
|
||||
return Err(format!("无法读取版本信息: {}", path.display()));
|
||||
}
|
||||
|
||||
let mut buffer = vec![0u8; size as usize];
|
||||
unsafe {
|
||||
GetFileVersionInfoW(
|
||||
PCWSTR(wide.as_ptr()),
|
||||
0,
|
||||
size,
|
||||
buffer.as_mut_ptr() as *mut c_void,
|
||||
)
|
||||
.map_err(|e| format!("GetFileVersionInfoW 失败: {e}"))?;
|
||||
}
|
||||
|
||||
let mut value_ptr: *mut c_void = std::ptr::null_mut();
|
||||
let mut value_len: u32 = 0;
|
||||
let sub_block: Vec<u16> = "\\"
|
||||
.encode_utf16()
|
||||
.chain(std::iter::once(0))
|
||||
.collect();
|
||||
|
||||
unsafe {
|
||||
if !VerQueryValueW(
|
||||
buffer.as_ptr() as *const c_void,
|
||||
PCWSTR(sub_block.as_ptr()),
|
||||
&mut value_ptr,
|
||||
&mut value_len,
|
||||
)
|
||||
.as_bool()
|
||||
{
|
||||
return Err("VerQueryValueW 失败".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
if value_ptr.is_null() || value_len < 2 {
|
||||
return Err("版本信息为空".to_string());
|
||||
}
|
||||
|
||||
let translate = unsafe {
|
||||
std::slice::from_raw_parts(value_ptr as *const u16, (value_len / 2) as usize)
|
||||
};
|
||||
let lang = format!("{:04x}{:04x}", translate[0], translate[1]);
|
||||
|
||||
for key in ["ProductVersion", "FileVersion"] {
|
||||
let query: Vec<u16> = format!("\\StringFileInfo\\{lang}\\{key}")
|
||||
.encode_utf16()
|
||||
.chain(std::iter::once(0))
|
||||
.collect();
|
||||
value_ptr = std::ptr::null_mut();
|
||||
value_len = 0;
|
||||
let ok = unsafe {
|
||||
VerQueryValueW(
|
||||
buffer.as_ptr() as *const c_void,
|
||||
PCWSTR(query.as_ptr()),
|
||||
&mut value_ptr,
|
||||
&mut value_len,
|
||||
)
|
||||
};
|
||||
if ok.as_bool() && !value_ptr.is_null() && value_len >= 2 {
|
||||
let slice = unsafe {
|
||||
std::slice::from_raw_parts(value_ptr as *const u16, value_len as usize)
|
||||
};
|
||||
let end = slice.iter().position(|&c| c == 0).unwrap_or(slice.len());
|
||||
let version = String::from_utf16_lossy(&slice[..end]);
|
||||
if !version.trim().is_empty() {
|
||||
return Ok(version.trim().to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(format!("未在 {} 中找到版本字符串", path.display()))
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
fn read_exe_version(path: &Path) -> Result<String, String> {
|
||||
let _ = path;
|
||||
Err("当前平台不支持读取可执行文件版本".to_string())
|
||||
}
|
||||
|
||||
async fn download_and_replace(client: &reqwest::Client, main_path: &Path) -> Result<(), String> {
|
||||
let response = client
|
||||
.get(DOWNLOAD_URL)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("下载主程序失败: {e}"))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(format!("下载主程序 HTTP {}", response.status()));
|
||||
}
|
||||
|
||||
let bytes = response
|
||||
.bytes()
|
||||
.await
|
||||
.map_err(|e| format!("读取下载内容失败: {e}"))?;
|
||||
|
||||
if bytes.is_empty() {
|
||||
return Err("下载的主程序为空".to_string());
|
||||
}
|
||||
|
||||
let temp_path = main_path.with_extension("exe.download");
|
||||
std::fs::write(&temp_path, &bytes).map_err(|e| format!("写入临时文件失败: {e}"))?;
|
||||
|
||||
if main_path.exists() {
|
||||
std::fs::remove_file(main_path).map_err(|e| format!("删除旧主程序失败: {e}"))?;
|
||||
}
|
||||
|
||||
std::fs::rename(&temp_path, main_path).map_err(|e| {
|
||||
let _ = std::fs::remove_file(&temp_path);
|
||||
format!("替换主程序失败: {e}")
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn launch_main_app(main_path: &Path, dir: &Path) -> Result<(), String> {
|
||||
if !main_path.is_file() {
|
||||
return Err(format!("主程序不存在: {}", main_path.display()));
|
||||
}
|
||||
|
||||
Command::new(main_path)
|
||||
.current_dir(dir)
|
||||
.env(ENV_FROM_UPDATER, "1")
|
||||
.spawn()
|
||||
.map_err(|e| format!("启动主程序失败: {e}"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn run_updater() -> Result<(), String> {
|
||||
let dir = exe_dir()?;
|
||||
let main_path = dir.join(MAIN_EXE);
|
||||
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(120))
|
||||
.build()
|
||||
.map_err(|e| format!("创建 HTTP 客户端失败: {e}"))?;
|
||||
|
||||
let remote_version = fetch_latest_version(&client).await?;
|
||||
let needs_update = if main_path.is_file() {
|
||||
match read_exe_version(&main_path) {
|
||||
Ok(local_version) => {
|
||||
let matched = versions_match(&local_version, &remote_version);
|
||||
if matched {
|
||||
eprintln!(
|
||||
"当前版本 {local_version} 已是最新 ({remote_version}),跳过下载"
|
||||
);
|
||||
} else {
|
||||
eprintln!(
|
||||
"版本不一致: 本地 {local_version},远端 {remote_version},开始更新"
|
||||
);
|
||||
}
|
||||
!matched
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("读取本地版本失败 ({e}),将尝试下载主程序");
|
||||
true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
eprintln!("主程序不存在,将下载: {}", main_path.display());
|
||||
true
|
||||
};
|
||||
|
||||
if needs_update {
|
||||
download_and_replace(&client, &main_path).await?;
|
||||
}
|
||||
|
||||
launch_main_app(&main_path, &dir)?;
|
||||
Ok(())
|
||||
}
|
||||
12
src-tauri/src/bin/updater/main.rs
Normal file
12
src-tauri/src/bin/updater/main.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
mod engine;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
if let Err(e) = engine::run_updater().await {
|
||||
eprintln!("updater 运行失败: {e}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod updater;
|
||||
pub mod api_client;
|
||||
pub mod api_crypto;
|
||||
pub mod app_config;
|
||||
|
||||
@@ -4,5 +4,14 @@
|
||||
// 心跳定时任务在 `tauri_app_lib::run` → `api_client::spawn_heartbeat_loop` 中启动(每 30 秒)。
|
||||
|
||||
fn main() {
|
||||
#[cfg(all(windows, not(debug_assertions)))]
|
||||
{
|
||||
if !tauri_app_lib::updater::launched_by_updater() {
|
||||
if tauri_app_lib::updater::try_launch_updater().is_ok() {
|
||||
std::process::exit(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tauri_app_lib::run()
|
||||
}
|
||||
|
||||
39
src-tauri/src/updater/mod.rs
Normal file
39
src-tauri/src/updater/mod.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
//! 主程序启动时检测是否由 updater 拉起,否则转交 updater.exe。
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
|
||||
pub const ENV_FROM_UPDATER: &str = "AICLIENT_FROM_UPDATER";
|
||||
pub const MAIN_EXE: &str = "aiclient.exe";
|
||||
pub const UPDATER_EXE: &str = "updater.exe";
|
||||
|
||||
pub fn launched_by_updater() -> bool {
|
||||
std::env::var(ENV_FROM_UPDATER)
|
||||
.map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn exe_dir() -> Result<PathBuf, String> {
|
||||
std::env::current_exe()
|
||||
.map_err(|e| format!("无法获取当前可执行文件路径: {e}"))
|
||||
.and_then(|p| {
|
||||
p.parent()
|
||||
.map(Path::to_path_buf)
|
||||
.ok_or_else(|| "无法获取可执行文件所在目录".to_string())
|
||||
})
|
||||
}
|
||||
|
||||
/// 主程序未由 updater 启动时,尝试拉起 updater。
|
||||
pub fn try_launch_updater() -> Result<(), String> {
|
||||
let dir = exe_dir()?;
|
||||
let updater_path = dir.join(UPDATER_EXE);
|
||||
if !updater_path.is_file() {
|
||||
return Err(format!("未找到 {}", updater_path.display()));
|
||||
}
|
||||
|
||||
Command::new(&updater_path)
|
||||
.current_dir(&dir)
|
||||
.spawn()
|
||||
.map_err(|e| format!("spawn {} 失败: {e}", updater_path.display()))?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "aiclient",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.1",
|
||||
"identifier": "com.aiclient.desktop",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev",
|
||||
@@ -11,15 +11,17 @@
|
||||
},
|
||||
"app": {
|
||||
"withGlobalTauri": true,
|
||||
"windows": [
|
||||
|
||||
],
|
||||
"windows": [],
|
||||
"security": {
|
||||
"capabilities": ["default"],
|
||||
"capabilities": [
|
||||
"default"
|
||||
],
|
||||
"csp": null,
|
||||
"assetProtocol": {
|
||||
"enable": true,
|
||||
"scope": ["**"]
|
||||
"scope": [
|
||||
"**"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user