win系统适配完成

This commit is contained in:
PineWin
2026-05-30 15:39:42 +08:00
parent b8debfcd46
commit 304f4b54ff
67 changed files with 323 additions and 111 deletions
+7
View File
@@ -0,0 +1,7 @@
{
"permissions": {
"allow": [
"Bash(cargo check *)"
]
}
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

+77 -19
View File
@@ -27,35 +27,93 @@
}
@keyframes spin { to { transform: rotate(360deg); } }
p { color: #4a5568; font-size: 14px; }
.status { color: #718096; font-size: 13px; margin-top: 12px; }
.retry-btn {
display: none;
margin-top: 16px;
padding: 8px 24px;
background: #2563eb;
color: #fff;
border: none;
border-radius: 6px;
font-size: 14px;
cursor: pointer;
}
.retry-btn:hover { background: #1d4ed8; }
.retry-btn.show { display: inline-block; }
</style>
<script>
// 等待 streamlit 服务就绪后跳转
(function() {
var STREAMLIT_URL = 'http://localhost:8501';
var retries = 0;
var maxRetries = 50;
var maxRetries = 120; // 最多等待 120 次(约 60 秒)
var interval = 500; // 每 500ms 检查一次
var statusEl = null;
var retryBtn = null;
function showStatus(msg, isError) {
if (!statusEl) {
statusEl = document.createElement('p');
statusEl.className = 'status';
document.querySelector('.loader').appendChild(statusEl);
}
statusEl.textContent = msg;
if (isError) {
statusEl.style.color = '#dc2626';
} else {
statusEl.style.color = '#718096';
}
if (!retryBtn) {
retryBtn = document.createElement('button');
retryBtn.className = 'retry-btn';
retryBtn.textContent = '手动重试';
retryBtn.onclick = function() {
retries = 0;
retryBtn.classList.remove('show');
showStatus('正在重新连接...', false);
setTimeout(check, 500);
};
document.querySelector('.loader').appendChild(retryBtn);
}
}
function check() {
var xhr = new XMLHttpRequest();
xhr.open('GET', 'http://127.0.0.1:8501', true);
xhr.timeout = 2000;
xhr.onload = function() {
window.location.href = 'http://127.0.0.1:8501';
};
xhr.onerror = function() {
// 使用 fetch no-cors 模式检测端口是否可达
// no-cors: 请求成功则 resolve,连接失败则 reject
var controller = new AbortController();
var timeoutId = setTimeout(function() { controller.abort(); }, 3000);
fetch(STREAMLIT_URL, { mode: 'no-cors', signal: controller.signal })
.then(function() {
clearTimeout(timeoutId);
// 服务可达 → 跳转
showStatus('服务已就绪,正在进入...');
window.location.replace(STREAMLIT_URL);
})
.catch(function(err) {
clearTimeout(timeoutId);
retries++;
if (retries < maxRetries) {
setTimeout(check, 500);
showStatus('正在启动服务... (' + retries + '/' + maxRetries + ')');
setTimeout(check, interval);
} else {
showStatus('服务启动超时。请确认 Streamlit 已在 :8501 端口运行,或点击下方按钮重试。', true);
if (retryBtn) retryBtn.classList.add('show');
}
};
xhr.ontimeout = function() {
retries++;
if (retries < maxRetries) {
setTimeout(check, 500);
});
}
};
xhr.send();
// 等待 DOM 就绪后再开始检测
function start() {
showStatus('正在启动服务...');
// 首次延迟 1.5 秒,给服务一点启动时间
setTimeout(check, 1500);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', start);
} else {
start();
}
// 等待 500ms 后开始轮询(给 streamlit 启动时间)
setTimeout(check, 500);
})();
</script>
</head>
BIN
View File
Binary file not shown.
+1 -1
View File
@@ -128,7 +128,6 @@ def save_cfg_to_file(cfg: dict) -> None:
- API Keys + 品牌信息 → .streamlit/secrets.toml
"""
import tomllib
import tomli_w # type: ignore
# ── 1. 非敏感配置 → config.json ──
config_path = Path(__file__).with_name("config.json")
@@ -203,6 +202,7 @@ def save_cfg_to_file(cfg: dict) -> None:
new_app_config["temperature"] = cfg["temperature"]
# 写出 TOML
import tomli_w
secrets_path.parent.mkdir(parents=True, exist_ok=True)
with secrets_path.open("wb") as f:
content = {"api_keys": new_api_keys, "app_config": new_app_config}
+4 -4
View File
@@ -21,7 +21,6 @@ datas = [
("config.json", "."),
("geo_data.db", "."),
(".streamlit", ".streamlit"),
("knowledge_base", "knowledge_base"),
("modules", "modules"),
("platform_sync", "platform_sync"),
]
@@ -150,9 +149,10 @@ hiddenimports = [
"watchdog",
"sqlite3",
"importlib.metadata",
"json",
"csv",
"hashlib",
"tomli_w",
# ---- Streamlit 内部依赖(动态子模块多,显式声明)----
"altair",
"pydeck",
]
# ── 排除不必要的包(减小体积) ──────────────────────────────────
+4 -4
View File
@@ -24,7 +24,7 @@ requires = ["setuptools"]
build-backend = "setuptools.build_meta"
[tool.setuptools.packages.find]
include = ["build_scripts*"]
include = ["scripts*"]
[dependency-groups]
dev = [
@@ -35,6 +35,6 @@ dev = [
package = true
[project.scripts]
python-build = "build_scripts.build:main"
tauri-run = "build_scripts.runtauri:main"
tauri-build = "build_scripts.build:build_tauri"
python-build = "scripts.build:main"
tauri-run = "scripts.runtauri:main"
tauri-build = "scripts.build:build_tauri"
View File
+19
View File
@@ -0,0 +1,19 @@
{
"permissions": {
"allow": [
"Bash(cargo clean *)",
"Bash(cargo build *)",
"Bash(awk '{print $NF}')",
"Bash(xxd \"D:/MyCode/ChouJuGEO/build_scripts/tauri.py\")",
"Bash(cargo tauri *)",
"Bash(cargo install *)",
"Read(//c/Users/Administrator/AppData/Local/tauri/WixTools314/**)",
"Bash(powershell *)",
"Bash(dism /online /get-featureinfo /featurename:NetFx3)",
"Bash(reg query *)",
"Bash(cmd *)",
"Bash(\"C:/Users/Administrator/AppData/Local/tauri/WixTools314/light.exe\")",
"Bash(C:/Users/Administrator/AppData/Local/tauri/WixTools314/light.exe *)"
]
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<background android:drawable="@color/ic_launcher_background"/>
</adaptive-icon>
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#fff</color>
</resources>
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 953 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

+182 -67
View File
@@ -2,7 +2,30 @@ use std::process::{Child, Command, Stdio};
use std::sync::Mutex;
use tauri::Manager;
struct StreamlitProcess(Mutex<Option<Child>>);
/// 包装 Child,在 Drop 时强制终止整个进程树(防止 panic/crash 时泄漏)
struct ProcessGuard {
child: Option<Child>,
}
impl ProcessGuard {
fn new(child: Child) -> Self {
Self { child: Some(child) }
}
fn take(&mut self) -> Option<Child> {
self.child.take()
}
}
impl Drop for ProcessGuard {
fn drop(&mut self) {
if let Some(ref mut child) = self.child.take() {
kill_process_tree(child);
}
}
}
struct StreamlitProcess(Mutex<Option<ProcessGuard>>);
/// 等待指定端口可连接,最多等待 timeout_secs 秒
fn wait_for_port(host: &str, port: u16, timeout_secs: u64) -> bool {
@@ -16,31 +39,135 @@ fn wait_for_port(host: &str, port: u16, timeout_secs: u64) -> bool {
false
}
/// 停止 Streamlit 子进程
/// 终止整个进程树(跨平台)
///
/// - Unix: 先 SIGTERM(优雅退出),等待 3 秒无响应后 SIGKILL
/// - Windows: 直接 TerminateProcess
fn kill_streamlit(child: &mut Child) {
/// - Windows: taskkill /F /T → 终止目标进程及其所有子进程
/// - Unix: SIGTERM → 等待 3s → SIGKILL
fn kill_process_tree(child: &mut Child) {
#[cfg(windows)]
{
let pid = child.id();
// taskkill /T 递归终止整个进程树,确保 Streamlit worker 等孙进程也被清理
match Command::new("taskkill")
.args(["/F", "/T", "/PID", &pid.to_string()])
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
{
Ok(mut tk) => {
let _ = tk.wait();
log::info!("[清理] taskkill /T /PID {} 完成", pid);
}
Err(e) => {
log::warn!("[清理] taskkill 调用失败: {},回退到直接 kill", e);
let _ = child.kill();
}
}
let _ = child.wait();
log::info!("[清理] Streamlit 进程树已终止 (Windows, PID: {})", pid);
}
#[cfg(unix)]
{
// 先发 SIGTERM 请求优雅退出
let _ = Command::new("kill")
.arg("-TERM")
.arg(child.id().to_string())
.spawn();
let pid = child.id();
unsafe {
extern "C" {
fn kill(pid: i32, sig: i32) -> i32;
}
kill(pid as i32, 15); // SIGTERM
}
std::thread::sleep(std::time::Duration::from_secs(3));
// 尝试 wait(如果已退出则返回 Ok(Some(status))
if let Ok(Some(_)) = child.try_wait() {
log::info!("Streamlit 进程已优雅退出");
log::info!("[清理] Streamlit 进程已优雅退出 (SIGTERM, PID: {})", pid);
return;
}
log::warn!("Streamlit 进程未响应 SIGTERM,强制终止...");
}
log::warn!("[清理] Streamlit 进程未响应 SIGTERM,强制终止...");
// 强制终止
let _ = child.kill();
let _ = child.wait();
log::info!("[清理] Streamlit 进程已强制终止 (Unix, PID: {})", pid);
}
#[cfg(not(any(windows, unix)))]
{
let _ = child.kill();
let _ = child.wait();
}
}
/// 从 StreamlitProcess 状态中取出进程并终止
fn cleanup_streamlit(state: &StreamlitProcess) {
if let Ok(mut guard) = state.0.lock() {
if let Some(ref mut pg) = *guard {
if let Some(ref mut child) = pg.take() {
kill_process_tree(child);
}
}
// 清除空壳,避免 Drop 时二次处理
*guard = None;
}
}
/// 在 debug 模式下,尝试多种方式启动 Streamlit
fn spawn_streamlit_debug(project_root: &std::path::Path) -> Result<Child, String> {
let script_path = project_root.join("geo_tool.py");
let script = script_path.to_str().unwrap_or("geo_tool.py");
let args = [
"run",
script,
"--server.port",
"8501",
"--server.address",
"127.0.0.1",
"--server.headless",
"true",
"--browser.gatherUsageStats",
"false",
"--logger.level",
"error",
];
log::info!("[开发模式] 启动 Streamlit: {:?}", script_path);
let result = Command::new("streamlit")
.args(&args)
.current_dir(project_root)
.stdout(Stdio::null())
.stderr(Stdio::inherit())
.spawn();
if let Ok(child) = result {
log::info!("[开发模式] 通过 'streamlit' 启动成功 (PID: {})", child.id());
return Ok(child);
}
log::warn!("[开发模式] 'streamlit' 命令未找到,尝试 python -m streamlit ...");
Command::new("python")
.arg("-m")
.arg("streamlit")
.args(&args)
.current_dir(project_root)
.stdout(Stdio::null())
.stderr(Stdio::inherit())
.spawn()
.map(|child| {
log::info!("[开发模式] 通过 'python -m streamlit' 启动成功 (PID: {})", child.id());
child
})
.map_err(|e| {
let msg = format!(
"无法启动 Streamlit。已尝试:\n \
1) streamlit run ...\n \
2) python -m streamlit run ...\n \
请确认已安装 streamlit: pip install streamlit\n \
最终错误: {}",
e
);
log::error!("[开发模式] {}", msg);
msg
})
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
@@ -55,39 +182,12 @@ pub fn run() {
)?;
}
let child = if cfg!(debug_assertions) {
// ── 开发模式 ── 使用系统安装的 streamlit ──
let child_result = if cfg!(debug_assertions) {
let manifest_dir =
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let project_root = manifest_dir.parent().unwrap_or(&manifest_dir);
let script_path = project_root.join("geo_tool.py");
log::info!(
"[开发模式] 启动 Streamlit: {:?}",
script_path
);
Command::new("streamlit")
.args([
"run",
script_path.to_str().unwrap_or("geo_tool.py"),
"--server.port",
"8501",
"--server.address",
"127.0.0.1",
"--server.headless",
"true",
"--browser.gatherUsageStats",
"false",
"--logger.level",
"error",
])
.current_dir(project_root)
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
spawn_streamlit_debug(project_root)
} else {
// ── 生产模式 ── 使用 PyInstaller 打包的独立可执行文件 ──
let resource_dir = app
.path()
.resource_dir()
@@ -98,12 +198,26 @@ pub fn run() {
} else {
"geo_tool_app"
};
let exe_path = resource_dir.join("_up_").join("dist_python").join(exe_name);
log::info!(
"[生产模式] 启动打包应用: {:?}",
exe_path
let exe_path_legacy =
resource_dir.join("_up_").join("dist_python").join(exe_name);
let exe_path_direct = resource_dir.join("dist_python").join(exe_name);
let exe_path = if exe_path_legacy.exists() {
exe_path_legacy
} else if exe_path_direct.exists() {
exe_path_direct
} else {
let msg = format!(
"未找到 Streamlit 打包应用:\n 尝试1: {:?}\n 尝试2: {:?}",
exe_path_legacy, exe_path_direct
);
log::error!("[生产模式] {}", msg);
eprintln!("错误:{}", msg);
return Ok(());
};
log::info!("[生产模式] 启动打包应用: {:?}", exe_path);
let mut cmd = Command::new(&exe_path);
cmd.current_dir(&resource_dir)
@@ -111,7 +225,6 @@ pub fn run() {
.stdout(Stdio::null())
.stderr(Stdio::null());
// Windows: 不显示控制台窗口
#[cfg(target_os = "windows")]
{
use std::os::windows::process::CommandExt;
@@ -119,26 +232,25 @@ pub fn run() {
cmd.creation_flags(CREATE_NO_WINDOW);
}
cmd.spawn()
cmd.spawn().map_err(|e| {
format!("无法启动打包应用 {:?}: {}", exe_path, e)
})
};
match child {
match child_result {
Ok(child) => {
log::info!("Streamlit 进程已启动 (PID: {})", child.id());
app.manage(StreamlitProcess(Mutex::new(Some(child))));
let guard = ProcessGuard::new(child);
app.manage(StreamlitProcess(Mutex::new(Some(guard))));
}
Err(e) => {
log::error!("启动 Streamlit 失败: {}", e);
eprintln!(
"错误:无法启动 Streamlit 服务。请确保已安装 streamlit。\n {}",
e
);
eprintln!("错误:{}", e);
}
}
// 等待 Streamlit 就绪(最多 15 秒)
if !wait_for_port("127.0.0.1", 8501, 15) {
log::warn!("Streamlit 在 15 秒内未能就绪");
log::warn!("Streamlit 在 15 秒内未能就绪,请检查是否已安装 streamlit");
} else {
log::info!("Streamlit 已就绪 (127.0.0.1:8501)");
}
@@ -149,19 +261,22 @@ pub fn run() {
.expect("error while building tauri application");
app.run(|app_handle, event| {
// 窗口关闭 → 停止子进程 → 退出应用
if let tauri::RunEvent::ExitRequested { .. } = event {
match event {
tauri::RunEvent::WindowEvent { event, .. } => {
if let tauri::WindowEvent::CloseRequested { .. } = event {
log::info!("窗口关闭请求,正在清理子进程...");
if let Some(state) = app_handle.try_state::<StreamlitProcess>() {
if let Ok(mut guard) = state.0.lock() {
if let Some(ref mut child) = *guard {
log::info!("正在停止 Streamlit 进程...");
kill_streamlit(child);
log::info!("Streamlit 进程已停止");
cleanup_streamlit(&*state);
}
}
}
// 确保应用进程完全退出
std::process::exit(0);
tauri::RunEvent::Exit => {
log::info!("应用退出,执行最终清理...");
if let Some(state) = app_handle.try_state::<StreamlitProcess>() {
cleanup_streamlit(&*state);
}
}
_ => {}
}
});
}
+8 -4
View File
@@ -4,8 +4,7 @@
"version": "0.1.0",
"identifier": "com.pinesound.geo-tool",
"build": {
"frontendDist": "../frontend",
"devUrl": "http://localhost:8501"
"frontendDist": "../frontend"
},
"app": {
"windows": [
@@ -35,7 +34,12 @@
"icons/icon.ico"
],
"resources": [
"../dist_python/geo_tool_app"
]
"../dist_python/geo_tool_app.exe"
],
"windows": {
"wix": {
"language": "zh-CN"
}
}
}
}