From 151fa7cc492abe37ca915e3ea12fa608918323e0 Mon Sep 17 00:00:00 2001 From: cnlimiter Date: Sun, 15 Mar 2026 20:54:52 +0800 Subject: [PATCH] =?UTF-8?q?feat(core):=20=E6=94=AF=E6=8C=81PyInstaller?= =?UTF-8?q?=E6=89=93=E5=8C=85=E5=B9=B6=E4=BC=98=E5=8C=96=E8=B5=84=E6=BA=90?= =?UTF-8?q?=E8=B7=AF=E5=BE=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修改app.py以支持PyInstaller打包后的资源路径 - 更新session.py以支持APP_DATA_DIR环境变量 - 增强webui.py以设置打包后的数据目录 - 添加pyproject.toml的PyInstaller依赖组 - 新增构建脚本和GitHub Actions工作流 --- .github/workflows/build.yml | 105 ++++++++++++++++++++++++++++++++++++ build.bat | 20 +++++++ build.sh | 49 +++++++++++++++++ pyproject.toml | 6 ++- src/database/session.py | 10 ++-- src/web/app.py | 11 ++-- webui.py | 35 +++++++----- 7 files changed, 215 insertions(+), 21 deletions(-) create mode 100644 .github/workflows/build.yml create mode 100644 build.bat create mode 100644 build.sh diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..4bd80d3 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,105 @@ +name: 多平台打包发布 + +on: + push: + tags: + - 'v*' + workflow_dispatch: + inputs: + version: + description: '版本号 (如 v1.0.0)' + required: false + default: 'dev' + +jobs: + build: + name: 打包 ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - os: windows-latest + artifact_name: codex-register.exe + asset_name: codex-register-windows-x64.exe + - os: ubuntu-latest + artifact_name: codex-register + asset_name: codex-register-linux-x64 + - os: macos-latest + artifact_name: codex-register + asset_name: codex-register-macos-arm64 + + steps: + - name: 检出代码 + uses: actions/checkout@v4 + + - name: 设置 Python 3.11 + uses: actions/setup-python@v5 + with: + python-version: '3.11' + cache: 'pip' + + - name: 安装依赖 + run: | + pip install -r requirements.txt pyinstaller + + - name: 打包 + run: | + pyinstaller codex_register.spec --clean --noconfirm + + - name: 上传构建产物 + uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.asset_name }} + path: dist/${{ matrix.artifact_name }} + if-no-files-found: error + + release: + name: 创建发布 + needs: build + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/') + permissions: + contents: write + + steps: + - name: 下载所有构建产物 + uses: actions/download-artifact@v4 + with: + path: dist/ + + - name: 整理文件 + run: | + mkdir -p release + find dist/ -type f | while read f; do + name=$(basename "$f") + cp "$f" "release/$name" + done + ls -lh release/ + + - name: 创建 GitHub Release + uses: softprops/action-gh-release@v2 + with: + files: release/* + generate_release_notes: true + body: | + ## OpenAI/Codex CLI 自动注册系统 + + ### 下载说明 + | 平台 | 文件 | + |------|------| + | Windows x64 | `codex-register-windows-x64.exe` | + | Linux x64 | `codex-register-linux-x64` | + | macOS ARM64 | `codex-register-macos-arm64` | + + ### 使用方法 + ```bash + # Linux/macOS 需要先赋予执行权限 + chmod +x codex-register-* + + # 启动 Web UI + ./codex-register + + # 指定端口 + ./codex-register --port 8080 + ``` diff --git a/build.bat b/build.bat new file mode 100644 index 0000000..9222f61 --- /dev/null +++ b/build.bat @@ -0,0 +1,20 @@ +@echo off +REM Windows 打包脚本 + +echo === 构建平台: Windows === + +REM 安装打包依赖 +pip install pyinstaller --quiet + +REM 执行打包 +pyinstaller codex_register.spec --clean --noconfirm + +IF EXIST dist\codex-register.exe ( + FOR /F "tokens=*" %%i IN ('powershell -Command "[System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture"') DO SET ARCH=%%i + SET OUTPUT=dist\codex-register-windows-%ARCH%.exe + MOVE dist\codex-register.exe "%OUTPUT%" + echo === 构建完成: %OUTPUT% === +) ELSE ( + echo === 构建失败,未找到输出文件 === + exit /b 1 +) diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..61be33c --- /dev/null +++ b/build.sh @@ -0,0 +1,49 @@ +#!/bin/bash +# 跨平台打包脚本(在各平台上分别运行) + +set -e + +OS=$(uname -s) +ARCH=$(uname -m) + +case "$OS" in + Darwin) + PLATFORM="macos" + EXT="" + ;; + Linux) + PLATFORM="linux" + EXT="" + ;; + MINGW*|CYGWIN*|MSYS*) + PLATFORM="windows" + EXT=".exe" + ;; + *) + PLATFORM="$OS" + EXT="" + ;; +esac + +OUTPUT_NAME="codex-register-${PLATFORM}-${ARCH}${EXT}" + +echo "=== 构建平台: ${PLATFORM} (${ARCH}) ===" +echo "=== 输出文件: dist/${OUTPUT_NAME} ===" + +# 安装打包依赖 +pip install pyinstaller --quiet 2>/dev/null || \ + uv run --with pyinstaller pyinstaller --version > /dev/null 2>&1 + +# 执行打包(优先用 uv,回退到直接调用) +if command -v uv &>/dev/null; then + uv run --with pyinstaller pyinstaller codex_register.spec --clean --noconfirm +else + pyinstaller codex_register.spec --clean --noconfirm +fi + +# 重命名输出文件 +mv dist/codex-register${EXT} dist/${OUTPUT_NAME} 2>/dev/null || \ + mv "dist/codex-register" "dist/${OUTPUT_NAME}" 2>/dev/null || true + +echo "=== 构建完成: dist/${OUTPUT_NAME} ===" +ls -lh dist/${OUTPUT_NAME} diff --git a/pyproject.toml b/pyproject.toml index 84c70b4..5ec14c9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,6 @@ dev = [ ] [project.scripts] -codex-register = "cli:main" codex-webui = "webui:main" [build-system] @@ -31,3 +30,8 @@ build-backend = "hatchling.build" [tool.hatch.build.targets.wheel] packages = ["src"] + +[dependency-groups] +dev = [ + "pyinstaller>=6.19.0", +] diff --git a/src/database/session.py b/src/database/session.py index 97b76af..5ea6d31 100644 --- a/src/database/session.py +++ b/src/database/session.py @@ -20,14 +20,14 @@ class DatabaseSessionManager: def __init__(self, database_url: str = None): if database_url is None: - # 默认使用项目根目录下的 SQLite 数据库 - db_path = os.path.join( + # 优先使用 APP_DATA_DIR 环境变量(PyInstaller 打包后由 webui.py 设置) + data_dir = os.environ.get('APP_DATA_DIR') or os.path.join( os.path.dirname(os.path.dirname(os.path.dirname(__file__))), - 'data', - 'database.db' + 'data' ) + db_path = os.path.join(data_dir, 'database.db') # 确保目录存在 - os.makedirs(os.path.dirname(db_path), exist_ok=True) + os.makedirs(data_dir, exist_ok=True) database_url = f"sqlite:///{db_path}" self.database_url = database_url diff --git a/src/web/app.py b/src/web/app.py index bfed89e..350a1fd 100644 --- a/src/web/app.py +++ b/src/web/app.py @@ -4,6 +4,7 @@ FastAPI 应用主文件 """ import logging +import sys from pathlib import Path from typing import Optional @@ -21,11 +22,15 @@ from .task_manager import task_manager logger = logging.getLogger(__name__) # 获取项目根目录 -PROJECT_ROOT = Path(__file__).parent.parent.parent +# PyInstaller 打包后静态资源在 sys._MEIPASS,开发时在源码根目录 +if getattr(sys, 'frozen', False): + _RESOURCE_ROOT = Path(sys._MEIPASS) +else: + _RESOURCE_ROOT = Path(__file__).parent.parent.parent # 静态文件和模板目录 -STATIC_DIR = PROJECT_ROOT / "static" -TEMPLATES_DIR = PROJECT_ROOT / "templates" +STATIC_DIR = _RESOURCE_ROOT / "static" +TEMPLATES_DIR = _RESOURCE_ROOT / "templates" def create_app() -> FastAPI: diff --git a/webui.py b/webui.py index 5335b8d..4f4b2a4 100644 --- a/webui.py +++ b/webui.py @@ -8,8 +8,16 @@ import sys from pathlib import Path # 添加项目根目录到 Python 路径 -project_root = Path(__file__).parent -sys.path.insert(0, str(project_root)) +# PyInstaller 打包后 __file__ 在临时解压目录,需要用 sys.executable 所在目录作为数据目录 +import os +if getattr(sys, 'frozen', False): + # 打包后:使用可执行文件所在目录 + project_root = Path(sys.executable).parent + _src_root = Path(sys._MEIPASS) +else: + project_root = Path(__file__).parent + _src_root = project_root +sys.path.insert(0, str(_src_root)) from src.core.utils import setup_logging from src.database.init_db import initialize_database @@ -18,6 +26,16 @@ from src.config.settings import get_settings def setup_application(): """设置应用程序""" + # 确保数据目录和日志目录在可执行文件所在目录(打包后也适用) + data_dir = project_root / "data" + logs_dir = project_root / "logs" + data_dir.mkdir(exist_ok=True) + logs_dir.mkdir(exist_ok=True) + + # 将数据目录路径注入环境变量,供数据库配置使用 + os.environ.setdefault("APP_DATA_DIR", str(data_dir)) + os.environ.setdefault("APP_LOGS_DIR", str(logs_dir)) + # 初始化数据库(必须先于获取设置) try: initialize_database() @@ -28,23 +46,16 @@ def setup_application(): # 获取配置(需要数据库已初始化) settings = get_settings() - # 配置日志 + # 配置日志(日志文件写到实际 logs 目录) + log_file = str(logs_dir / Path(settings.log_file).name) setup_logging( log_level=settings.log_level, - log_file=settings.log_file + log_file=log_file ) logger = logging.getLogger(__name__) logger.info("数据库初始化完成") - - # 检查数据目录 - data_dir = project_root / "data" - data_dir.mkdir(exist_ok=True) logger.info(f"数据目录: {data_dir}") - - # 检查日志目录 - logs_dir = project_root / "logs" - logs_dir.mkdir(exist_ok=True) logger.info(f"日志目录: {logs_dir}") logger.info("应用程序设置完成")