Skip to content

Commit 1f4e897

Browse files
committed
refactor: 重构脚本目录结构,添加 PyInstaller 打包支持
1 parent 9d221bd commit 1f4e897

File tree

15 files changed

+1255
-44
lines changed

15 files changed

+1255
-44
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ __pycache__/
1313
# Distribution / packaging
1414
.Python
1515
build/
16+
!scripts/build/
1617
develop-eggs/
1718
dist/
1819
downloads/
@@ -35,7 +36,7 @@ MANIFEST
3536
# Usually these files are written by a python script from a template
3637
# before PyInstaller builds the exe, so as to inject date/other infos into it.
3738
*.manifest
38-
*.spec
39+
# *.spec # 注意:dicepp.spec 需要追踪,所以不忽略 .spec 文件
3940

4041
# Installer logs
4142
pip-log.txt

README.md

Lines changed: 67 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -104,32 +104,84 @@ docker-compose up -d
104104
105105
## 开发者
106106

107-
### 安装测试依赖
107+
### 安装开发依赖
108108

109109
```bash
110-
pip install pytest pytest-asyncio pytest-cov
111-
# 或使用 uv
112-
uv sync --extra dev
110+
# 推荐:使用 uv 安装开发依赖
111+
uv sync --dev
112+
113+
# 这会安装 pytest、pyinstaller 等开发工具
113114
```
114115

115-
### 运行测试
116+
### 开发脚本
117+
118+
项目提供了完整的开发、测试、构建脚本:
119+
120+
```
121+
scripts/
122+
├── build/ # 构建相关
123+
│ ├── build.bat # 构建打包脚本
124+
│ └── dicepp.spec # PyInstaller 打包配置
125+
├── dev/ # 开发环境脚本
126+
│ ├── install.bat # 安装开发依赖
127+
│ └── run.bat # 启动开发服务器
128+
├── test/ # 测试脚本
129+
│ ├── run_unit_test.bat # 单元测试
130+
│ ├── run_integration_test.bat # 集成测试
131+
│ ├── run_build_test.bat # 构建验证
132+
│ └── test_bot.py # 集成测试核心
133+
├── deploy/ # 部署脚本
134+
│ ├── linux/ # Linux 部署 (start/stop/restart/logs.sh)
135+
│ └── windows/ # Windows 部署
136+
└── migrate/ # 数据迁移工具
137+
```
138+
139+
**常用命令:**
140+
141+
| 用途 | 命令 |
142+
|------|------|
143+
| 开发运行 | `scripts\dev\run.bat` |
144+
| 单元测试 | `scripts\test\run_unit_test.bat` |
145+
| 集成测试 | `scripts\test\run_integration_test.bat` |
146+
| 打包构建 | `scripts\build\build.bat` |
147+
| 构建验证 | `scripts\test\run_build_test.bat` |
148+
149+
### 构建独立 EXE
150+
151+
项目支持打包为独立的 Windows EXE,用户无需安装 Python 即可运行:
116152

117153
```bash
118-
# 运行所有测试
119-
pytest
154+
# 运行构建脚本
155+
scripts\build\build.bat
156+
157+
# 验证构建
158+
scripts\test\run_build_test.bat
159+
```
120160

121-
# 运行测试并显示覆盖率
122-
pytest --cov
161+
构建完成后,产物位于 `dist\DicePP\` 目录:
123162

124-
# 运行特定目录的测试
125-
pytest src/plugins/DicePP
126163
```
164+
dist/DicePP/
165+
├── DicePP.exe # 主程序
166+
├── .env # 配置文件(用户可编辑)
167+
├── Data/ # 数据目录(用户数据存储位置)
168+
└── _internal/ # 内部依赖(无需修改)
169+
```
170+
171+
### 运行测试
127172

128-
### 测试文件命名规范
173+
```bash
174+
# 单元测试
175+
scripts\test\run_unit_test.bat
176+
# 或直接使用 pytest
177+
pytest
129178

