feat(core): 支持PyInstaller打包并优化资源路径

- 修改app.py以支持PyInstaller打包后的资源路径
- 更新session.py以支持APP_DATA_DIR环境变量
- 增强webui.py以设置打包后的数据目录
- 添加pyproject.toml的PyInstaller依赖组
- 新增构建脚本和GitHub Actions工作流
This commit is contained in:
cnlimiter
2026-03-15 20:54:52 +08:00
parent 45503102a6
commit 151fa7cc49
7 changed files with 215 additions and 21 deletions

105
.github/workflows/build.yml vendored Normal file
View File

@@ -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
```

20
build.bat Normal file
View File

@@ -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
)

49
build.sh Normal file
View File

@@ -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}

View File

@@ -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",
]

View File

@@ -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

View File

@@ -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:

View File

@@ -8,8 +8,16 @@ import sys
from pathlib import Path
# 添加项目根目录到 Python 路径
# 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
sys.path.insert(0, str(project_root))
_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("应用程序设置完成")