diff --git a/README.md b/README.md index e712cf2..915c3b5 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ CapacityReport 用于导入每周容量报表数据,按 `Configure.json` 的 CapaReport/ ├─ app/ # FastAPI 后端源码 │ ├─ api/routers/ # API 路由 -│ ├─ services/ # 运行时和远程下载服务 +│ ├─ services/ # 授权、远程下载等业务服务 │ ├─ utils/ # 通用工具 │ ├─ main.py # 后端入口和前端托管 │ ├─ processor.py # 数据处理主流程 @@ -63,12 +63,11 @@ uv venv uv pip install -r requirements.txt ``` -安装并构建前端: +安装前端依赖: ```powershell cd frontend npm install -npm run build cd .. ``` @@ -91,7 +90,7 @@ cd frontend npm run dev ``` -Vite 会把 `/api` 和 `/health` 代理到 `http://localhost:9081`。 +Vite 会把 `/api` 和 `/health` 代理到 `http://localhost:9081`。如果 `frontend/dist` 不存在,`run.bat` 会自动安装前端依赖并执行构建。 ## 一键构建 @@ -148,10 +147,10 @@ scripts\build.bat desktop dist\desktop\*-setup.exe ``` -桌面版使用 Tauri 启动 Python sidecar。sidecar 默认监听 `127.0.0.1:19082`,运行数据写入系统 app data 目录,不写入安装目录。 +桌面版使用 Tauri 启动 Python sidecar。sidecar 默认监听 `127.0.0.1:9081`,运行数据写入系统 app data 目录,不写入安装目录。 首次启动会把安装包内置的 `Configure.json` 和 `ReportScript.sql` 复制到运行数据目录;Windows 下通常是 `%APPDATA%\com.nixevol.capacityreport\`。安装目录中的 `_up_` 只是 Tauri 打包资源目录,程序运行时不会直接编辑它。 Windows 桌面版使用 NSIS 安装器,卸载时会询问是否同时删除 `%APPDATA%\com.nixevol.capacityreport\` 中的配置、脚本、授权、缓存和日志。 -桌面版 release 包已启用 DevTools,如需排查前端请求,可在窗口中按 `F12` 或右键打开开发者工具。 +Windows 桌面版默认安装到 `D:\Program Files\CapacityReport`;如果没有 D 盘,则默认安装到系统 `Program Files\CapacityReport`。桌面版已关闭 release DevTools 和右键浏览器菜单。 构建桌面版需要 Rust 和 Tauri CLI。脚本会在缺少 Tauri CLI 时自动执行: @@ -185,7 +184,7 @@ docker compose -f dist\docker\docker-compose.yml up -d 访问: ```text -http://localhost:19081 +http://localhost:9081 ``` 停止: @@ -219,7 +218,7 @@ Server Portable 和桌面版需要在目标系统原生构建:Windows 包在 W 登录密码保存在本地 `auth.ini`,该文件不应提交到版本库。 -授权到期日期保存在本地加密文件 `license.dat`,默认到期日为 `2026-06-20`。处理任务不会读取系统日期,而是从任务目录 ZIP 文件名中的 `YYYYMMDDHHMM` 或 `YYYYMMDDHHMMSS` 时间戳取最大日期进行比对。 +授权到期日期保存在本地加密文件 `license.dat`,默认到期日由 `app/services/license.py` 中的 `DEFAULT_EXPIRES_ON` 控制,当前为 `2026-06-20`。处理任务不会读取系统日期,而是从任务目录 ZIP 文件名中的 `YYYYMMDDHHMM` 或 `YYYYMMDDHHMMSS` 时间戳取最大日期进行比对。登录后连续点击左上角品牌图标 8 次,可主动打开授权延期窗口。 ## 常用接口 @@ -234,7 +233,6 @@ Server Portable 和桌面版需要在目标系统原生构建:Windows 包在 W - `POST /api/license/activate`:提交激活码并顺延授权期限 - `GET /api/history`:处理历史 - `POST /api/history/download`:下载历史原始数据 -- `POST /api/service/restart`:重启服务 - `GET /health`:健康检查 ## 维护注意事项 diff --git a/app/api/routers/health.py b/app/api/routers/health.py index 7211ae0..446e181 100644 --- a/app/api/routers/health.py +++ b/app/api/routers/health.py @@ -32,7 +32,7 @@ async def health_check(): return { "status": "healthy" if is_healthy else "unhealthy", "timestamp": datetime.now().isoformat(), - "version": "2.0.3", + "version": "3.0.0", "uptime_pid": os.getpid(), "checks": checks, } diff --git a/app/api/routers/service.py b/app/api/routers/service.py deleted file mode 100644 index 847c1fd..0000000 --- a/app/api/routers/service.py +++ /dev/null @@ -1,68 +0,0 @@ -import os -import platform -import time -from threading import Thread - -from fastapi import APIRouter - -from app.services.runtime import ( - is_container_runtime, - is_supervisor_running, - restart_method_name, - restart_via_supervisor, - terminate_current_process, -) - - -router = APIRouter(tags=["service"]) - - -@router.post("/api/service/restart") -async def restart_service(): - supervisor_running = is_supervisor_running() - container_runtime = is_container_runtime() - - if supervisor_running: - success, message = restart_via_supervisor() - if success: - return { - "success": True, - "message": message, - "method": "supervisor", - "container": container_runtime, - } - - method = restart_method_name(supervisor_running) - thread = Thread(target=_delayed_exit, daemon=True) - thread.start() - - message = "服务正在重启,请稍后刷新页面..." - if supervisor_running: - message = "supervisor 命令不可用,已切换为进程重启..." - elif container_runtime: - message = "服务进程正在退出,容器或进程管理器将拉起服务..." - - return { - "success": True, - "message": message, - "method": method, - "container": container_runtime, - } - - -@router.get("/api/service/status") -async def get_service_status(): - return { - "status": "running", - "version": "2.0.3", - "platform": platform.system(), - "supervisor": is_supervisor_running(), - "container": is_container_runtime(), - "pid": os.getpid(), - "python_version": platform.python_version(), - } - - -def _delayed_exit() -> None: - time.sleep(1) - terminate_current_process() diff --git a/app/main.py b/app/main.py index ce3b5a7..293eb1e 100644 --- a/app/main.py +++ b/app/main.py @@ -7,12 +7,12 @@ from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import FileResponse, JSONResponse from app import state -from app.api.routers import auth, cache, config, database, health, history, license, remote, script, service, tasks, upload +from app.api.routers import auth, cache, config, database, health, history, license, remote, script, tasks, upload from app.auth import verify_jwt_token from app.config import BASE_DIR -APP_VERSION = "2.0.3" +APP_VERSION = "3.0.0" APP_HOST = "0.0.0.0" APP_PORT = 9081 FRONTEND_DIST_DIR = BASE_DIR / "frontend" / "dist" @@ -71,7 +71,6 @@ def register_routes(app: FastAPI) -> None: database.router, config.router, cache.router, - service.router, script.router, ] for router in routers: diff --git a/app/services/runtime.py b/app/services/runtime.py deleted file mode 100644 index b04bee0..0000000 --- a/app/services/runtime.py +++ /dev/null @@ -1,88 +0,0 @@ -import os -import platform -import signal -import subprocess -from pathlib import Path - - -def is_supervisor_running() -> bool: - if os.environ.get("SUPERVISOR_ENABLED") == "1": - return True - - supervisor_sock = Path("/var/run/supervisor.sock") - if supervisor_sock.exists() and _command_succeeds(["supervisorctl", "status"], timeout=5): - return True - - try: - import psutil # pyright: ignore[reportMissingModuleSource] - - parent = psutil.Process().parent() - if parent and "supervisor" in parent.name().lower(): - return True - except Exception: - pass - - return _command_succeeds(["pgrep", "-f", "supervisord"], timeout=2) - - -def is_container_runtime() -> bool: - if os.environ.get("container"): - return True - if Path("/.dockerenv").exists(): - return True - - cgroup_path = Path("/proc/1/cgroup") - if not cgroup_path.exists(): - return False - - try: - cgroup = cgroup_path.read_text(encoding="utf-8", errors="ignore").lower() - except Exception: - return False - - return any(marker in cgroup for marker in ("docker", "kubepods", "containerd")) - - -def restart_via_supervisor() -> tuple[bool, str]: - try: - result = subprocess.run( - ["supervisorctl", "restart", "fastapi"], - capture_output=True, - text=True, - timeout=30, - ) - if result.returncode == 0: - return True, "服务正在通过 supervisor 重启..." - - error_msg = result.stderr or result.stdout or "未知错误" - return False, f"重启失败: {error_msg}" - except subprocess.TimeoutExpired: - return False, "重启操作超时" - except FileNotFoundError: - return False, "找不到 supervisorctl 命令" - except Exception as exc: - return False, f"重启异常: {exc}" - - -def terminate_current_process() -> None: - if platform.system() == "Windows": - os._exit(0) - os.kill(os.getpid(), signal.SIGTERM) - - -def restart_method_name(supervisor_running: bool) -> str: - if supervisor_running: - return "supervisor" - if platform.system() == "Windows": - return "windows-process" - if is_container_runtime(): - return "container-process" - return "process" - - -def _command_succeeds(command: list[str], timeout: int) -> bool: - try: - result = subprocess.run(command, capture_output=True, text=True, timeout=timeout) - return result.returncode == 0 and bool(result.stdout.strip() or command[0] == "supervisorctl") - except Exception: - return False diff --git a/docs/project_context.md b/docs/project_context.md index 35cf88a..4e4cd34 100644 --- a/docs/project_context.md +++ b/docs/project_context.md @@ -1,4 +1,15 @@ # 项目上下文记录 +## 2026-05-20:统一端口、升级 3.0.0 并收敛桌面安装行为 + +- 应用户要求,应用访问端口统一回 `9081`:Server Portable、Docker 宿主机映射、Tauri 桌面 sidecar、桌面前端 `VITE_API_BASE` 和前端 Tauri 兜底 API 地址均使用 `http://127.0.0.1:9081`;桌面版启动前只会清理同名 `capareport-server.exe` 的残留监听进程,避免误杀其它占用 `9081` 的程序。 +- 版本统一提升为 `3.0.0`,同步更新后端 `APP_VERSION`、健康检查、侧边栏显示、Tauri 配置和 Cargo 包版本。 +- `run.bat` 不再因 `frontend/dist/index.html` 缺失直接退出;缺少前端构建产物时会检查 `npm`、按需执行 `npm ci`,然后自动运行 `npm run build` 再启动后端。 +- 桌面版去除 release DevTools:`src-tauri/Cargo.toml` 移除 Tauri `devtools` feature,`tauri.conf.json` 移除窗口 `devtools` 配置,前端在 Tauri 环境下阻止右键浏览器菜单和 `F12`/`Ctrl+Shift+I`。 +- Windows NSIS 安装器改为 per-machine,并在未选择自定义安装目录时默认落到 `D:\Program Files\CapacityReport`;如果没有 D 盘,则使用系统 `Program Files\CapacityReport`。`scripts/build.ps1` 构建桌面版时会临时生成 Tauri NSIS 模板并恢复 `tauri.conf.json`,避免旧安装记录把默认路径带回 C 盘。默认授权到期日仍由 `app/services/license.py` 的 `DEFAULT_EXPIRES_ON` 控制。 +- 桌面端不再提供服务重启功能:前端移除重启按钮和等待遮罩,后端删除 `/api/service/restart`、`/api/service/status` 以及对应 runtime 重启实现,避免桌面 sidecar 无法可靠自重启时误导用户。 +- 登录后连续点击左上角品牌图标 8 次会主动打开授权延期窗口,窗口显示当前激活 key 标签并允许连续提交激活码,每次成功后按新的到期日刷新下一次 key。 +- 已验证 `cmd /c scripts\build.bat desktop` 可生成 `dist\desktop\CapacityReport_3.0.0_x64-setup.exe`;静默安装后 `capacity-report-desktop.exe`、`capareport-server.exe`、`Configure.json` 和 `ReportScript.sql` 均位于 `D:\Program Files\CapacityReport`,注册表 `InstallLocation` 指向 D 盘。已启动安装后的桌面程序验证 sidecar `/health` 返回 `3.0.0`,并验证配置、脚本和授权接口可读取;验证后已停止测试进程并清理中间产物。 + ## 2026-05-20:修复桌面版跨源预检导致配置网络错误 - 桌面版前端访问 `127.0.0.1:19082` 属于 WebView 跨源请求,带 `Authorization` 或上传配置文件时浏览器会先发 `OPTIONS` 预检;`app/main.py` 的 JWT 中间件现在直接放行 `OPTIONS`,让 FastAPI CORS 中间件返回允许头,避免配置读取和配置上传显示“网络错误”。 @@ -381,12 +392,10 @@ - `database.py`:数据库测试、表查询、表维护和导出。 - `config.py`:配置读取、保存、上传和下载。 - `cache.py`:缓存大小统计。 - - `service.py`:服务状态和重启。 - `script.py`:SQL 脚本读取、保存和执行。 - `health.py`:健康检查。 - 运行时共享状态放在 `app/state.py`,包括配置实例、历史管理器、处理任务、上传会话和全局任务锁。 - 登录、密码文件和 Token 逻辑放在 `app/auth.py`,继续使用本地 `auth.ini`。 -- 服务重启检测和进程退出逻辑放在 `app/services/runtime.py`。 - 文件大小等工具函数放在 `app/utils/files.py`。 ### 前端 diff --git a/frontend/src/AppShell.vue b/frontend/src/AppShell.vue index aed4f2d..1fe2f49 100644 --- a/frontend/src/AppShell.vue +++ b/frontend/src/AppShell.vue @@ -11,7 +11,17 @@ @update:collapsed="handleSidebarCollapsed" >
-
📊
+
+ 📊 +
CapacityReport
版本: - v2.0.3 + v3.0.0 Power by:NIXEVOL @@ -88,15 +98,6 @@ -