130-
- 文件名: `unit_test.py`, `test_*.py`, `*_test.py`
131-
- 类名: `MyTestCase`, `Test*`
132-
- 函数名: `test*`
179+
# 集成测试(需要先启动 Bot)
180+
scripts\test\run_integration_test.bat
181+
182+
# 交互模式测试
183+
scripts\test\run_integration_test.bat -i
184+
```
133185

134186
## 交流群
135187

bot.py

Lines changed: 57 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,45 +2,80 @@
22
# -*- coding: utf-8 -*-
33
import os
44
import sys
5+
6+
# ============================================================
7+
# 打包环境适配:确保 EXE 运行时工作目录和路径正确
8+
# ============================================================
9+
_IS_FROZEN = getattr(sys, 'frozen', False)
10+
_INTERNAL_DIR = None
11+
12+
if _IS_FROZEN:
13+
# PyInstaller 打包环境
14+
# 1. 将工作目录切换到 EXE 所在位置,确保 .env 和 Data 目录可被正确访问
15+
exe_dir = os.path.dirname(sys.executable)
16+
os.chdir(exe_dir)
17+
# 2. 记录 _internal 目录路径(pyproject.toml 和 DicePP 插件在这里)
18+
_INTERNAL_DIR = os.path.join(exe_dir, '_internal')
19+
if _INTERNAL_DIR not in sys.path:
20+
sys.path.insert(0, _INTERNAL_DIR)
21+
# 3. 把 src/plugins 加入 sys.path,让 NoneBot 能正确导入插件
22+
_plugins_base = os.path.join(_INTERNAL_DIR, 'src', 'plugins')
23+
if _plugins_base not in sys.path:
24+
sys.path.insert(0, _plugins_base)
25+
else:
26+
# 开发环境:保持原有行为
27+
dir_path = os.path.abspath(os.path.dirname(__file__))
28+
sys.path.insert(0, dir_path)
29+
530
import nonebot
631
from nonebot.log import logger, default_format
732
from nonebot.adapters.onebot.v11 import Adapter as OneBot_V11_Adapter
833

9-
# Fix import path problem in server
10-
dir_path = os.path.abspath(os.path.dirname(__file__))
11-
sys.path.insert(0, dir_path)
12-
13-
# Custom your logger: reduce console noise and persist errors to file
14-
# Console shows WARNING and above, while errors are written to error.log
15-
try:
16-
# remove default sinks to customize
17-
logger.remove()
18-
except Exception:
19-
pass
34+
# 日志配置:控制台显示 INFO 及以上,错误写入文件
35+
logger.remove()
2036
logger.add(sys.stderr,
21-
level="WARNING",
37+
level="INFO",
2238
format=default_format)
2339
logger.add("error.log",
2440
rotation="10 MB",
2541
diagnose=False,
2642
level="ERROR",
27-
format=default_format)
43+
format=default_format,
44+
delay=True) # 延迟创建:只有真正写入错误时才创建文件
2845

29-
# You can pass some keyword args config to init function
46+
# 初始化 NoneBot
3047
nonebot.init()
31-
nonebot.load_plugins("DicePP/plugins")
32-
app = nonebot.get_asgi()
3348

49+
# 显示启动信息
50+
@nonebot.get_driver().on_startup
51+
async def _startup_message():
52+
"""Bot 启动后显示提示信息"""
53+
logger.info("=" * 50)
54+
logger.info("DicePP 骰子机器人已启动!")
55+
logger.info("=" * 50)
56+
logger.info("等待聊天客户端连接...")
57+
logger.info("请确保您的聊天客户端 (如 LLBot) 已正确配置并连接")
58+
logger.info("=" * 50)
59+
60+
# 技术细节和测试信息只在 DEBUG 级别显示
61+
logger.debug("正在监听 OneBot V11 协议连接...")
62+
logger.debug("测试模式: 可运行 scripts\\test\\test_bot.bat 进行验证")
63+
logger.debug("测试时的 ApiNotAvailable 警告属于正常现象 (无真实客户端接收响应)")
64+
65+
# 注册适配器
3466
driver = nonebot.get_driver()
3567
driver.register_adapter(OneBot_V11_Adapter)
3668

37-
nonebot.load_from_toml("pyproject.toml")
38-
# Modify some config / config depends on loaded configs
39-
#
40-
# config = driver.config
41-
# do something...
69+
# 加载插件
70+
if _IS_FROZEN:
71+
# 打包环境:src/plugins 已加入 sys.path,直接用模块名加载
72+
nonebot.load_plugin("DicePP")
73+
else:
74+
# 开发环境:使用 load_plugins 扫描目录
75+
_plugins_dir = os.path.join("src", "plugins")
76+
nonebot.load_plugins(_plugins_dir)
4277

78+
app = nonebot.get_asgi()
4379

4480
if __name__ == "__main__":
45-
# nonebot.logger.warning("Always use `nb run` to start the bot instead of manually running!")
4681
nonebot.run(app="__mp_main__:app")

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,12 @@ dependencies = [
2929
"chardet>=3.0.2,<6.0.0",
3030
]
3131

32-
[project.optional-dependencies]
32+
[dependency-groups]
3333
dev = [
3434
"pytest>=7.4",
3535
"pytest-asyncio>=0.23",
3636
"pytest-cov>=4.1",
37+
"pyinstaller>=6.0",
3738
]
3839

3940
[tool.uv]

scripts/build/build.bat

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
@echo off
2+
REM ============================================================
3+
REM DicePP Build Script
4+
REM 使用 PyInstaller 打包 DicePP 为 Windows EXE
5+
REM ============================================================
6+
7+
setlocal enabledelayedexpansion
8+
9+
REM 切换到项目根目录 (从 scripts/build/ 向上两级)
10+
cd /d "%~dp0..\.."
11+
12+
echo ============================================================
13+
echo DicePP Build Script
14+
echo ============================================================
15+
echo.
16+
17+
REM 检查 uv 是否可用
18+
where uv >nul 2>&1
19+
if errorlevel 1 (
20+
echo [ERROR] uv not found. Please install uv first:
21+
echo https://docs.astral.sh/uv/getting-started/installation/
22+
exit /b 1
23+
)
24+
25+
REM 同步依赖(包括 dev 依赖中的 pyinstaller)
26+
echo [INFO] Syncing dependencies with uv...
27+
uv sync --dev
28+
if errorlevel 1 (
29+
echo [ERROR] Failed to sync dependencies
30+
exit /b 1
31+
)
32+
33+
echo [INFO] PyInstaller version:
34+
uv run pyinstaller --version
35+
echo.
36+
37+
REM 清理旧的 dist 目录
38+
echo [INFO] Cleaning old dist artifacts...
39+
if exist "dist" rmdir /s /q "dist"
40+
echo [INFO] Clean complete
41+
echo.
42+
43+
REM 执行打包
44+
echo [INFO] Building DicePP...
45+
echo [INFO] This may take several minutes...
46+
echo.
47+
48+
uv run pyinstaller scripts\build\dicepp.spec --clean --noconfirm
49+
50+
if errorlevel 1 (
51+
echo.
52+
echo ============================================================
53+
echo [ERROR] Build failed!
54+
echo ============================================================
55+
exit /b 1
56+
)
57+
58+
echo.
59+
echo [INFO] Relocating user-accessible files...
60+
REM 将用户需要访问的文件从 _internal 移动到 EXE 同级目录
61+
set "DIST_DIR=dist\DicePP"
62+
set "INTERNAL_DIR=%DIST_DIR%\_internal"
63+
64+
REM 移动 .env(用户配置文件)
65+
if exist "%INTERNAL_DIR%\.env" (
66+
move "%INTERNAL_DIR%\.env" "%DIST_DIR%\" >nul
67+
echo [INFO] Moved .env to application root
68+
)
69+
70+
REM 移动 Data 目录(用户数据)
71+
if exist "%INTERNAL_DIR%\Data" (
72+
move "%INTERNAL_DIR%\Data" "%DIST_DIR%\" >nul
73+
echo [INFO] Moved Data directory to application root
74+
)
75+
76+
REM pyproject.toml 可以留在 _internal,不需要用户访问
77+
78+
REM 清理 build 缓存目录
79+
echo [INFO] Cleaning build cache...
80+
if exist "build" rmdir /s /q "build"
81+
82+
echo.
83+
echo ============================================================
84+
echo [SUCCESS] Build complete!
85+
echo ============================================================
86+
echo.
87+
echo Output location: dist\DicePP\
88+
echo.
89+
echo Contents:
90+
dir /b "dist\DicePP\"
91+
echo.
92+
echo To run: dist\DicePP\DicePP.exe
93+
echo ============================================================
94+
95+
endlocal

0 commit comments

Comments
 (0)