diff --git a/README.md b/README.md index 71ab196..538b4a1 100644 --- a/README.md +++ b/README.md @@ -150,6 +150,7 @@ dist\desktop\*-setup.exe ``` 桌面版使用 Tauri 启动 Python sidecar。sidecar 默认监听 `127.0.0.1:19082`,运行数据写入系统 app data 目录,不写入安装目录。 +首次启动会把安装包内置的 `Configure.json` 和 `ReportScript.sql` 复制到运行数据目录;Windows 下通常是 `%APPDATA%\com.nixevol.capacityreport\`。安装目录中的 `_up_` 只是 Tauri 打包资源目录,程序运行时不会直接编辑它。 构建桌面版需要 Rust 和 Tauri CLI。脚本会在缺少 Tauri CLI 时自动执行: diff --git a/docs/project_context.md b/docs/project_context.md index be51b1f..9a60395 100644 --- a/docs/project_context.md +++ b/docs/project_context.md @@ -1,4 +1,12 @@ # 项目上下文记录 +## 2026-05-20:修复桌面版启动期配置和脚本加载竞态 + +- 桌面版运行配置和脚本仍从安装包资源 `Configure.json`、`ReportScript.sql` 首次复制到系统 AppData 后读取;安装目录中的 `_up_` 是 Tauri 对 `../` 资源的打包目录,不是后端实际运行目录。 +- `src-tauri/src/main.rs` 在启动 Python sidecar 后会等待 `127.0.0.1:19082` 可连接,最多等待 20 秒;如果端口没有起来,会主动杀掉刚启动的 sidecar 并让启动失败,避免前端先加载导致配置页停在默认空表单、脚本页停在“正在加载”。 +- `frontend/src/api/client.ts` 对普通 `fetch` 请求增加短暂重试,处理桌面 sidecar 启动或服务重启瞬间的 `Failed to fetch`;上传 XHR 不做自动重试,避免重复上传。 +- 已实测当前安装目录 `D:\Program Files\CapacityReport\_up_` 和运行目录 `%APPDATA%\com.nixevol.capacityreport` 均存在配置与脚本,`http://127.0.0.1:19082/health`、`/api/config/full`、`/api/script/content` 均能读取;本次修复的是前端初始请求早于 sidecar 就绪的竞态。 +- 已执行 `.venv\Scripts\python.exe -m compileall app`、`npm run build` 和带临时 sidecar 占位文件的 `cargo check --manifest-path src-tauri\Cargo.toml`,均通过;生成产物随后清理。 + ## 2026-05-20:修复登录失败误提示会话过期 - `frontend/src/api/client.ts` 不再把 `/api/login` 的 401 响应当作全局会话过期处理,登录失败会按后端真实错误显示“账号或密码错误”。 diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 1a80a6d..bd07348 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -2,6 +2,8 @@ import type { ApiError, ApiErrorDetail } from '../types'; const TOKEN_KEY = 'capacity_report_token'; const API_BASE = (import.meta.env.VITE_API_BASE || '').replace(/\/$/, ''); +const API_FETCH_RETRIES = 8; +const API_FETCH_RETRY_DELAY_MS = 500; let onUnauthorized: (() => void) | null = null; export class ApiRequestError extends Error { @@ -55,7 +57,7 @@ export async function request(url: string, init: RequestInit = {}): Promise { + let lastError: unknown; + for (let attempt = 0; attempt <= API_FETCH_RETRIES; attempt += 1) { + try { + return await fetch(url, init); + } catch (error) { + lastError = error; + if (attempt === API_FETCH_RETRIES) { + break; + } + await sleep(API_FETCH_RETRY_DELAY_MS); + } + } + + throw lastError instanceof Error ? lastError : new Error('网络请求失败'); +} + +function sleep(ms: number): Promise { + return new Promise(resolve => window.setTimeout(resolve, ms)); +} + function parseFilename(disposition: string | null): string { if (!disposition) return ''; diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index db42283..4cfef84 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -2,8 +2,11 @@ use std::error::Error; use std::fs; +use std::net::{SocketAddr, TcpStream}; use std::path::{Path, PathBuf}; use std::sync::Mutex; +use std::thread; +use std::time::{Duration, Instant}; use tauri::{Manager, Runtime}; use tauri_plugin_shell::process::CommandChild; @@ -77,6 +80,11 @@ fn start_server(app: &tauri::App) -> Result<(), Box> { .args(["--host", "127.0.0.1", "--port", "19082"]) .spawn()?; + if let Err(error) = wait_for_server("127.0.0.1:19082", Duration::from_secs(20)) { + let _ = child.kill(); + return Err(error); + } + tauri::async_runtime::spawn(async move { while rx.recv().await.is_some() {} }); let pid = child.pid(); @@ -85,6 +93,18 @@ fn start_server(app: &tauri::App) -> Result<(), Box> { Ok(()) } +fn wait_for_server(addr: &str, timeout: Duration) -> Result<(), Box> { + let socket_addr: SocketAddr = addr.parse()?; + let started_at = Instant::now(); + while started_at.elapsed() < timeout { + if TcpStream::connect_timeout(&socket_addr, Duration::from_millis(250)).is_ok() { + return Ok(()); + } + thread::sleep(Duration::from_millis(250)); + } + Err(format!("Server did not start within {} seconds", timeout.as_secs()).into()) +} + fn stop_server(app: &tauri::AppHandle) { let state = app.state::(); let child = state.0.lock().expect("server state lock poisoned").take();