Files
server_manager/PROJECT_REPLICATION_PROMPT.md
T
2026-05-22 00:16:08 +08:00

2934 lines
148 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Server Manager 项目总结与复刻提示词
本文档用于从零复刻当前 `server_manager` 项目。复刻目标是还原源码、配置模板、运行脚本、打包脚本、更新机制和主要交互行为;`src/dist/``output/*.exe` 等二进制构建产物不需要手写,应通过脚本重新生成。
## 当前项目总结
`Server Manager` 是一个跨平台的 Erlang 游戏服务器管理工具。
- Windows 侧提供 PyQt6 图形界面,支持打开服务器项目、维护项目配置、编译代码、创建服务器、管理本地/远程节点、查看日志和自动更新。
- Linux/Windows 都提供命令行入口,支持 `list``create``start``stop``connect``compile``regen``interactive` 等命令。
- 工具面向一个 Erlang/rebar3 游戏服务器工程,约定服务器工程根目录包含 `run/``config/``rebar3` 等内容。
- 每个服务器实例位于 `<服务器项目根>/run/<prefix>_<type>_s<id>/`,实例差异配置写入 `config/kv.config`,启动前根据模板生成 `config/sys.config`
- 项目级工具配置位于 `<服务器项目根>/.server_manager/config/`,首次打开项目时从软件自带模板目录同步 `default.kv``sys_*.config.example`
- 应用级配置位于 `%LOCALAPPDATA%/ServerManager``~/.config/ServerManager`,用于保存最近项目列表和应用日志。
- 打包使用 PyInstaller 目录模式生成 `src/dist/main/main.exe`Inno Setup 生成 `output/ServerManager_Setup.exe`
- 实际 GUI/CLI 主入口是 `src/server_manager_gui.py`;根目录 `main.py` 是一个 Tkinter 自动更新集成示例/遗留入口。
- 更新机制支持 `version.json` 清单、全量安装包更新,以及基于 `bsdiff4``main.exe` 增量补丁;替换 exe 时由独立 `mini_updater.exe` 等待主进程退出后完成替换并重启。
- 日志查看支持单文件加载、玩家协议日志、日期归档日志、ripgrep 全局搜索、正文高亮、匹配行过滤和 less 风格快捷键。
## 核心文件树
```text
server_manager/
├── README.md
├── RELEASE.md
├── PROJECT_REPLICATION_PROMPT.md
├── version.json
├── main.py
├── updater.py
├── mini_updater.py
├── release.py
├── release.bat
├── pack_all.bat
├── pack_local_test.py
├── pack_local_test.bat
├── build_installer.iss
├── rg.exe # 可选,ripgrep Windows 可执行文件
├── output/
│ ├── config/
│ │ ├── default.kv
│ │ ├── sys_game.config.example
│ │ ├── sys_login.config.example
│ │ ├── sys_center.config.example
│ │ ├── sys_cross.config.example
│ │ └── sys_client.config.example
│ ├── patches/
│ ├── version.json
│ └── ServerManager_Setup.exe # 构建产物
├── output_local/ # 本地测试包产物,通常忽略
└── src/
├── app_config.py
├── server_manager_gui.py
├── server_manager_cli.py
├── server_commands.py
├── server_creator.py
├── log_viewer_tab.py
├── log_viewer_pane.py
├── rg_search.py
├── updater.py
├── config.json
├── requirements.txt
├── run.bat
├── run.sh
├── build.bat
├── build_portable.bat
├── build_linux.sh
└── icon/
├── icon.png
├── icon.ico
└── convert_icon.py
```
## 完整复刻提示词
将下面提示词交给代码生成模型,即可要求其从零生成当前项目的等价实现。
```text
你是资深 Python 桌面工具工程师。请从零创建一个名为 Server Manager 的项目,目标是复刻一个用于 Erlang/rebar3 游戏服务器工程的跨平台管理工具。输出完整仓库源码、配置模板、运行脚本、打包脚本和文档。不要手工生成 exe、dist、installer 这类二进制产物;这些产物必须由脚本重新构建。
一、项目目标
1. 实现一个 Python 3.8+ 项目。
2. Windows 下提供 PyQt6 GUIWindows/Linux 下提供 CLI。
3. GUI 无参数启动;命令行带任意参数时进入 CLI,行为与 src/server_manager_cli.py 一致。
4. 工具管理一个外部 Erlang 游戏服务器项目,项目根目录通常含 run、config、rebar3、apps 等。
5. 首次打开服务器项目时,将软件自带模板 output/config 下的 default.kv 与 sys_*.config.example 同步到 <项目根>/.server_manager/config/。
6. 所有项目级配置保存在 <项目根>/.server_manager/config/tool.config;应用级配置和日志保存在 %LOCALAPPDATA%/ServerManager 或 ~/.config/ServerManager。
7. 支持服务器创建、启动、快速启动、停止、remsh 连接、编译、配置编辑、sys.config 重新生成、本地/远程服务器列表、节点状态批量检查、日志查看、日志搜索、自动更新、打包发版。
二、技术栈与依赖
1. Python 3.8+。
2. GUI: PyQt6。
3. 打包: pyinstaller。
4. 数据库访问: pymysql。
5. 更新下载: requests,缺失时可回退 urllib。
6. 增量更新: bsdiff4。
7. 日志全局搜索: 优先使用 ripgrep,可执行文件名 rg.exe 或 rg;也支持 RG_PATH 环境变量。
8. Windows 安装包: Inno Setup 6。
9. 被管理服务器依赖 Erlang/OTP、rebar3Linux 后台启动优先 screen,其次 tmux,最后后台进程。
三、仓库文件
请创建如下文件:
根目录:
- README.md:写明项目介绍、安装、运行、配置、打包、version.json 更新说明、CLI 参数和注意事项。
- RELEASE.md:写明本地测试包、正式发版、增量更新、ripgrep 放置方式。
- version.json:版本清单,当前版本为 1.0.46,字段包含 version、update_url、release_notes、full_installer_url、delta_updates。
- main.pyTkinter 示例/遗留入口,LOCAL_VERSION = "1.0.46",演示 updater 自动更新集成;真实桌面工具入口仍以 src/server_manager_gui.py 为准。
- updater.py:保留一个 Tkinter/独立入口可用的无感更新模块,兼容 delta 与 delta_updates。
- mini_updater.py:独立进程外更新器,等待主进程 PID 退出后替换 main.exe 并重启。
- release.py:发版脚本,负责更新 build_installer.iss、main.py、version.json,生成 delta_updates,打包后可生成 bsdiff4 patch 并填 new_exe_sha256。
- release.bat:一键正式发版脚本,顺序为 PyInstaller -> release.py 版本预处理 -> Inno Setup -> release.py --post-build。
- pack_all.bat:正式打包但不改版本号,生成 output/ServerManager_Setup.exe,并复制 version.json 到 output/。
- pack_local_test.py 与 pack_local_test.bat:生成 output_local/ServerManager_Setup_LOCAL.exe,不改版本号、不覆盖 output/。
- build_installer.issInno Setup 脚本,正式版 AppName=ServerManager,测试版 AppName=ServerManager (本地测试),安装 src/dist/main/*、src/dist/mini_updater.exe、version.json、output/config/*。
- .gitignore:忽略 output_local/,可注明 release_backup/ 可选忽略。
- rg.exe:可选,不需要生成二进制;文档说明放置在根目录即可被打包复制。
src 目录:
- requirements.txt:包含 PyQt6>=6.4.0、pyinstaller>=6.0.0、pymysql>=1.0.0、requests>=2.28.0、bsdiff4>=1.2.0。
- app_config.py:应用级配置目录、app_config.json、最近项目列表、应用日志目录。
- server_manager_gui.pyPyQt6 主 GUI 和主入口;无参数 GUI,有参数转发 CLI。
- server_manager_cli.py:命令行逻辑。
- server_commands.py:服务器命令、配置模板同步、节点状态、启动/停止/remsh、sys.config 生成、远程服务器 RPC 查询、日志路径和 trace RPC。
- server_creator.py:服务器创建、kv.config 生成、配置项定义、ID 范围、端口默认值。
- log_viewer_tab.py:日志查看顶级页签,含“日志搜索”和“全局搜索”两个子页。
- log_viewer_pane.py:可嵌入或独立窗口的日志正文检索组件。
- rg_search.pyripgrep 封装。
- updater.pyGUI 使用的自动更新工具函数。
- config.json:旧版/示例配置,可包含 workspace、database、server、erlang、commands、login_db。
- run.bat 与 run.sh:源码/打包入口自动选择脚本。
- build.bat、build_portable.bat、build_linux.shPyInstaller 打包脚本。
- icon/convert_icon.py、icon/icon.png、icon/icon.ico:图标资源和转换脚本。
output/config 目录:
- default.kv:默认配置和配置项定义。
- sys_game.config.example、sys_login.config.example、sys_center.config.example、sys_cross.config.example、sys_client.config.exampleErlang sys.config 模板,使用 ${key} 占位符。
四、version.json 内容
生成根目录 version.json,内容结构如下:
{
"version": "1.0.46",
"update_url": "http://172.18.180.94:3000/api/file?path=server_manager/output/version.json",
"release_notes": "优化ui",
"full_installer_url": "http://172.18.180.94:3000/api/download?path=server_manager/output/ServerManager_Setup.exe",
"delta_updates": {
"1.0.45": {
"patch_url": "http://172.18.180.94:3000/api/download?path=server_manager/output/patches/v1.0.45_to_v1.0.46.patch",
"new_exe_sha256": ""
}
}
}
五、配置体系
1. 项目级配置目录固定为 <项目根>/.server_manager/config。
2. 软件自带模板目录:
- 打包后:main.exe 同级的 config/。
- 源码运行:仓库根目录 output/config/。
3. 受管模板文件:
- default.kv
- sys_center.config.example
- sys_client.config.example
- sys_cross.config.example
- sys_game.config.example
- sys_login.config.example
4. 打开项目时必须确保 .server_manager/config 存在并同步上述模板;保留用户自己的 tool.config。
5. 配置合并优先级从低到高:
- .server_manager/config/default.kv
- .server_manager/config/tool.config
- run_dir/config/tool.config 或 run_dir/tool.config
- run_dir/<server_dir>/config/kv.config
6. 合并配置后强制用当前机器 IP 覆盖 ip 和 game_host。
7. tool.config 是 key=value 文件,至少写入:
- server_root、run_dir
- r25_path
- db_host、db_port、db_user、db_pass
- login_db_name、login_db_server_id、login_db_host、login_db_port、login_db_user、login_db_pass
- prefix、default_server_id、server_type、server_name、login_node、center_node、cookie
8. 应用级配置:
- Windows: %LOCALAPPDATA%/ServerManager/app_config.json
- Linux/macOS: ~/.config/ServerManager/app_config.json
- 保存最近 10 个有效项目,判断有效项目时需存在 .server_manager/config/default.kv 或旧式 config/default.kv。
- 应用日志写到同目录 logs/,日志文件名 server_manager_YYYYMMDD.log 或 server_manager_cli_YYYYMMDD.log,保留最近 7 天。
六、default.kv 要求
default.kv 必须含以下默认值和配置定义:
- prefix=ddxq2
- server_id=1
- server_type=game_server
- server_name=自己名字_1
- ip=
- game_host=
- tcp_port=18001
- http_port=19001
- login_node=ddxq2_login_server_s900@172.16.14.122
- center_node=ddxq2_center_server_s1@172.16.14.122
- db_host=172.16.12.220
- db_port=3306
- db_user=ddxqadmin
- db_pass=ddxq@44168deabe63
- db_save_time=300000
- db_save_count=10000
- log_save_time=120000
- log_save_count=1000
- log_dir=run/server/log
- role_log=run/server/log
- logger_level=debug
- open_time={{2025,1,1},{10,0,0}}
- merge_server_ids=[]
- merge_server_time={{1970,1,1},{0,0,0}}
- last_merge_server_ids=[]
- game_tcp_port=18001
- login_host=172.16.14.122
- login_http_port=19900
- is_develop=true
- is_inner=true
- auto_reload=true
- gm_auth=true
- risk_control_server_ip=msg.4399sy.com
- risk_control_server_post=5978
- game_id_min=100, game_id_max=9999, game_id_default=100
- login_id_min=11, login_id_max=99, login_id_default=10
- center_id_min=1, center_id_max=10, center_id_default=1
- cross_id_min=10000, cross_id_max=20000, cross_id_default=10000
- client_id_min=1, client_id_max=10000, client_id_default=1
- login_db_name=
- login_db_server_id=900
- login_db_host=172.16.12.220
- login_db_port=3306
- login_db_user=ddxqadmin
- login_db_pass=ddxq@44168deabe63
- cookie=ddxq2-node
必须配置项定义:
- required_keys_base=prefix,server_id,server_type,ip,db_host,db_user,db_pass,db_port,log_dir,db_game_name,auto_reload
- required_keys_game=server_name,game_host,tcp_port,http_port,open_time,login_node,center_node,db_log_name
- required_keys_login=tcp_port
- required_keys_center=open_time,login_node,db_log_name
- required_keys_cross=open_time,center_node,db_log_name
- required_keys_client=tcp_port,game_host,login_host,login_http_port,login_node
配置项定义使用 config_keys_base_*、config_keys_game_*、config_keys_login_*、config_keys_center_*、config_keys_cross_*、config_keys_client_*;编辑对话框可选项使用 editable_config_*。至少包含当前项目中的这些 keyprefix、server_id、server_type、ip、db_host、db_port、db_user、db_pass、db_save_time、db_save_count、log_save_time、log_save_count、log_dir、logger_level、is_develop、is_inner、auto_reload、gm_auth、server_name、game_host、tcp_port、http_port、login_node、center_node、open_time、merge_server_ids、merge_server_time、last_merge_server_ids、risk_control_server_ip、risk_control_server_post、role_log、game_tcp_port、login_host、login_http_port。
七、sys.config 模板要求
1. 每种服务器类型都有一个 sys_*.config.example。
2. 模板中使用 ${key} 占位符,至少覆盖:
${auto_reload}, ${center_node}, ${db_game_name}, ${db_host}, ${db_log_name}, ${db_pass}, ${db_port}, ${db_save_count}, ${db_save_time}, ${db_user}, ${game_host}, ${game_tcp_port}, ${gm_auth}, ${http_port}, ${is_develop}, ${is_inner}, ${log_dir}, ${log_save_count}, ${log_save_time}, ${logger_level}, ${login_host}, ${login_http_port}, ${login_node}, ${merge_server_ids}, ${open_time}, ${prefix}, ${risk_control_server_ip}, ${risk_control_server_post}, ${role_log}, ${server_id}, ${server_name}, ${server_type}, ${tcp_port}。
3. 生成 sys.config 时必须根据 server_type 选择模板:
- game_server -> sys_game.config.example
- login_server -> sys_login.config.example
- client_server -> sys_client.config.example
- center_server -> sys_center.config.example
- cross_server -> sys_cross.config.example
4. 生成 sys.config 前需要将 log_dir 和 role_log 归一化为 run/<server_dir>/<tail>。如果 default.kv 中是 run/server/log,则实际应变为 run/<server_dir>/log。
5. 若模板中仍有未替换占位符,要输出警告但不要崩溃。
八、服务器创建逻辑
1. 支持服务器类型:
- game -> game_server,显示名“游戏服”
- login -> login_server,显示名“登录服”
- client -> client_server,显示名“客户端测试服”
- center -> center_server,显示名“中心服”
- cross -> cross_server,显示名“跨服”
2. 目录名格式:<prefix>_<type>_s<server_id>,例如 ddxq2_game_s100。
3. 服务器目录:<run_dir>/<server_dir>/config/。
4. 差异配置文件:kv.config,只写入当前实例的差异配置。
5. 创建后立即调用 generate_start_config 生成 sys.config。
6. 默认端口:
- tcp_port = 18000 + server_id
- http_port = 19000 + server_id
7. 默认游戏服显示名:<prefix>_<server_id>GUI 中可由 server_name 前缀 + ID 动态生成。
8. 数据库名:
- db_game_name=<prefix>_<type_prefix>_s<id>
- db_log_name=<prefix>_<type_prefix>_log_s<id>,登录服不写 db_log_name
9. game 额外写入 server_name、game_host、tcp_port、http_port、open_time、role_log。
10. login 写入 tcp_port。
11. center 写入 open_time、login_node。
12. cross 写入 open_time、center_node。
13. client 写入 tcp_port、game_host、game_tcp_port、login_host、login_http_port、login_node。
14. auto_reload 作为布尔复选项,写成 true/false。
15. GUI 创建前检查同类型同 ID 是否已存在;已存在时警告,覆盖只在二次确认后允许。
九、命令构建与服务器运行
1. 平台检测:
- Windows: REBAR3_CMD = rebar3.cmd
- Linux: REBAR3_CMD = ./rebar3
2. 如果配置了 Erlang 根目录 r25_path
- Windows 使用 <r25_path>/bin/erl.exe 与 escript.exe
- Linux 使用 <r25_path>/bin/erl 与 escript
3. rebar3 命令:
- Windows 如配置了 Erlang 路径,用 "<escript>" "<server_root>/rebar3",否则 rebar3.cmd。
- Linux 如配置了 Erlang 路径,用 "<escript>" "<server_root>/rebar3",否则 ./rebar3。
4. rebar profile:
- login_server -> login_server_dev, app login_server
- client_server -> client_server_dev, app client_server
- 其他 -> game_server_dev, app game_server
5. 标准启动命令:
rebar3 as <profile> shell --eval -1,"application:ensure_all_started(<app>)." --setcookie <cookie> --name <node_name> --config "<sys.config>"
6. 快速启动命令:
erl -pa <_build profile ebin paths> -smp enable -setcookie <cookie> -name <node_name> -config "<sys.config>" -eval "application:ensure_all_started(<app>)."
7. 启动工作目录统一为 server_root,不要切换到 run/<server_dir>;日志路径由 sys.config 指向 run/<server_dir>/log。
8. stop 命令用 erl -noshell -name stop_<timestamp>@<local_ip> -setcookie <cookie> -eval "rpc:call('<target_node>', init, stop, [])" -s c q,参数列表方式执行,避免 shell 引号问题。
9. remsh 命令用 erl -setcookie <cookie> -name remsh_<timestamp>@<local_ip> -remsh <target_node>。
10. 节点状态使用 net_adm:ping;批量状态检查需要缓存 NodeStatusCache,减少重复查询。
11. 可实现 EPMD 快速查询作为优化;至少提供 check_node_status 和 check_nodes_status。
12. Windows 新窗口启动用 cmd/start 或 powershell/cmd 方案,并记录窗口标题,停止服务器后尝试关闭由本工具启动的服务窗口和 remsh 窗口。
13. Linux 后台启动优先 screen,其次 tmux,否则后台进程。
十、CLI 规格
创建 server_manager_cli.py,命令行参数:
全局参数:
- --root / -r:服务器根目录
- --config / -c:指定配置文件
- --cookie:覆盖 Erlang cookie
子命令:
- list:列出所有服务器,按 game/cross/login/client/center/other 分组。
- create:创建服务器。
- --type/-t 必填,choices game/login/center/cross/client
- --id/-i 必填 int
- --prefix/-p
- --db-host、--db-port、--db-user、--db-pass
- --login-node、--center-node、--server-name
- --tcp-port、--http-port
- --open-time
- --overwrite
- start
- --server/-s 必填
- --quick/-q 表示不用 rebar3,直接 erl
- --background/-b 表示后台运行;默认前台
- stop--server/-s 必填
- connect--server/-s 必填,--ip 可选
- compile
- --target/-t choices all/code/proto/table/tbllog,默认 all
- --type choices game/login,默认 game
- regen--server/-s 必填,重新生成 sys.config
- interactive / i:交互式菜单
CLI 初始化时自动检测 server_root
1. 命令行 --root 优先。
2. 读取 tool.config 的 server_root/run_dir 次之。
3. 当前工作目录、脚本目录、上级目录中查找 apps 与 config。
4. 加载配置后执行 run_startup_migrations。
十一、GUI 规格
GUI 使用 PyQt6。主窗口标题为 Server Manager v<version>,最小尺寸 1000x750,深色主题:
- 背景 #181b22
- 面板 #242932
- 边框 #3a4352
- 主色 #4f8cff
- 成功 #25c889
- 危险 #ff5c6a
- 文本 #e8eef7 / #aab4c3
主窗口行为:
1. 无项目启动时显示欢迎页。
2. 菜单栏:
- 文件:打开项目、最近的项目、关闭项目、退出。
- 帮助:检查更新、关于。
3. 状态栏显示运行状态、版本号、系统时间。
4. 打开项目成功后显示主项目内容:上方主标签页,下方控制台输出区。
5. 主标签页:
- 控制台
- 创建服务器
- 服务器管理
- 日志查看
- 工具设置
6. 控制台输出区只在“控制台”页显示;其他页隐藏。
7. 顶部角落显示本机 IP,点击可复制。
8. 首次打开项目后初始化节点状态缓存。
GUI 图标:
使用代码内嵌简洁线性 SVG 图标渲染为 QIcon,至少支持 calendar、plus、server、log、settings、code、bolt、shield、table、database、file、trash、play、stop、rocket、network、user、branch、download、folder、check、link、edit、refresh、save、external、eye、eye_off。
控制台页:
1. 分三组卡片:
- 编译与构建:编译代码、快速编译、编译协议、编译Table、编译Tbllog、清理编译。
- 服务器运行:运行服务器、快速启动、连接节点、查看账号、停止服务器、服务器清档。
- 版本控制:SVN 更新。
2. 编译命令:
- svn update
- rebar3 as game_server_dev compile
- rebar3 protobuf compile -a game_server
- rebar3 cache compile -a game_server
- rebar3 tbllog compile
- rebar3 clean
- 快速编译:escript shell/es/emake.escript 1 <module> && escript shell/es/emake.escript 2 <module>
3. 运行/快速启动前检查节点是否已在线,在线则询问是否 remsh 连接。
4. 停止服务器要异步执行,进度输出到控制台,成功后更新状态缓存。
5. 清档功能:停止服务器后连接 MySQL,删除游戏库和日志库,需要明确危险提示。
6. 查看账号功能:连接数据库展示玩家账号相关数据,结果表格支持数字排序。
创建服务器页:
1. 卡片布局包含“服务器配置”“数据库配置”“节点配置”。
2. 服务器类型下拉:游戏服务器、登录服务器、客户端服务器、中心服务器、跨服服务器。
3. 按类型动态显示字段:
- gameserver_name、tcp_port、http_port、auto_reload、open_time、login_node、center_node
- logintcp_port、auto_reload
- centerauto_reload、open_time、login_node
- crossauto_reload、open_time、center_node
- client:连接服务器ID、连接服务器端口、登录服地址、登录服HTTP端口、auto_reload、login_node
4. ID 范围从 default.kv 的 <type>_id_min/max/default 读取,越界时自动修正并提示。
5. 服务器 ID 改变时自动更新服务器名与默认端口。
6. 数据库和节点支持“使用默认配置”复选框,默认勾选并禁用手工输入。
7. 点击创建前展示确认信息,再调用 server_creator.create_server。
服务器管理页:
1. 顶部显示运行目录。
2. 两个子标签:
- 本地服务器:扫描 run_dir,按 game/cross/login/client/center/other 分类显示;每项显示状态图标、类型图标、是否有 kv.config。
- 远程服务器:从登录服节点 RPC 加载远程服务器列表,表格列为服务器ID、服务器名称、服务器节点,支持过滤和排序。
3. 本地操作:刷新列表、刷新状态、远程连接、编辑配置、重新生成配置、删除服务器。
4. 远程操作:从登录服加载、刷新状态、搜索过滤、双击或按钮 remsh 连接。
5. 远程列表通过登录服节点 rpc:call 获取 server_temp_info/server_info_lib 数据;输出需解析 server_id、server_name、server_node。
6. 状态刷新使用批量 check_nodes_status,并写入 NodeStatusCache。
7. 编辑配置打开 KvConfigEditDialog,读取 kv.config,允许添加/删除 default.kv 中声明的可编辑配置项;保存后重新生成 sys.config。
日志查看页:
1. 顶级“日志查看”内有两个子页签:
- 日志搜索:加载单个日志文件。
- 全局搜索:用 ripgrep 在当前服务器 log 目录递归搜索。
2. 日志搜索支持:
- 选择服务器目录。
- 日志类型:服务器日志 / 玩家协议(tag_log)。
- 服务器日志预设:error.log、debug.log、http_info.log、http_error.log、info.log、http.log。
- 自定义日志文件名。
- 玩家协议按角色 ID 生成 network_codec-<role_id>.log。
- 指定归档日期,文件名变为 <base>.YYYYMMDD。
- 加载日志、开启玩家协议、关闭玩家协议、打开文件夹、独立窗口、清空、自动滚动。
3. 日志文件路径规则:
- 无日期:<run_dir>/<server_dir>/log/<subdir>/<base_filename>
- 有日期:<run_dir>/<server_dir>/log/<subdir>/<base_filename>.YYYYMMDD
- 玩家协议 subdir=tag_log
4. 单文件读取最大约 3MB,编码依次尝试 utf-8、utf-8-sig、gbk、latin-1。
5. 开启/关闭玩家协议通过 rpc_role_gs_trace_network 调 role_gs:trace_network/1 或 trace_network_close/1。
6. 全局搜索要求找到 rg,否则提示用户放置 rg.exe 或设置 RG_PATH;搜索结果在独立窗口打开。
日志正文组件:
1. LogViewerPane 包含搜索行和只读 QPlainTextEdit。
2. 支持区分大小写、仅显示匹配行、正则表达、全词匹配。
3. 检测到 rg 时,过滤、高亮、上/下一个都尽量与 ripgrep 一致;否则回退 Python re。
4. 支持快捷键:
- Enter/F3/n:下一个
- Shift+F3/Shift+N:上一个
- g:文件开头
- Shift+G:文件末尾
- / 或 Ctrl+F:聚焦搜索框
5. 使用 rg --json 时要将 UTF-8 byte offsets 转为 Qt UTF-16 光标位置。
工具设置页:
1. 表单分组:
- 项目目录配置:服务器根目录、运行目录,均有浏览按钮。
- 数据库配置:host、port、user、password,密码框带眼睛图标。
- 服务器默认配置:prefix、默认服务器ID、server_type、server_name、默认 TCP/HTTP 端口。
- Erlang 与节点配置:Erlang 路径、Erlang 版本、登录服节点、中心服节点、Cookie。
2. 提供“检查版本”按钮,比对系统 erl 和配置路径 erl 的 OTP 版本。
3. 登录服节点、中心服节点提供“测试连接”,使用 net_adm:ping。
4. “填充默认值”从 default.kv 填入表单。
5. “保存配置”写入 tool.config;若 Erlang 路径变化,询问是否删除 server_root/_build 并执行 rebar3 as game_server_dev compile。
欢迎页:
1. 无项目时显示欢迎页,提供打开项目、最近项目、定位项目、帮助入口。
2. 最近项目来自应用级 app_config.json,只显示仍存在且含 default.kv 的项目。
更新机制:
1. src/updater.py 提供:
- get_current_version
- get_version_info
- version_less
- fetch_update_manifest
- get_delta_for_current
- download_file
- clean_up_old_version
- apply_delta_patch
- run_installer_and_exit
2. 打包后 version.json 优先从 main.exe 同目录读取,其次从 _MEIPASS。
3. GUI 启动 1.5 秒后后台检查 update_url;远端 version 与本地不同则询问是否更新。
4. 当前版本有 delta_updates 且 patch_url、new_exe_sha256 完整时优先增量,否则下载 full_installer_url 或 download_url。
5. 下载时显示 QProgressDialog;可取消。
6. 增量使用 bsdiff4.patch(当前 main.exe, patch) 生成临时新 exe,校验 sha256,通过 mini_updater.exe 替换并重启。
7. 全量更新下载安装包后用 /VERYSILENT /SUPPRESSMSGBOXES /FORCECLOSEAPPLICATIONS 静默安装并退出。
8. mini_updater.py 只依赖标准库,参数:
- --pid
- --install-dir
- --new-exe-path
- --target-exe-name
等待 pid 退出,重命名旧 exe 为 .old,移动新 exe,cwd 为安装目录启动新程序。
打包脚本:
1. src/build.bat
- 安装 PyQt6、pyinstaller、pymysql、Pillow。
- 调 icon/convert_icon.py。
- 清理 dist/build/spec。
- PyInstaller onedir console 模式,name=main,入口 server_manager_gui.py。
- add-data: ../version.json、config.json、server_commands.py、server_creator.py、icon。
- hidden-import: PyQt6.QtWidgets/Core/Gui、pymysql、server_commands、server_creator、server_manager_cli、updater、app_config、bsdiff4、requests。
- collect-all PyQt6 与 bsdiff4。
- 若根目录有 rg.exe,复制到 dist/main/rg.exe。
- 复制 ../output/config 到 dist/main/config。
- 再用 PyInstaller onefile windowed 打包 ../mini_updater.py 到 dist/mini_updater.exe。
2. src/build_linux.sh 与 build.bat 等价,但使用 python3、Linux add-data 冒号分隔,输出 dist/main/main。
3. src/build_portable.bat 提供单文件便携版尝试,失败回退 onedir。
4. build_installer.iss
- 正式输出 output/ServerManager_Setup.exe。
- 本地测试 /DLOCAL_TEST=1 输出 output_local/ServerManager_Setup_LOCAL.exe,独立 AppId。
- 安装 src/dist/main/* 到 {app},安装 mini_updater.exe、version.json、output/config/*。
- 创建开始菜单/可选桌面快捷方式。
- 安装完成后运行 main.exe。
5. release.py
- get_current_version 以 output/version.json 为准读取旧版本。
- set_version_pre(new_version, notes):更新 build_installer.iss 的 MyAppVersion、main.py 的 LOCAL_VERSION、version.json 的 version/release_notes/delta_updates,并同步到 src/dist/main/version.json 和 _internal/version.json。
- set_version_post(old_exe):复制 version.json 到 output;若提供旧版 main.exe 且安装 bsdiff4,生成 output/patches/v<old>_to_v<new>.patch 并填 new_exe_sha256。
入口脚本:
1. src/run.bat
- 自动计算 SERVER_ROOT 为脚本目录向上两级。
- 优先使用 SERVER_MANAGER_MAIN、同目录 main.exe、dist/main/main.exe。
- 无参数进入 interactive。
- gui 启动 GUI。
- 其他参数转发 CLI,并附加 --root SERVER_ROOT。
2. src/run.sh 同理,优先 SERVER_MANAGER_MAIN、同目录 main、dist/main/main。
十二、核心代码复刻细则
下面是必须复刻的核心代码结构、关键函数签名和关键实现逻辑。生成代码时可以按这些骨架实现,允许适度重构,但外部行为、配置文件路径、命令格式、函数职责必须保持一致。
1. app_config.py 核心代码
必须提供这些函数:
- get_app_config_dir() -> Path
- get_app_config_path() -> Path
- load_app_config() -> Dict[str, Any]
- save_app_config(data: Dict[str, Any]) -> None
- get_recent_projects() -> List[str]
- save_recent_project(project_path: str) -> None
- get_app_log_dir() -> Path
实现要点:
- Windows 使用 os.environ["LOCALAPPDATA"] 或用户主目录下的 ServerManager。
- 非 Windows 使用 Path.home() / ".config" / "ServerManager"。
- load_app_config 读取 JSON,异常或格式错误返回 {}。
- save_app_config 使用 json.dump(..., ensure_ascii=False, indent=2)。
- get_recent_projects 从 app_config.json 的 recent_projects 读取,去重,只保留存在且 project_has_default_kv_for_manager(path) 为 True 的项目,最多 10 个。
- save_recent_project 将 resolve 后的路径插到列表首位。
伪代码:
def get_app_config_dir():
if sys.platform == "win32":
base = os.environ.get("LOCALAPPDATA") or os.path.expanduser("~")
root = Path(base) / "ServerManager"
else:
root = Path.home() / ".config" / "ServerManager"
root.mkdir(parents=True, exist_ok=True)
return root
def get_recent_projects():
data = load_app_config()
raw = data.get("recent_projects") or []
result = []
for item in raw[:15]:
p = Path(str(item).strip())
if p.exists() and str(p) not in result and project_has_default_kv_for_manager(p):
result.append(str(p))
if len(result) >= 10:
break
return result
2. server_commands.py 配置与模板同步核心代码
必须定义:
- IS_WINDOWS = platform.system() == "Windows"
- IS_LINUX = platform.system() == "Linux"
- REBAR3_CMD = "rebar3.cmd" if IS_WINDOWS else "./rebar3"
- MANAGED_CONFIG_TEMPLATE_FILES = ("default.kv", "sys_center.config.example", "sys_client.config.example", "sys_cross.config.example", "sys_game.config.example", "sys_login.config.example")
必须提供这些函数:
- get_server_manager_config_dir(server_root: Union[str, Path]) -> Path
- get_bundled_tool_config_template_dir() -> Path
- sync_server_manager_config_templates(project_root: Path) -> Tuple[bool, str]
- ensure_server_manager_config(project_root: Path) -> Tuple[bool, str]
- ensure_and_get_server_manager_config_dir(project_root: Path) -> Tuple[Optional[Path], str]
- project_has_default_kv_for_manager(project_root: Union[str, Path]) -> bool
- read_config_file(file_path: str) -> Dict[str, str]
- write_config_file(file_path: str, config: Dict[str, str]) -> None
- read_merged_config(server_root: str, server_dir: str = None, run_dir: str = None) -> Dict[str, str]
实现要点:
- get_server_manager_config_dir 返回 Path(server_root).resolve() / ".server_manager" / "config"。
- get_bundled_tool_config_template_dir 打包后返回 Path(sys.executable).parent / "config",源码运行返回 Path(__file__).parent.parent / "output" / "config"。
- sync_server_manager_config_templates 必须检查模板目录存在、default.kv 存在、每个 MANAGED_CONFIG_TEMPLATE_FILES 都存在;逐个 copy2 到项目 .server_manager/config。
- read_config_file 读取 key=value,忽略空行、# 注释、% 注释;去除空白和首尾双引号。
- read_merged_config 合并顺序为 default.kv -> tool.config -> run_dir tool.config -> kv.config,最后强制 merged["ip"] 和 merged["game_host"] 为 get_local_ip()。
read_merged_config 伪代码:
def read_merged_config(server_root, server_dir=None, run_dir=None):
cfg_dir = get_server_manager_config_dir(server_root)
merged = {}
if (cfg_dir / "default.kv").exists():
merged.update(read_config_file(str(cfg_dir / "default.kv")))
elif (cfg_dir / "default.config").exists():
merged.update(read_config_file(str(cfg_dir / "default.config")))
if (cfg_dir / "tool.config").exists():
merged.update(read_config_file(str(cfg_dir / "tool.config")))
if run_dir:
for p in (Path(run_dir) / "config" / "tool.config", Path(run_dir) / "tool.config"):
if p.exists():
merged.update(read_config_file(str(p)))
break
if server_dir:
kv = (Path(run_dir) if run_dir else Path(server_root) / "run") / server_dir / "config" / "kv.config"
if kv.exists():
merged.update(read_config_file(str(kv)))
local_ip = get_local_ip()
merged["ip"] = local_ip
merged["game_host"] = local_ip
return merged
3. server_commands.py 命令生成核心代码
必须提供这些函数:
- get_local_ip() -> str
- get_erl_cmd(erl_path: str = None) -> str
- get_escript_cmd(erl_path: str = None) -> str
- get_rebar3_cmd(server_root: str, erl_path: str = None) -> str
- get_ebin_paths(server_root: str, profile: str = "game_server_dev") -> List[str]
- get_rebar_profile(server_type: str) -> Tuple[str, str]
- build_start_command_by_config(server_root: str, merged_config: Dict[str, str], cookie: str, use_rebar: bool = True, erl_path: str = None) -> str
- build_start_command(server_root: str, server_name: str, cookie: str, use_rebar: bool = True, erl_path: str = None) -> str
- build_stop_command(server_name: str, cookie: str, local_host: str = None, erl_path: str = None) -> List[str]
- build_remsh_command(server_name: str, cookie: str, target_ip: str = None, erl_path: str = None) -> str
- subprocess_args_to_display(args: List[str]) -> str
命令格式必须符合:
- login_server 使用 profile login_server_devapp login_server。
- client_server 使用 profile client_server_devapp client_server。
- game/center/cross 使用 profile game_server_devapp game_server。
- rebar3 启动:
<rebar3> as <profile> shell --eval -1,"application:ensure_all_started(<app>)." --setcookie <cookie> --name <node_name> --config "<sys.config>"
- 快速 erl 启动:
"<erl>" -pa "<ebin1>" -pa "<ebin2>" ... -smp enable -setcookie <cookie> -name <node_name> -config "<sys.config>" -eval "application:ensure_all_started(<app>)."
- stop 必须返回参数列表:
[erl, "-noshell", "-name", "stop_<timestamp>@<ip>", "-setcookie", cookie, "-eval", "rpc:call('<target_node>', init, stop, [])", "-s", "c", "q"]
build_start_command_by_config 伪代码:
def build_start_command_by_config(server_root, merged_config, cookie, use_rebar=True, erl_path=None):
server_dir = merged_config.get("_server_dir", "")
config_file = merged_config.get("_config_file", "")
game_host = merged_config.get("game_host", get_local_ip())
node_name = merged_config.get("node_name", f"{server_dir}@{game_host}").replace('"', '').replace("'", "")
server_type = merged_config.get("server_type", "game_server").strip('"').strip("'")
profile, app_name = get_rebar_profile(server_type)
if use_rebar:
rebar3 = get_rebar3_cmd(server_root, erl_path)
return f'{rebar3} as {profile} shell --eval -1,"application:ensure_all_started({app_name})." --setcookie {cookie} --name {node_name} --config "{config_file}"'
erl = get_erl_cmd(erl_path)
pa_args = " ".join(f'-pa "{p}"' for p in get_ebin_paths(server_root, profile))
return f'"{erl}" {pa_args} -smp enable -setcookie {cookie} -name {node_name} -config "{config_file}" -eval "application:ensure_all_started({app_name})."'
4. server_commands.py sys.config 生成核心代码
必须提供:
- replace_placeholders(template_content: str, config: Dict[str, str]) -> str
- get_template_file(config_path: Path, server_type: str) -> Optional[Path]
- build_replace_map(config: Dict[str, str]) -> Dict[str, str]
- generate_start_config(server_root: str, server_dir: str) -> Optional[Dict[str, str]]
generate_start_config 必须:
- 定位 kv.config: <server_root>/run/<server_dir>/config/kv.config。
- 读取 read_merged_config(server_root, server_dir)。
- 按 server_type 选择 sys_*.config.example。
- 将 log_dir 和 role_log 归一化为 run/<server_dir>/<tail>。
- 替换所有 ${key}。
- 写入 <server_root>/run/<server_dir>/config/sys.config。
- 在 merged_config 中添加内部字段 _server_dir 和 _config_file。
核心伪代码:
def _normalize_run_log_path(raw, default_tail):
raw = (raw or "").strip('"').strip("'").strip()
if raw.startswith("run/"):
parts = raw.split("/", 2)
tail = parts[2] if len(parts) > 2 and parts[2] else default_tail
else:
tail = raw or default_tail
return f"run/{server_dir}/{tail}"
merged_config["log_dir"] = _normalize_run_log_path(merged_config.get("log_dir", "log"), "log")
if "role_log" in merged_config:
merged_config["role_log"] = _normalize_run_log_path(merged_config.get("role_log", ""), "log")
replace_map = build_replace_map(merged_config)
output = template_content
for key, value in replace_map.items():
output = output.replace(f"${{{key}}}", str(value))
unreplaced = re.findall(r"\$\{(\w+)\}", output)
if unreplaced:
print(f"[警告] 配置文件中有未替换的占位符: {set(unreplaced)}")
sys_config_file.write_text(output, encoding="utf-8")
build_replace_map 必须为缺失字段提供默认值:
- prefix=ddxq2
- server_id=1
- server_type=game_server
- tcp_port=18001
- http_port=19001
- log_dir=log
- merge_server_ids=[]
- merge_server_time={{1970,1,1},{0,0,0}}
- last_merge_server_ids=[]
- open_time={{2025,1,1},{10,0,0}}
- auto_reload=true
- ip/game_host 使用 get_local_ip()
5. server_commands.py 迁移与列表核心代码
必须提供:
- run_startup_migrations(server_root, logger_func=None) -> Dict[str, Any]
- get_config_list(run_dir: str) -> List[str]
- get_server_list(run_dir: str) -> Dict[str, List[str]]
- flatten_server_dirs(run_dir: str) -> List[str]
迁移状态:
- 状态文件为 <项目根>/.server_manager/migrations.json。
- 迁移 ID 至少包含 sys_config_log_dir_v2 与 sys_config_role_log_v1。
- 每个迁移只执行一次,执行内容是为所有包含 config/kv.config 的服务器重新 generate_start_config。
get_server_list 分类规则:
- 含 _game_s 或 _game_ -> game
- 含 _cross_s 或 _cross_ -> cross
- 含 _login_s 或 _login_ -> login
- 含 _client_s 或 _client_ -> client
- 含 _center_s 或 _center_ -> center
- 其他有 config 目录的 -> other
6. server_commands.py 节点状态与远程 RPC 核心代码
必须提供:
- class NodeStatusCache
- get(node_name) -> Optional[bool]
- set(node_name, online)
- set_online(node_name)
- set_offline(node_name)
- batch_set(results)
- get_all()
- clear()
- is_initialized()
- set_initialized(value=True)
- get_node_status_cache() -> NodeStatusCache
- check_node_status(server_name, cookie, target_ip=None, erl_path=None) -> bool
- check_nodes_status(server_names, cookie, target_ip=None, erl_path=None) -> Dict[str, bool]
- query_remote_servers_from_login_rpc(login_node, cookie, erl_path=None, timeout=120) -> List[Dict[str, Any]]
- rpc_role_gs_trace_network(server_name, cookie, role_id, enable, target_ip=None, erl_path=None) -> Tuple[int, str, str, str]
节点状态实现要求:
- check_node_status 内部可调用 check_nodes_status。
- check_nodes_status 优先尝试 epmd 快速查询,再用 erl net_adm:ping 批量检测;即使不实现 epmd 优化,也必须提供批量接口和超时保护。
- 本地临时节点名使用 sm_ping_<timestamp>@<local_ip> 或类似名称,避免重复。
- 启动 erl 前尽量执行 epmd -daemon。
远程服务器 RPC 要点:
- 先 ping 登录服节点,失败则给出清晰错误。
- 通过临时 Erlang 脚本 file:script/1 调用登录服节点。
- 输出格式为:
SM_COUNT<TAB>数量
server_id<TAB>server_name_base64<TAB>server_node
- Python 侧解析 base64 得到 UTF-8 server_name。
- 返回字段至少包含 server_id、server_name、server_node、running=False。
玩家协议 trace RPC
- enable=True 调 role_gs:trace_network(RoleId)。
- enable=False 调 role_gs:trace_network_close(RoleId)。
- 返回 returncode、stdout、stderr、完整命令行。
7. server_commands.py 日志路径核心代码
必须提供:
- resolve_log_file_path(run_dir, server_dir, base_filename, date_yyyymmdd=None, subdir="") -> Path
- read_text_file_best_effort(path: Path, max_bytes: int = 3145728) -> Tuple[str, Optional[str]]
路径规则:
log_root = Path(run_dir) / server_dir / "log"
if subdir:
log_root = log_root / subdir
if date_yyyymmdd 是 8 位数字:
name = f"{base_filename}.{date_yyyymmdd}"
else:
name = base_filename
return log_root / name
读取规则:
- 文件不存在、非普通文件、超过 max_bytes 都返回 ("", 错误说明)。
- 编码依次尝试 utf-8、utf-8-sig、gbk、latin-1。
- 都失败则 utf-8 errors="replace"。
8. server_creator.py 核心代码
必须定义:
- SERVER_TYPE_MAP = {"game": "game_server", "login": "login_server", "client": "client_server", "center": "center_server", "cross": "cross_server"}
- SERVER_TYPE_NAMES = {"game": "游戏服", "login": "登录服", "client": "客户端测试服", "center": "中心服", "cross": "跨服"}
- BASE_REQUIRED_KEYS、SERVER_TYPE_REQUIRED_KEYS、BASE_CONFIG_KEYS、SERVER_TYPE_SPECIFIC_KEYS。
必须提供:
- get_required_keys_from_kv(server_root, server_type)
- get_config_keys_from_kv(server_root, server_type)
- get_editable_config_from_default_kv(server_root)
- get_allowed_config_keys(server_type, server_root="")
- get_all_valid_keys(server_root="")
- get_required_keys(server_type, server_root="")
- get_server_type_name(server_type)
- extract_template_placeholders_with_comments(server_root, server_type)
- get_template_config_keys(server_root, server_type)
- get_server_type_str(server_type)
- generate_server_dir_name(prefix, server_type, server_id)
- get_existing_server_ids(run_dir, server_type, prefix)
- build_kv_config(...)
- create_server(...)
- get_id_range_from_config(default_kv, server_type)
- get_default_port(server_type, server_id)
- get_default_server_name(prefix, server_id)
- build_confirmation_message(...)
build_kv_config 必须生成差异配置:
server_type_str = SERVER_TYPE_MAP.get(server_type, "game_server")
type_prefix = {"game": "game", "login": "login", "center": "center", "cross": "cross", "client": "client"}[server_type]
db_game_name = f"{prefix}_{type_prefix}_s{server_id}"
db_log_name = f"{prefix}_{type_prefix}_log_s{server_id}" if server_type != "login" else ""
diff_config = {
"prefix": prefix,
"server_id": str(server_id),
"server_type": server_type_str,
"ip": ip,
"db_host": db_host,
"db_user": db_user,
"db_pass": db_pass,
"db_port": str(db_port),
"log_dir": "log",
"db_game_name": db_game_name,
}
类型字段:
- game:写 server_name、game_host、tcp_port、http_port、open_time、role_log=run/<server_dir>/log。
- login:写 tcp_port。
- center:写 open_time。
- cross:写 open_time。
- client:写 game_host、tcp_port、game_tcp_port、login_host、login_http_port。
- game/client/center 有 login_node 时写 login_node。
- game/cross 有 center_node 时写 center_node。
create_server 必须:
- 从 read_merged_config(server_root) 取 ip,取不到用 get_local_ip。
- run_dir 为空时使用 Path(server_root) / "run"。
- 创建 <run_dir>/<server_dir>/config。
- 如果 kv.config 已存在且 overwrite=False,返回 False。
- 写入 kv.config。
- 调 generate_start_config(server_root, server_dir)。
- 返回 (True, "服务器 <server_dir> 创建成功", merged_config) 或 (False, 错误, None)。
9. server_manager_cli.py 核心代码
必须实现 class ServerManagerCLI
- __init__(self, server_root: str = None, config_path: str = None)
- _read_tool_config_value(self, detected_root: str, key: str) -> str
- _read_configured_server_root(self, detected_root: str) -> str
- _read_configured_run_dir(self, detected_root: str) -> str
- _detect_server_root(self) -> str
- _find_server_root(self) -> Path
- _load_config(self, config_path: str = None)
- list_servers(self) -> dict
- create_server_cmd(...)
- start_server(self, server_name: str, use_rebar: bool = True, foreground: bool = False) -> bool
- stop_server(self, server_name: str) -> bool
- connect_server(self, server_name: str, target_ip: str = None) -> bool
- regenerate_config(self, server_name: str) -> bool
- compile(self, target: str = "all", server_type: str = "game") -> bool
- interactive_menu(self)
CLI 初始化行为:
- detected_root = _detect_server_root()
- configured_root = _read_configured_server_root(detected_root)
- server_root 优先级:参数 server_root > configured_root > detected_root
- run_dir 优先级:tool.config 的 run_dir > server_root/run
- config_path = get_server_manager_config_dir(server_root)
- 加载配置后设置 prefix、db_host、db_port、db_user、db_pass、cookie。
- 执行 run_startup_migrations(server_root)。
compile 命令格式:
- all:依次 code、proto、table、tbllog。
- code<rebar3> as game_server_dev compile 或 login_server_dev compile。
- proto<rebar3> protobuf compile -a game_server。
- table<rebar3> cache compile -a game_server。
- tbllog<rebar3> tbllog compile。
10. server_manager_gui.py Config 核心代码
必须实现 _empty_config_data 与 class Config。
_empty_config_data 返回结构:
{
"workspace": {"server_root": "", "run_dir": ""},
"database": {"host": "", "user": "", "password": "", "port": 3306},
"server": {
"prefix": "ddxq2",
"login_server_node": "",
"center_server_node": "",
"default_server_id": "1",
"server_type": "game_server",
"server_name": "",
"ip": get_local_ip(),
"game_host": get_local_ip(),
"cookie": "ddxq2-node",
},
"erlang": {"r25_path": "", "erl": "erl", "werl": "werl", "escript": "escript"},
"login_db": {"name": "", "server_id": 900, "host": "", "port": 0, "user": "", "password": ""},
"commands": {"rebar": REBAR3_CMD, "svn": "svn"},
}
Config 必须:
- __init__(project_root=None):无项目时 config_error="未打开项目"。
- open_project(project_root):调用 ensure_and_get_server_manager_config_dir;成功后设置 tool_dir、tool_config_path、data。
- _load_tool_config:读 key=value。
- _save_tool_config:按固定分组写 tool.config。
- _load_default_kv(server_root):读 .server_manager/config/default.kv。
- _load_configtool.config 为空时从 default.kv 补齐;首次运行自动创建 run_dir;需要保存补齐后的 tool.config。
- save:写 tool.config 前刷新 ip/game_host 为 get_local_ip()。
- get/set 支持嵌套 key。
- has_project/is_config_complete/is_configured/get_missing_config。
11. server_manager_gui.py GUI 类结构
必须保留这些核心类和职责:
- NoWheelSpinBox、NoWheelComboBox、NoWheelDateTimeEdit:禁用滚轮误操作。
- ServerIdTableItem、NumericTableWidgetItem:表格数字排序。
- CopyableIpLabel:点击复制 IP。
- CommandRunner(QThread):执行 shell 命令,分批发 output_signal,结束发 finished_signal。
- NodeStatusCheckWorker(QThread):批量检查节点状态。
- GroupedNodeStatusCheckWorker(QThread):按 cookie 分组检查节点状态。
- StopServerWorker(QThread):停止节点、轮询离线、关闭本工具启动的窗口。
- OutputConsole(QTextEdit):彩色追加输出,支持独立 ConsoleWindow。
- CommandTab:编译、启动、remsh、停止、清档、查看账号、SVN。
- ClearDatabaseDialog/ClearDatabaseThread:清档。
- ViewAccountsDialog:查账号。
- ServerSelectDialog:本地服务器选择,含状态刷新。
- RemoteConnectDialog:本地/远程节点连接选择。
- KvConfigEditDialogkv.config 编辑与 sys.config 重新生成。
- CreateServerTab:创建服务器。
- ServerManageTab:本地/远程服务器管理。
- ConfigTab:工具设置。
- DefaultKvTabdefault.kv 编辑。
- UpdateCheckWorker/UpdateDownloadWorker:自动更新后台工作线程。
- WelcomePage:无项目欢迎页。
- MainWindow:主窗口、菜单、欢迎页/项目页切换、更新检查、项目迁移、状态缓存。
MainWindow 必须:
- 初始化深色样式。
- 使用 QStackedWidget 管理 WelcomePage 与项目内容。
- 菜单栏 setNativeMenuBar(False)。
- 文件菜单含打开项目、最近的项目、关闭项目、退出。
- 帮助菜单含检查更新、关于。
- 项目内容使用 QSplitter 垂直分割主标签页与控制台。
- 主标签页添加:控制台、创建服务器、服务器管理、日志查看、工具设置。
- 切到服务器管理页时从缓存刷新状态;切到日志查看页时刷新服务器列表。
- closeEvent 中停止还在运行的 QThread,避免退出崩溃。
server_manager_gui.py main() 行为:
- 调 clean_up_old_version。
- 抑制 PyInstaller 退出时的临时目录告警。
- 如果 len(sys.argv) > 1,调用 _run_cli_from_argv(),将参数交给 server_manager_cli.main。
- 无参数创建 QApplication、配置 frozen Qt 运行时、显示 MainWindow。
12. log_viewer_pane.py 与 rg_search.py 核心代码
rg_search.py 必须提供:
- find_rg_executable() -> Optional[Path]
- rg_match_spans_qt(full_text, pattern, case_sensitive, use_regex, whole_word, max_spans=3000)
- compile_python_span_pattern(pattern, case_sensitive, use_regex, whole_word)
- rg_filter_lines(full_text, pattern, case_sensitive, use_regex, whole_word)
- rg_search_directory(search_root, pattern, case_sensitive, use_regex, whole_word, glob_globs=None)
find_rg_executable 查找顺序:
- RG_PATH 环境变量。
- 打包后 main.exe 同目录的 rg.exe/rg。
- 源码仓库根目录的 rg.exe/rg。
- PATH 中的 rg。
rg_match_spans_qt 要点:
- 调 rg --json --color never。
- 非大小写敏感加 -i。
- use_regex=False 用 --fixed-stringswhole_word=True 加 -w。
- use_regex=True 且 whole_word=True 时 pattern 包装为 \b(?:pattern)\b。
- 从 rg JSON 的 absolute_offset 和 submatches start/end 得到 UTF-8 byte offsets。
- 将 UTF-8 byte offsets 转成 Qt QTextCursor 使用的 UTF-16 偏移:
qt_pos = len(prefix.encode("utf-16-le")) // 2。
LogViewerPane 必须:
- 保存 self._full_text、self._current_match_range、spans cache。
- UI 包含搜索输入、区分大小写、仅显示匹配行、正则表达、全词匹配、上一个、下一个、rg 提示、只读 QPlainTextEdit。
- set_full_text(text, reset_search=True) 设置全文并可重置搜索选项。
- _apply_filter_or_full:勾选仅显示匹配行时优先 rg_filter_lines,失败回退 Python re。
- _match_spans_in_plain:优先 rg_match_spans_qt,失败回退 Python re,最多 3000 个匹配。
- find_next/find_prev:循环跳转匹配。
- _highlight_matches:当前匹配用亮色,其余匹配用暗黄色。
- 快捷键 n、Shift+N、g、Shift+G、/、F3、Shift+F3、Ctrl+F。
13. log_viewer_tab.py 核心代码
必须提供:
- _run_dir_from_config(config) -> str
- _refresh_server_combo(combo, config) -> bool
- class LogLoadTab
- class LogSearchTab
- class LogViewerTab
LogLoadTab 行为:
- 预设日志名:error.log、debug.log、http_info.log、http_error.log、info.log、http.log。
- 两种模式:服务器日志、玩家协议(tag_log)。
- 玩家协议文件名 network_codec-<role_id>.logrole_id 必须纯数字。
- 日期归档文件名 <base>.YYYYMMDD。
- load_log 调 resolve_log_file_path 和 read_text_file_best_effort;成功显示到内嵌 QPlainTextEdit,并可打开 LogViewerStandaloneDialog。
- open_log_directory Windows 用 os.startfile,其他平台用 xdg-open。
- _prompt_trace 只允许 game 类型服务器,输入 role_id 后调 rpc_role_gs_trace_network。
LogSearchTab 行为:
- 必须检测 rg,找不到时 QMessageBox 提示。
- 搜索根目录为 <run_dir>/<server_dir>/log。
- 调 rg_search_directory;结果用 LogViewerStandaloneDialog 打开。
- 与 LogLoadTab 的服务器下拉互相同步。
14. updater.py 与 mini_updater.py 核心代码
src/updater.py 必须实现:
- _version_json_path:打包后优先 main.exe 同目录 version.json,再 sys._MEIPASS/version.json;源码运行用项目根 version.json。
- get_current_version:读取 version 字段,失败返回 0.0.0。
- get_version_info:读取完整 JSON,失败返回 {"version": "0.0.0", "release_notes": ""}。
- _parse_version/version_less:将版本字符串转 int tuple 比较。
- fetch_update_manifestHTTP GET update_url,兼容 full_installer_url 与旧 download_url。
- get_delta_for_current:从 manifest["delta_updates"][current_version] 取 patch_url/new_exe_sha256。
- download_filerequests stream 下载,progress_callback(percent)。
- apply_delta_patchbsdiff4.patch 当前 exe + patch,校验 sha256,写 TEMP/ServerManager_new.exe,调用 apply_update_and_restart。
- run_installer_and_exitWindows 下静默参数启动安装包,frozen 时 os._exit(0)。
mini_updater.py 必须:
- argparse 参数 --pid、--install-dir、--new-exe-path、--target-exe-name。
- is_process_alive Windows 用 ctypes OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION)Unix 用 os.kill(pid, 0)。
- 每 0.5 秒等待主进程退出。
- 删除旧的 target.exe.old。
- os.rename(target.exe, target.exe.old)。
- shutil.move(new_exe_path, target.exe)。
- subprocess.Popen([target_exe_full], cwd=install_dir, close_fds=True, creationflags=DETACHED_PROCESS)。
- 错误写入 install_dir/logs/update_error.log。
15. release.py 核心代码
必须提供:
- read_text/write_text
- read_version_from_json
- get_current_version:只以 output/version.json 为准。
- set_version_pre(new_version, release_notes=None) -> str
- set_version_post(old_exe_path=None) -> None
- _copy_version_to_output()
set_version_pre 必须:
- 读取根目录 version.json。
- old_version 从 output/version.json 读取,读不到直接失败。
- 正则更新 build_installer.iss 中 #define MyAppVersion。
- 正则更新 main.py 中 LOCAL_VERSION。
- version.json 设置 version、release_notes。
- full_installer_url 固定指向 ServerManager_Setup.exe。
- delta_updates 只保留 old_version -> new_version 一项,patch_url 格式为 output/patches/v<old>_to_v<new>.patch。
- 如果 src/dist/main/_internal/version.json 或 src/dist/main/version.json 存在,则同步复制。
set_version_post 必须:
- 找新 exesrc/dist/main/main.exe,找不到再找 src/dist/main.exe。
- 没有 old_exe_path 时只复制 version.json 到 output。
- 有 old_exe_path 且 bsdiff4 可用时生成 output/patches/v<old>_to_v<new>.patch。
- 计算新 exe sha256,填回 version.json 的 delta_updates[old_version].new_exe_sha256。
- 最后复制 version.json 到 output。
16. server_commands.py 新窗口启动与窗口追踪细则
必须提供:
- run_cmd_in_new_window(cmd: str, working_dir: str, window_title: str = "Server", erl_path: str = None) -> str
- run_cmd_in_terminal_linux(cmd: str, working_dir: str, window_title: str = "Server", erl_path: str = None) -> str
- register_launched_window(server_name: str, kind: str, window_title: str, launch_info: str = "") -> None
- get_launched_windows(server_name: str) -> List[Dict[str, Any]]
- close_launched_windows(server_name: str) -> List[Tuple[Dict[str, Any], bool, str]]
- run_cmd_foreground(cmd: str, working_dir: str, window_title: str = "Server") -> subprocess.Popen
Windows 新窗口启动必须这样实现:
- 创建临时 .bat 文件。
- bat 内容包含:
- `@echo off`
- `title <window_title>`
- `cd /d "<working_dir>"`
- `set ESCRIPT_EMULATOR=erl`
- 打印命令分隔线和命令内容
- 执行原始 cmd
- 使用 `subprocess.Popen(["cmd", "/k", bat_path], creationflags=0x00000010, cwd=working_dir)` 启动新控制台。
- 返回 `winpid:<pid>`,用于后续精确关闭窗口。
- 如果失败,兜底使用 `start cmd /k "<bat_path>"`,返回 bat_path。
Linux 新窗口/后台启动:
- 创建临时 .sh,内容包含 cd、`export ESCRIPT_EMULATOR=erl`、打印命令、执行 cmd。
- chmod 755。
- 如果有 screen`screen -dmS <session> bash <script>`,返回 `screen:<session>`。
- 否则如果有 tmux`tmux new-session -d -s <session> "bash <script>"`,返回 `tmux:<session>`。
- 否则 `subprocess.Popen(["bash", script_path], start_new_session=True)`,返回 `background:<script_path>`。
- session 名用 window_title 替换空格和点为下划线。
窗口追踪:
- 全局变量 `_launched_windows: Dict[str, List[Dict[str, Any]]] = {}`。
- 使用 threading.Lock 保护。
- key 为服务器基础名,即 `server_name.split("@", 1)[0]`。
- entry 结构:
`{ "kind": "server" 或 "remsh", "title": window_title, "launch_info": launch_info }`
- 启动服务窗口后调用:
`register_launched_window(server, "server", f"SERVER {server}", launch_info)`
- 启动 remsh 后调用:
`register_launched_window(target_node, "remsh", f"REMSH {target_node}", launch_info)`
Windows 关闭窗口:
- launch_info 以 `winpid:` 开头时优先 `taskkill /F /T /PID <pid>`。
- 如果 PID 关闭失败,再按标题关闭。
- 标题关闭用 `taskkill /F /T /FI "WINDOWTITLE eq <title>*"`。
- 需要尝试标题变体:
- `<title>`
- `管理员: <title>`
- `管理员:<title>`
- `Administrator: <title>`
- 找不到窗口可视为成功,提示“可能已关闭”。
Linux 关闭窗口:
- `screen:<session>` -> `screen -X -S <session> quit`
- `tmux:<session>` -> `tmux kill-session -t <session>`
- `background:*` -> 返回成功并说明无独立控制台,跳过
17. CommandRunner、StopServerWorker 与状态线程细则
CommandRunner(QThread) 必须:
- signals:
- output_signal = pyqtSignal(str)
- finished_signal = pyqtSignal(int)
- 构造参数:command、cwd=None、shell=True、start_new_window=False。
- 启动时输出:
- `[HH:MM:SS] 执行命令: <command>`
- `[HH:MM:SS] 工作目录: <cwd>`
- start_new_window=True 时,启动新窗口后立即 finished_signal(0)。
- 普通执行使用 subprocess.Popenstdout=PIPEstderr=STDOUTtext=Trueencoding="utf-8"errors="replace"。
- 为避免 GUI 卡顿,输出需要攒批:
- 最多每 0.15 秒 emit 一次。
- 或累积 80 行 emit 一次。
- 结束后输出退出码。
- stop() 调 terminate()。
NodeStatusCheckWorker 必须:
- 构造参数:server_names、cookie、target_ip=None、erl_path=None。
- run() 调 check_nodes_status,然后 emit dict。
GroupedNodeStatusCheckWorker 必须:
- 构造参数:cookie_groups: Dict[str, List[str]]、erl_path=None。
- run() 逐组调用 check_nodes_status,并合并结果。
StopServerWorker 必须:
- signals:
- progress_signal = pyqtSignal(str)
- finished_signal = pyqtSignal(bool, str)
- 构造参数:
- server_name
- cookie
- erl_path=None
- poll_timeout_sec=30
- poll_interval_sec=1.0
- 流程:
1. build_stop_command。
2. 用 subprocess_args_to_display 输出完整命令。
3. subprocess.run(args, capture_output=True, text=True, timeout=15)。
4. 即使命令非 0、超时、异常,也继续等待节点离线。
5. 在 poll_timeout_sec 内每 poll_interval_sec 调 check_node_status。
6. 超时仍在线则 finished_signal(False, "节点 ... 仍未离线")。
7. 离线后调用 close_launched_windows(server)。
8. 逐条输出关闭结果:服务进程/remsh、成功或失败。
9. finished_signal(True, "服务器 ... 已停止")。
18. OutputConsole 与独立控制台细则
ConsoleWindow
- QDialog,标题“输出控制台”,最小 800x600。
- 内部 QTextEdit 只读,字体 Consolas 11。
- document().setMaximumBlockCount(4000)。
- 底部按钮:清空、关闭。
- append_output(text) 用 QTextCursor 追加,不用 insertHtml。
OutputConsole(QTextEdit)
- 只读,字体 Consolas。
- 最大 block 数限制,防止长时间运行撑爆内存。
- append_output(text) 根据文本内容选择颜色:
- 包含 `[错误]`、`失败`、`ERROR` 使用红色。
- 包含 `[警告]`、`WARN`、`超时` 使用黄色。
- 包含 `[成功]`、`完成`、`在线` 使用绿色。
- 普通输出使用浅色。
- 追加输出后滚动到底部。
- 如果独立 ConsoleWindow 已打开,同步追加到独立窗口。
- open_window() 打开独立窗口并同步当前内容。
- clear_output() 同时清空内嵌和独立窗口。
19. ClearDatabaseDialog 与 ViewAccountsDialog 细则
ClearDatabaseDialog 必须:
- 标题“服务器清档”,最小 500x400。
- 顶部红色危险提示:“清档将删除游戏数据库和日志数据库,此操作不可恢复”。
- 列表按 get_server_list(run_dir) 分类展示。
- 需要输入 `DELETE` 才能继续。
- 点击执行后再次 QMessageBox.critical 二次确认,说明步骤:
1. 关闭服务器
2. 删除游戏数据库
3. 删除日志数据库
- 读取目标服务器配置:
- server_root、run_dir
- kv_config_file = Path(run_dir) / server_name / "config" / "kv.config"
- merged_cfg = read_merged_config(server_root, server_name)
- db_host、db_port、db_user、db_pass、db_game_name、db_log_name
- db_game_name 为空时禁止清档。
- 构建 stop_cmd = build_stop_command(server_name, cookie, erl_path=erl_path)。
- 启动 ClearDatabaseThread,线程运行期间禁用对话框。
ClearDatabaseThread 必须:
- signals:
- finished_signal(list)
- error_signal(str)
- status_signal(str)
- run() 流程:
1. status “正在停止服务器...”
2. subprocess.run(stop_cmd, timeout=10, stdout=DEVNULL, stderr=DEVNULL)
3. sleep 3 秒。
4. status “正在删除数据库...”
5. pymysql.connect(host, port, user, password, charset="utf8mb4")
6. `DROP DATABASE IF EXISTS \`<db_game>\``
7. db_log 非空时同样 DROP。
8. commit、close。
9. emit 删除的库列表。
- 捕获异常并 error_signal(str(e))。
ViewAccountsDialog 必须:
- 标题“查看服务器账号”,最小 900x600。
- 顶部服务器下拉 + 查询按钮。
- 搜索框,placeholder “输入角色名、角色ID或账号进行搜索...”。
- 表格 4 列:角色ID、角色名、账号、等级。
- 表格只读、按行选择、列头可排序。
- 角色ID和等级使用 NumericTableWidgetItem 保证数字排序。
- 查询时读取合并配置中的 db_game_name,连接对应数据库。
- 无搜索:
`SELECT role_id, role_name, fn_uid, level FROM role ORDER BY level DESC, role_id LIMIT 500`
- 有搜索:
`WHERE role_name LIKE %s OR CAST(role_id AS CHAR) LIKE %s OR fn_uid LIKE %s`
参数均为 `%search_text%`。
- 状态栏显示记录数;达到 500 条时提示“结果已截断”。
20. ServerSelectDialog 与 RemoteConnectDialog 细则
ServerSelectDialog 必须:
- 标题由调用方传入,如“运行服务器”“快速启动”“停止服务器”。
- 顶部有“刷新状态”按钮和状态文本。
- 列表按游戏、跨服、登录、客户端、中心、其他分组。
- 每个服务器项 data(UserRole)=server_name。
- 状态图标从 NodeStatusCache 读取:
- True -> `🟢`
- False/None -> `⚪`
- 如果有服务器但缓存没有这些本地节点的数据,自动禁用刷新按钮并启动异步状态刷新。
- 双击服务器等价于确定。
- accept() 设置 self.selected_server 后关闭。
RemoteConnectDialog 必须:
- 标题“远程连接”,最小 550x550。
- 输入项:
- 节点名称,placeholder `ai002_game_server_s100@192.168.1.1`
- 目标 IP,默认 get_local_ip;节点已包含 @ip 时忽略。
- Cookie,优先 config.server.cookie,不存在则 read_merged_config(server_root).cookie,默认 ddxq2-node。
- 下方 QTabWidget
- 本地服务器
- 远程服务器
- 本地服务器页:
- 从 run_dir 扫描 get_server_list。
- 若状态缓存缺失,可同步或异步批量 check_nodes_status。
- 点击本地项填充 node_input=server、ip_input=get_local_ip。
- 双击直接 accept。
- 远程服务器页:
- “从登录服加载”按钮。
- 搜索框过滤 server_id、server_name、server_node。
- QTableWidget 3 列:服务器ID、服务器名称、服务器节点。
- 服务器ID列用 ServerIdTableItem,支持数字排序。
- 点击远程项填充 node_input=server_node,并从 @ 后提取 IP。
- 双击直接 accept。
- 加载远程列表前必须检查 login_node 是否配置,并 check_node_status(login_node, cookie)。
- 登录服不可达时弹窗提示检查节点名、Cookie、Erlang 路径、网络与防火墙。
- accept() 必须先 check_node_status(node, cookie, target_ip);离线则拒绝连接并提示;在线则设置 node_name、target_ip、cookie。
21. KvConfigEditDialog 细则
KvConfigEditDialog 必须:
- 构造参数:
- kv_config_path
- server_type
- server_name
- server_root
- console
- parent=None
- BOOL_CONFIG_KEYS = {"auto_reload", "is_develop", "is_inner", "gm_auth"}。
- allowed_keys 从 get_template_config_keys(server_root, server_type) 动态读取。
- 标题:`编辑配置 - <server_name>`,最小 600x500。
- UI
- 当前配置 QGroupBox + QFormLayout。
- 添加配置项 QGroupBoxNoWheelComboBox + 添加按钮。
- 底部按钮:保存配置、关闭。
load_config
- read_config_file(kv_config_path)。
- 如果缺少 auto_reload,自动加入智能默认值。
- 清空旧表单。
- 每个 key/value 调 _add_config_row。
- 调 _update_add_combo。
_add_config_row
- required_keys = get_required_keys(server_type, server_root)。
- 必须项 label 前加 `* `,且不显示删除按钮。
- label 优先显示 allowed_keys[key],否则显示 key。
- value 编辑器由 _create_config_editor 生成。
- 非必须项右侧有删除按钮。
_create_config_editor
- BOOL_CONFIG_KEYS 使用 QCheckBox("启用")true 勾选。
- 其他使用 QLineEdit。
_get_smart_default_value
- log_dir -> `run/<server_name>/log`
- role_log -> `run/<server_name>/log`
- auto_reload -> read_merged_config(server_root).get("auto_reload", "true")
- ip/game_host -> get_local_ip()
- login_host -> node_host_only(read_merged_config(server_root).get("login_host")) or get_local_ip()
- server_name -> 从目录名 `<prefix>_..._s<id>` 推出 `<prefix>_<id>`
- tcp_port -> 18000 + id
- http_port -> 19000 + id
- game_tcp_port -> 18000 + id
- 其他 -> read_merged_config(server_root).get(key, "")
save_config
- 收集所有 widget 当前值。
- write_config_file(kv_config_path, config)。
- 输出 `[成功] 配置已保存`。
- 自动 generate_start_config(server_root, server_name)。
- 成功提示“配置已保存并重新生成 sys.config”,失败提示“配置已保存,但重新生成 sys.config 失败”。
22. UpdateCheckWorker、UpdateDownloadWorker 与 MainWindow 更新 UI 细则
UpdateCheckWorker
- 构造参数 update_url。
- run() 调 fetch_update_manifest(update_url)emit manifest 或 None。
UpdateDownloadWorker
- signals:
- progress_signal(int)
- finished_signal(object, bool, object)
- 构造参数:
- download_url
- dest_path
- is_delta=False
- expected_sha256=None
- run() 调 updater_download_file(download_url, dest_path, progress_callback=on_progress)。
- finished_signal(dest_path if ok else None, is_delta, expected_sha256)。
MainWindow 启动检查:
- showEvent 首次触发后 QTimer.singleShot(1500, _start_startup_update_check)。
- _start_startup_update_check 读取 get_version_info()["update_url"],为空则跳过。
- 后台 worker 完成后,如果 manifest.version 与当前版本不同,则弹 QMessageBox.question。
- 有 delta 且 patch_url/new_exe_sha256 完整时下载增量;否则下载 full_installer_url/download_url。
手动检查更新:
- 帮助菜单“检查更新”调用 _on_check_update。
- fetch_update_manifest 失败时展示网络/地址排查提示,并显示当前 update_url。
- 当前版本不低于远端时提示“当前已是最新版本”。
- 发现新版本时,如果有增量和全量,QMessageBox 自定义按钮:
- 取消
- 立即更新(增量)
- 立即更新(全量)
- 如果只有全量,则按钮“立即更新”。
下载 UI
- _start_update_download 创建 tempdir/ServerManager_Update。
- 增量目标文件名 `ServerManager_patch<suffix>`;全量目标 `ServerManager_Setup<suffix>`。
- QProgressDialog("正在下载更新...", "取消", 0, 100)。
- 取消时 terminate worker 并关闭进度框。
- 下载失败弹“下载失败,请稍后重试或手动下载”。
- 增量下载完成:
1. 提示“增量包已下载,即将退出并重启以应用补丁。”
2. 调 apply_delta_patch(path, expected_sha256)。
3. 如果返回失败,弹窗说明原因,并提供“全量更新”按钮;点击后下载 full_url。
- 全量下载完成:
1. 提示“更新包已下载,即将退出并重启以完成安装。”
2. run_installer_and_exit(path, silent=True)。
23. server_manager_gui.py 入口与 frozen 运行细则
必须实现:
- _suppress_pyinstaller_warnings()
- _run_cli_from_argv()
- _configure_frozen_qt_runtime()
- main()
_suppress_pyinstaller_warnings
- 仅 frozen 时执行。
- 设置:
- PYINSTALLER_SUPPRESS_WARNINGS=1
- _MEIPASS2=sys._MEIPASS
- Windows 下调用 SetErrorMode,组合:
- SEM_FAILCRITICALERRORS
- SEM_NOGPFAULTERRORBOX
- SEM_NOALIGNMENTFAULTEXCEPT
- SEM_NOOPENFILEERRORBOX
- 注册 atexit 处理器,frozen 时 os._exit(0),减少 PyInstaller 临时目录清理告警。
_configure_frozen_qt_runtime
- frozen 时定位 base_dir = sys._MEIPASS 或 sys.executable.parent。
- qt_root = base_dir / "PyQt6" / "Qt6"。
- 如果 qt_root/bin 存在,加入 PATH 前面。
- 如果 qt_root/plugins 存在,设置 QT_PLUGIN_PATH。
- 如果 qt_root/plugins/platforms 存在,设置 QT_QPA_PLATFORM_PLUGIN_PATH。
main
- Windows frozen GUI 模式隐藏控制台窗口,但不能 FreeConsole。
- 调 clean_up_old_version。
- 调 _suppress_pyinstaller_warnings。
- 调 _configure_frozen_qt_runtime。
- 创建 QApplicationstyle=Fusion。
- 图标查找顺序:
- frozen: sys._MEIPASS/icon/icon.png
- 源码: cwd/icon/icon.png
- cwd/src/icon/icon.png
- Path(__file__).parent/icon/icon.png
- 自动打开最近项目第一个;失败则欢迎页。
- 显示 MainWindow。
- app.exec() 结束后 frozen 用 os._exit(exit_code),源码用 sys.exit(exit_code)。
`if __name__ == "__main__"`
- `len(sys.argv) > 1`:调用 _run_cli_from_argv。
- 否则:main()。
24. README 与 RELEASE 文档细则
README 必须覆盖:
- 项目简介:Windows GUI + Linux CLI。
- 功能特性:控制台、创建服务器、服务器管理、日志查看、工具设置、自动更新。
- 打包与发版维护者流程:先 pack_local_test,再 release/pack_all。
- rg.exe 放置说明:根目录与 src 同级;build.bat 会复制到 dist/main。
- 安装说明:Windows 安装包、源码运行 Windows/Linux。
- 快速上手:打开项目时选择服务器项目根目录,不要选 run/config/.server_manager/config。
- 首次打开项目会复制模板到 .server_manager/config。
- 重点路径:服务器根目录、运行目录、Erlang 路径。
- CLI 用法表:main.exe/main 无参数 GUI,带参数 CLI。
- 命令行参数完整说明。
- version.json 字段说明、download/update URL 规则。
- 配置说明:项目级配置、单服配置、模板来源。
- 文件结构。
- 系统要求。
- Linux 后台运行 screen/tmux 说明。
- 注意事项与版本历史。
RELEASE 必须覆盖:
- 推荐流程:本地测试包 -> 本地安装验证 -> 正式发版。
- pack_local_test 不改版本、不覆盖 output。
- release.bat 顺序:PyInstaller -> 改版本 -> Inno -> post-build。
- 本地不保存 main_*.exe;若要增量,需手动提供上一版 main.exe。
- 手动发版命令:
- cd src
- build.bat
- cd ..
- python release.py 1.0.x --notes "说明"
- ISCC build_installer.iss
- python release.py --post-build
- 增量补丁生成说明:bsdiff4.diff(old, new)、sha256 填回 version.json。
- 发布 output 目录包含 ServerManager_Setup.exe、version.json、patches/*.patch。
25. 节点状态批量检测算法细则
需要同时实现两类状态检测:
- 严格检测:`check_node_status` / `_check_nodes_status_via_ping`,通过 Erlang `net_adm:ping`,会校验 cookie 和分布式握手。
- 快速批量检测:`check_nodes_status`,通过 epmd 查询已注册节点名,用于服务器列表状态展示,速度优先,不校验 cookie。
常量必须保留:
- `_NODE_STATUS_BATCH_SIZE = 40`
- `_NODE_STATUS_MAX_EVAL_CHARS = 8000`
- `_NODE_STATUS_MAX_WORKERS = 4`
- `_NODE_STATUS_FAST_TIMEOUT = 0.8`
- `_NODE_STATUS_FAST_MAX_WORKERS = 48`
- `_EPMD_PORT = 4369`
严格检测批次拆分 `_split_node_status_batches(target_nodes)`
- 按节点数量和 eval 字符长度拆分。
- 单个节点估算长度为 `len(target_node) + 4`。
- 当前批次达到 40 个,或累计字符数超过 8000 时切新批次。
`_check_nodes_status_batch` 必须生成如下 Erlang eval 思路:
- Nodes = ['node1@ip','node2@ip',...]
- Parent = self()
- Collector 递归 receive `{node_status, N, R}`。
- 每个节点 spawn 一个进程执行 `net_adm:ping(N)`。
- 8 秒超时,剩余节点标为 pang。
- 输出格式:`node1@ip::pong,node2@ip::pang`
- Python 解析 `::`status == "pong" 为在线。
- 一个完整节点名可能对应多个原始输入名,需要 target_mapping 映射回原始 server_name。
- 同时建立 short_name_mapping,兼容 Erlang 输出里短名和全名不完全一致的情况。
`_check_nodes_status_via_ping` 流程:
- results 初始值为所有输入 False。
- 输入包含 @ 时直接作为 target_node;否则拼成 `<server_name>@<target_ip or local_ip>`。
- cookie 为空时默认 ddxq2-node。
- 调 `_ensure_epmd_daemon(erl_path)`。
- 单批次直接执行;多批次用 ThreadPoolExecutormax_workers 不超过 4。
- 每个 subprocess.run 使用 creationflags=CREATE_NO_WINDOWWindows)并设置 timeout。
epmd 快速查询 `_query_epmd_registered_names(host)`
- socket.create_connection((host, 4369), timeout=0.8)。
- 发送 `struct.pack(">HB", 1, ord("n"))`,即 epmd names request。
- 读取所有响应 chunk。
- 响应前 4 字节为 epmd 返回头,可丢弃。
- latin-1 解码,逐行匹配正则 `\bname\s+([^\s]+)\s+at\s+port\b`。
- 返回注册的短节点名列表。
`check_nodes_status` 快速模式:
- 将输入按 host 分组。
- 输入含 @`short_name, host = node.split("@", 1)`。
- 输入不含 @host 使用 target_ip 或 get_local_ip()。
- host_mapping 结构:`{host: {short_name: [original_names]}}`。
- ThreadPoolExecutor 并发查询每个 hostmax_workers 不超过 48。
- registered_names 中包含 short_name 则对应所有 original_names 为 True。
注意:
- GUI 列表状态用 `check_nodes_status`,快。
- 连接/停止前的确认用 `check_node_status`,严谨。
- NodeStatusCache 的值只缓存展示状态,不应作为最终连接成功的唯一依据。
26. 登录服 RPC 远程列表脚本细则
`_erl_quoted_atom(node)`
- 输入为空时 raise ValueError("登录服节点为空")。
- 返回单引号 Erlang atom 字面量。
- 需要转义反斜杠和单引号:
`return "'" + n.replace("\\", "\\\\").replace("'", "\\'") + "'"`
`_FETCH_REMOTE_RPC_SCRIPT` 必须按如下协议输出:
- 用 `LN = __LOGIN_ATOM__` 注入登录服节点。
- 执行:
`rpc:call(LN, erlang, apply, [fun() -> ... end, []])`
- 远端 fun 内部调用:
`ms_cache:tab2list_foldl(server_temp_info, Fun, [])`
- 每个 OneServer
- ServerId = element(2, server_temp_info_c:get_server_id(OneServer))
- ServerName = unicode:characters_to_binary([element(2, server_temp_info_c:get_server_name(OneServer))])
- 通过 `server_info_lib:get_server_node(ServerId)` 获取 Node。
- 返回列表元素 `{ServerId, ServerNameBin, Node}`。
- badrpc 时输出 `RPC_ERROR: <Err>` 并 halt(2)。
- 正常时先输出 `SM_COUNT\t<length>`。
- 每行输出:
`<ServerId>\t<base64(server_name_utf8)>\t<NodeStr>`
- NodeStr 需要兼容 atom/list/binary/其他 term。
`query_remote_servers_from_login_rpc` 细节:
- cookie 不能为空;为空默认 ddxq2-node。
- cookie 不允许包含空白或单双引号;否则 ValueError。
- 本机临时节点名:`sm_ls_<timestamp_mod>`。
- 本地 host_part 尝试顺序:
1. get_local_ip()
2. 127.0.0.1
- 这样做是为了某些机器用局域网 IP 启动分布式节点失败时,回退 localhost。
- 将 `_FETCH_REMOTE_RPC_SCRIPT.replace("__LOGIN_ATOM__", ln)` 写入 NamedTemporaryFilesuffix=".erl"UTF-8newline="\n"。
- 路径传给 Erlang 前替换反斜杠为 `/`。
- eval 启动代码:
`case file:script("<tmp_path>") of {error, E} -> io:format("SCRIPT_ERROR: ~p~n", [E]), halt(1); _ -> halt(0) end.`
- subprocess.run 参数:
`[erl, "-noshell", "-name", f"{ping_node}@{host_part}", "-setcookie", cookie_arg, "-eval", eval_launch]`
- capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=120。
- Windows 设置 CREATE_NO_WINDOW。
- finally 删除临时 .erl。
错误处理:
- TimeoutExpired -> `从登录服加载超时(120s`。
- FileNotFoundError -> 提示找不到 erl,并要求安装 Erlang 或配置 Erlang 路径。
- returncode 非 0 且 stderr/stdout 包含 nodistribution、failed_to_start_child/net_kernel、Kernel pid terminated 等,且还有下一个 host_part 时继续重试。
- combined_text = stdout + "\n" + stderrWindows 下 -noshell 的 io:format 可能落 stderr。
- combined_text 中有 RPC_ERROR 行时 raise `登录服 RPC 失败: ...`。
- returncode 非 0 时 raise `erl 退出码 ...`。
解析:
- 跳过空行、RPC_ERROR、SCRIPT_ERROR、SM_COUNT、Eshell、Erlang/OTP。
- 每行按 tab split,最多 3 段。
- base64 解码 server_nameutf-8 解码,失败时 errors="replace",再失败则空字符串。
- 返回:
`{ "server_id": str, "server_name": str, "server_node": str, "running": False }`
- 如果 SM_COUNT > 0 但没有解析出任何服务器,raise 明确错误并附 stdout 前 500 字,便于排查编码/输出分流。
27. remsh、前台命令与 Erlang 工具命令细则
`build_remsh_command` 必须:
- local_ip = get_local_ip()。
- remsh_host = local_host or local_ip。
- erl = get_erl_cmd(erl_path)。
- server_name 含 @ 时 target_node=server_name。
- 不含 @ 时 target_node=f"{server_name}@{target_ip or remsh_host}"。
- timestamp = int(time.time()) % 10000。
- 返回:
`"<erl>" +P 1024000 -name remsh_<timestamp>@<remsh_host> -setcookie <cookie> -remsh <target_node>`
`run_cmd_foreground` 必须:
- 打印分隔线、窗口名、工作目录、命令。
- 设置 env = os.environ.copy()env["ESCRIPT_EMULATOR"] = "erl"。
- Windows 使用 `subprocess.Popen(cmd, shell=True, cwd=working_dir, env=env)`。
- Linux 使用 `subprocess.Popen(["bash", "-c", cmd], cwd=working_dir, env=env)`。
- 返回 Popen。
`get_ebin_paths(server_root, profile)` 需要扫描:
- `<server_root>/_build/<profile>/lib/*/ebin`
- `<server_root>/_build/<profile>/checkouts/*/ebin`
- 只添加实际存在的 ebin 目录。
28. GUI 图标与按钮样式细则
`make_line_icon(kind, color="#c6d7ef", size=18)` 必须:
- 创建透明 QPixmap(size, size)。
- 从 `_ICON_SVG_BODY[kind]` 取 SVG body;找不到时用 `<circle cx="12" cy="12" r="6"/>`。
- body 中 `{color}` 替换为 color。
- 外层 SVG
- viewBox="0 0 24 24"
- fill="none"
- stroke=color
- stroke-width="2"
- stroke-linecap="round"
- stroke-linejoin="round"
- 用 QSvgRenderer(QByteArray(svg.encode("utf-8"))) 渲染。
- QPainter 开启 Antialiasing。
- 返回 QIcon(pixmap)。
`set_button_icon(button, kind, color, size)`
- button.setIcon(make_line_icon(...))
- button.setIconSize(QSize(size, size))
`CopyableIpLabel`
- 初始文字 `本机IP <ip>`。
- tooltip `点击复制本机 IP`。
- 鼠标左键点击复制 IP 到 QApplication.clipboard()。
- 文本改为 `已复制`,1.2 秒后恢复。
`button_style(variant, compact=False)` 调色板必须保持:
- primary: bg #4f8cff, fg #e8eef7, border #4f8cff, hover #3f7df0, pressed #356bd0
- success: bg #25c889, fg #0f1b16, border #25c889, hover #1fad75, pressed #16895f
- outline: bg #2b313c, fg #c6d7ef, border #4f6b92, hover #263a55, pressed #2b4d75
- danger: bg #342229, fg #ff5c6a, border #8b3941, hover #3e2730, pressed #4a2b34
- neutral: bg #242932, fg #aab4c3, border #3a4352, hover #2b313c, pressed #20242c
compact=True:
- padding 5px 10px
- border-radius 5px
- min-height 24px
compact=False:
- padding 8px 14px
- border-radius 6px
- min-height 30px
所有按钮 disabled 时:
- background #20242c
- color #778398
- border-color #3a4352
29. GUI 主标签页命令按钮细则
CommandTab 中按钮与回调必须对应:
编译与构建:
- “编译代码” -> do_compile -> `<rebar3> as game_server_dev compile`
- “快速编译” -> do_quick_compile -> 输入模块名,执行:
`"<escript>" shell/es/emake.escript 1 <module> && "<escript>" shell/es/emake.escript 2 <module>`
- “编译协议” -> do_protobuf -> `<rebar3> protobuf compile -a game_server`
- “编译Table” -> do_compile_table -> `<rebar3> cache compile -a game_server`
- “编译Tbllog” -> do_compile_tbllog -> `<rebar3> tbllog compile`
- “清理编译” -> do_clear -> 二次确认后 `<rebar3> clean`
服务器运行:
- “运行服务器” -> do_run
1. 打开 ServerSelectDialog。
2. check_node_status(server, cookie)。
3. 在线则询问是否连接该节点,选择是打开 RemoteConnectDialog。
4. 离线则 build_start_command(use_rebar=True)。
5. run_cmd_in_new_window(cmd, server_root, f"SERVER {server}")。
6. register_launched_window(server, "server", title, launch_info)。
7. _schedule_status_check(server, cookie)。
- “快速启动” -> do_quick_start:同 do_run,但 build_start_command(use_rebar=False)。
- “连接节点” -> do_remsh
1. RemoteConnectDialog。
2. build_remsh_command。
3. run_cmd_in_new_window(cmd, server_root, f"REMSH {target_node}")。
4. register_launched_window(target_node, "remsh", title, launch_info)。
- “查看账号” -> ViewAccountsDialog。
- “停止服务器” -> StopServerWorker。
- “服务器清档” -> ClearDatabaseDialog。
SVN
- “SVN 更新” -> `svn update`
`_schedule_status_check(server_name, cookie, retry_count=0)`
- QTimer.singleShot(5000, do_check)。
- 最多重试 10 次。
- 每次 check_node_status 后更新 NodeStatusCache。
- 在线输出 `[状态] 服务器 <server> 已启动 🟢`。
- 未在线且未到最大次数输出启动中。
- 超过最大次数输出启动超时。
30. GUI 工具设置页保存与 Erlang 版本检查细则
ConfigTab 的 Erlang 版本检查:
- `_update_erl_version`
- r25_path 为空时用 `erl`。
- r25_path 非空时 Windows 用 `<r25_path>/bin/erl.exe`,其他用 `<r25_path>/bin/erl`。
- 执行:
`[erl_cmd, "-eval", "io:format(\"~s\", [erlang:system_info(otp_release)]), halt().", "-noshell"]`
- timeout=5。
- 成功显示 `OTP <version> (配置路径)` 或 `OTP <version> (系统环境)`。
- 失败显示路径无效、未找到 erl、获取失败、获取超时。
- `_check_env_erl_version`
- 先检查系统环境 `erl` 版本。
- 再检查配置路径版本。
- 输出到控制台带时间戳。
- 两者不同则 QMessageBox.warning,说明执行 rebar3 编译时将使用配置路径版本。
- 一致则 QMessageBox.information。
保存配置:
- 保存前记录 old_r25_path。
- 写入 workspace、database、server、erlang 字段。
- self.config.save()。
- 如果 Erlang 路径变化:
1. QMessageBox.question 询问是否删除 `<server_root>/_build` 并重新编译。
2. 选择是则 shutil.rmtree(_build)。
3. 执行 `<rebar3> as game_server_dev compile`,输出到控制台。
- emit config_saved。
- 提示“项目配置已保存到当前项目的 .server_manager/config/tool.config”。
31. CLI argparse 精确结构细则
`server_manager_cli.main()` 必须使用 argparse.RawDescriptionHelpFormatter,并在 epilog 中给出示例。
parser
- prog 使用 Path(sys.argv[0]).name。
- description = "Server Manager - 命令行版本"。
- 全局参数:
- `--root`, `-r`
- `--config`, `-c`
- `--cookie`
subparsers dest="command"。
list
- `subparsers.add_parser("list", help="列出所有服务器")`
create
- `--type`, `-t`, required=True, choices=["game","login","center","cross","client"]
- `--id`, `-i`, type=int, required=True
- `--prefix`, `-p`
- `--db-host`
- `--db-port`, type=int
- `--db-user`
- `--db-pass`
- `--login-node`
- `--center-node`
- `--server-name`
- `--tcp-port`, type=int
- `--http-port`, type=int
- `--open-time`
- `--overwrite`, action="store_true"
start
- `--server`, `-s`, required=True
- `--quick`, `-q`, action="store_true"
- `--background`, `-b`, action="store_true"
- 调用 `cli.start_server(server_name=args.server, use_rebar=not args.quick, foreground=not args.background)`。
stop
- `--server`, `-s`, required=True
connect
- `--server`, `-s`, required=True
- `--ip`
compile
- `--target`, `-t`, choices=["all","code","proto","table","tbllog"], default="all"
- `--type`, choices=["game","login"], default="game"
regen
- `--server`, `-s`, required=True
interactive
- add_parser("interactive", aliases=["i"])
无 command 时进入 interactive_menu。
32. PyInstaller 与 Inno Setup 命令细则
Windows `src/build.bat` PyInstaller 主命令必须等价于:
- `pyinstaller --noconfirm --onedir --console --name main`
- `--icon icon\icon.ico`,没有 icon.ico 时用 `--icon NONE`
- `--add-data "..\version.json;."`
- `--add-data "config.json;."`
- `--add-data "server_commands.py;."`
- `--add-data "server_creator.py;."`
- `--add-data "icon;icon"`
- hidden imports:
- PyQt6.QtWidgets
- PyQt6.QtCore
- PyQt6.QtGui
- pymysql
- server_commands
- server_creator
- server_manager_cli
- updater
- app_config
- bsdiff4
- requests
- `--collect-all PyQt6`
- `--collect-all bsdiff4`
- entry `server_manager_gui.py`
Windows build 后处理:
- 如果 `..\rg.exe` 存在,复制到 `dist\main\rg.exe`。
- 如果 `..\output\config` 存在,复制到 `dist\main\config\`。
- 生成 mini_updater
`pyinstaller --noconfirm --onefile --windowed --name mini_updater --distpath dist --workpath build_mini --specpath build_mini ..\mini_updater.py`
- 如果 `config\tool.config` 存在,复制到 `dist\main\config\tool.config`。
- 清理 build、build_mini、spec。
Linux `build_linux.sh`
- 使用 `python3 -m PyInstaller`。
- add-data 分隔符用冒号:
- `../version.json:.`
- `config.json:.`
- `server_commands.py:.`
- `server_creator.py:.`
- `icon:icon`
- 输出 `dist/main/main`。
- 复制 `../output/config/.` 到 `dist/main/config/`。
Inno Setup `build_installer.iss`
- 支持 `#ifdef LOCAL_TEST`
- MyAppName "ServerManager (本地测试)"
- 独立 AppId
- OutputDir output_local
- OutputBaseFilename ServerManager_Setup_LOCAL
- 正式版:
- MyAppName "ServerManager"
- OutputDir output
- OutputBaseFilename ServerManager_Setup
- MyAppVersion "1.0.46"。
- MyAppExeName "main.exe"。
- Files:
- Source "src\dist\main\*" DestDir "{app}" recursesubdirs createallsubdirs
- Source "src\dist\mini_updater.exe" DestDir "{app}"
- Source "version.json" DestDir "{app}"
- Source "output\config\*" DestDir "{app}\config" recursesubdirs createallsubdirs
- Dirs:
- `{app}\config` uninsneveruninstall
- `{app}\logs` uninsneveruninstall
- Icons:
- 开始菜单主程序
- 卸载入口
- 可选桌面快捷方式
- 可选快速启动栏
- Run:
- 安装完成后 nowait postinstall 启动 `{app}\main.exe`
- UninstallDelete:
- 删除 `{app}`
- 删除 `{localappdata}\{#MyAppName}`
33. default.kv 与 sys 模板生成细则
如果无法还原真实 Erlang sys_*.config.example 的完整内容,也必须生成结构化模板,满足以下要求:
- 是合法 Erlang config term 文件。
- 顶层为列表,末尾句点。
- 每个模板必须包含对应 app 相关配置段,并引用必要占位符。
- 占位符必须使用 `${key}`,不要使用 Jinja 或 Python format。
- 字符串值如果在 Erlang 中需要字符串,模板里直接写 `"${key}"`Python 替换值不负责加引号。
- atom/boolean/list/tuple 值如 `${auto_reload}`、`${open_time}`、`${merge_server_ids}` 不要加引号。
sys_game.config.example 至少包含:
- server_id
- server_type
- server_name
- game_host
- tcp_port
- http_port
- login_node
- center_node
- open_time
- auto_reload
- db_game_name
- db_log_name
- db_host/db_port/db_user/db_pass
- db_save_time/db_save_count
- log_save_time/log_save_count
- log_dir
- role_log
- logger_level
- merge_server_ids/merge_server_time/last_merge_server_ids
- risk_control_server_ip/risk_control_server_post
- is_develop/is_inner/gm_auth
sys_login.config.example 至少包含:
- server_id
- server_type
- tcp_port
- auto_reload
- db_game_name
- db_host/db_port/db_user/db_pass
- log_dir
- logger_level
sys_center.config.example 至少包含:
- server_id
- server_type
- login_node
- open_time
- db_game_name
- db_log_name
- db_host/db_port/db_user/db_pass
- log_dir
- logger_level
sys_cross.config.example 至少包含:
- server_id
- server_type
- center_node
- open_time
- db_game_name
- db_log_name
- db_host/db_port/db_user/db_pass
- log_dir
- logger_level
sys_client.config.example 至少包含:
- server_id
- server_type
- game_host
- game_tcp_port
- tcp_port
- login_host
- login_http_port
- login_node
- db_game_name
- db_host/db_port/db_user/db_pass
- log_dir
- logger_level
模板可以包含注释,但生成 sys.config 时原样保留注释。
34. 复刻时的最小可运行检查脚本建议
生成项目后,至少用以下命令做静态检查:
- `python -m py_compile src/server_manager_gui.py src/server_manager_cli.py src/server_commands.py src/server_creator.py src/log_viewer_tab.py src/log_viewer_pane.py src/rg_search.py src/app_config.py src/updater.py`
- `python src/server_manager_cli.py --help`
- `python -c "from server_commands import read_config_file; print(read_config_file('output/config/default.kv').get('prefix'))"`
- `python -c "from server_creator import generate_server_dir_name; print(generate_server_dir_name('ddxq2','game',100))"`
- `python -c "from rg_search import find_rg_executable; print(find_rg_executable())"`
GUI 需要人工检查:
- 无项目启动欢迎页。
- 打开一个临时服务器项目目录后生成 `.server_manager/config`。
- 工具设置保存后存在 `.server_manager/config/tool.config`。
- 创建服务器后存在 `run/<server>/config/kv.config` 与 `sys.config`。
- 日志页在无日志文件时给出“文件不存在”而不是崩溃。
35. ServerManageTab 本地与远程管理细则
ServerManageTab 初始化状态:
- 保存 config、console。
- 线程成员:
- `_local_status_worker = None`
- `_remote_status_worker = None`
- `_scheduled_status_worker = None`
- 启动后状态检查队列:
- `_pending_status_checks = {}`
- `_status_check_timer = QTimer(self)`singleShot=Truetimeout 连接 `_flush_scheduled_status_checks`
本地服务器列表刷新:
- run_dir 优先 config.workspace.run_dir;为空则用 server_root/run。
- run_dir 不存在时:
- list 中显示“运行目录不存在,请在配置页面设置”
- 控制台输出 `[警告] 运行目录不存在: <run_dir>`
- 扫描前输出 `[信息] 扫描目录: <run_dir>`。
- 分类及图标:
- 游戏服务器 -> 🎮
- 跨服服务器 -> 🌐
- 登录服务器 -> 🔐
- 客户端服务器 -> 📱
- 中心服务器 -> 🏢
- 其他服务器 -> 📁
- 分类标题为 `--- <分类> ---`,不可选,前景色 #25c889。
- 每个服务器:
- 检查 `<run_dir>/<server>/config/kv.config`
- 有 kv.config 显示 `🔧`
- 只有 config 目录/其他配置显示 `📄`
- item 文本:` <类型图标> ⚪ <server> <配置图标>`
- UserRole 保存 server。
- local_servers_data[server] = {item, icon, config_icon}
- 总数为 0 时输出 `[提示] 请先创建服务器`。
- 有服务器时输出:
- `[信息] 找到 <n> 个服务器`
- `[提示] 🔧=有kv.config(动态生成) 📄=仅sys.config | 点击刷新状态查看运行状态`
- 调 refresh_status_from_cache。
本地状态刷新:
- refresh_status_from_cache 只读 NodeStatusCache,不发起网络请求。
- cached_status True -> `🟢`,前景 #25c889,计入 online_count。
- 其他 -> `⚪`,前景 #aab4c3。
- local_status_label 显示 `在线: <online>/<total>`。
- _refresh_server_status
- 无服务器时显示“没有服务器”。
- worker 正在运行时显示“正在检查...”并返回。
- 禁用刷新按钮,启动 NodeStatusCheckWorker。
- _on_local_status_refresh_finished
- cache.batch_set(results)。
- 更新所有 item 文本和颜色。
- 启用刷新按钮。
- 控制台输出 `[状态] 在线服务器: <online>/<total>`。
启动后的延迟状态检查:
- `_schedule_status_check(server_name, cookie, retry_count=0)` 不直接启动 worker,而是写入 `_pending_status_checks`
`{ "cookie": cookie, "retry_count": retry_count, "due_at": time.monotonic() + 5.0 }`
- `_restart_status_check_timer` 找最早 due_at,设置 QTimer delay。
- `_flush_scheduled_status_checks`
- 如果已有 scheduled worker 运行,则重启 timer 后返回。
- 取出所有到期检查。
- 按 cookie 分组,启动 GroupedNodeStatusCheckWorker。
- `_on_scheduled_status_checks_finished`
- 更新 NodeStatusCache 和本地列表。
- 在线则输出 `[状态] 服务器 <server> 已启动 🟢`。
- 不在线且 retry < 10,则输出启动中并重新 schedule。
- 达到 10 次输出启动超时。
远程服务器加载:
- 登录服节点从 config.server.login_server_node 读取。
- cookie 从 config.server.cookie 读取,默认 ddxq2-node。
- erl_path 从工具设置读取。
- login_node 为空时 remote_status_label 显示红色“请先在项目设置中配置登录服节点”。
- 加载前先显示黄色“正在检测登录服节点...”。
- check_node_status(login_node, cookie, erl_path) 失败时:
- remote_status_label 红色“登录服节点不可达”
- QMessageBox.warning,提示检查节点名称、Cookie、Erlang 路径、网络与防火墙。
- 成功后显示“正在从登录服加载列表 <login_node> ...”。
- query_remote_servers_from_login_rpc 成功后:
- 按 `_server_id_sort_key(server_id)` 排序。
- 清空 remote_server_list。
- 每行 3 列:服务器ID、服务器名称、服务器节点。
- 第一列用 ServerIdTableItem(status_icon, server_id)。
- 第三列 UserRole 保存 server_node。
- remote_servers_data[server_node] = {item, server_id, server}
- 启用排序并按第一列升序。
- remote_status_label 绿色“已加载 <n> 个服务器”。
- 控制台输出 `[远程] 从登录服 <login_node> 加载了 <n> 个远程服务器`。
- 清空搜索框。
- QTimer.singleShot(0, _refresh_remote_server_status)。
- ValueError 单独显示错误文本。
- 其他 Exception 显示“加载失败”,控制台输出具体错误,并弹 QMessageBox。
远程状态刷新:
- 只检查当前表格显示的节点,避免搜索过滤后还检查隐藏节点。
- 去重后 display_nodes。
- total = len(remote_servers_data)check_count = len(display_nodes)。
- worker 完成后:
- cache.batch_set(results)。
- 更新每行状态图标和前景色。
- data["server"]["running"] 同步结果。
- 如果 check_count == total`在线: <online>/<check_count>`
- 否则:`当前显示在线: <online>/<check_count> | 总数: <total>`
- 控制台输出 `[远程状态] 批量检查 <check_count> 个节点,在线: <online>`。
远程搜索过滤:
- 输入 text lower 后匹配 `server_id + " " + server_name + " " + server_node`。
- 过滤时重建表格。
- 保留每行 UserRole。
- 状态文本:
- 有搜索:`显示 <filtered>/<total> 个服务器`
- 无搜索:`已加载 <total> 个服务器`
36. DefaultKvTab 细则
DefaultKvTab 用于编辑当前项目 `.server_manager/config/default.kv`。
UI
- 标题:`默认配置管理 (.server_manager/config/default.kv)`,颜色 #25c88916px,加粗。
- 描述:`此文件定义创建服务器时的默认值,支持 ${key} 占位符替换`。
- 横向 QSplitter
- 左侧:分类列表,最大宽 200;底部 `+ 新增配置项`。
- 右侧:配置项编辑区,QScrollArea + QFormLayout。
- 底部按钮:
- 重新加载
- 保存配置
load_default_kv
- server_root 为空时控制台输出 `[警告] 请先配置服务器根目录`。
- default_kv_path = get_server_manager_config_dir(server_root) / "default.kv"。
- 文件不存在时输出警告。
- 读取文件时:
- current_category 初始为“其他”。
- 行形如 `# === xxx ===` 且以 `===` 结尾时作为分类标题。
- 非注释且包含 `=` 的行解析为 key/value。
- kv_data[key] = value。
- categories[current_category].append(key)。
- 分类列表按文件顺序添加。
- 默认选中第一类。
- 控制台输出 `[信息] 已加载默认配置: <path>`。
分类切换:
- 清空右侧表单。
- 每个 key 生成 QLineEdit(value)。
- 每行右侧有删除按钮。
- config_inputs[key] = input_field。
新增配置:
- QInputDialog 先输入 key,再输入 value。
- 当前选中分类为空时归入“其他”。
- 写入 kv_data 和 categories。
- 刷新当前分类。
- 控制台输出 `[信息] 已添加配置项: key=value`。
删除配置:
- QMessageBox.question 确认。
- 从 kv_data 删除。
- 从所在 category keys 删除。
- 刷新当前分类。
- 控制台输出 `[信息] 已删除配置项: key`。
保存:
- 先把当前界面 config_inputs 写回 kv_data。
- 重新生成文件:
- 固定文件头四行注释。
- 每个分类写 `# === <category> ===`。
- 逐项写 `key=value`。
- 分类后空行。
- UTF-8 写入。
- 成功输出 `[成功] 配置已保存: <path>` 并弹窗。
- 失败输出错误并 QMessageBox.critical。
注意:此页保存会重写 default.kv,因此注释和空行不会完全保留;复刻时应保持这个行为。
37. CreateServerTab 动态字段与校验细则
服务器类型下拉数据:
- `("游戏服务器 (GameServer)", "game")`
- `("登录服务器 (LoginServer)", "login")`
- `("客户端服务器 (ClientServer)", "client")`
- `("中心服务器 (CenterServer)", "center")`
- `("跨服服务器 (CrossServer)", "cross")`
字段默认值来源:
- default_kv = read_merged_config(server_root)。
- server_id 初始值为 config.server.default_server_id。
- server_name 优先 config.server.server_name,其次 default_kv.server_name,默认“未命名服_1”。
- server_name 前缀通过 `_extract_name_prefix` 去掉末尾 `_数字`。
- tcp_port 默认 default_kv.tcp_portfallback 18001。
- http_port 默认 default_kv.http_portfallback 19001。
- login_host 默认 `node_host_only(default_kv.login_host)` 或 get_local_ip()。
- login_http_port 默认 default_kv.login_http_port 或 19900。
- 数据库配置框内提供 db_game_name、db_log_name 输入框;game/login/center/cross 显示,client 隐藏。
- db_game_name 默认 `{prefix}_{type}_s{id}`db_log_name 默认 `{prefix}_{type}_log_s{id}`ID 或类型变化时同步默认值,但创建前可手动修改。
- auto_reload 默认 default_kv.auto_reload == "true"。
- 登录/中心节点优先 tool.config,其次 default.kv,去掉首尾单引号。
`_on_server_id_changed(value)`
- server_name_input = `<server_name_prefix>_<value>`。
- tcp_port = 18000 + value。
- http_port = 19000 + value。
- 同步 db_game_name、db_log_name 默认值。
`_on_server_type_changed`
- ID 范围从 default_kv
- `<type>_id_min`
- `<type>_id_max`
- `<type>_id_default`
- fallback
- game: 100, 9999, 100
- login: 10, 100, 10
- center: 1, 10, 1
- cross: 10000, 20000, 10000
- client: 1, 10000, 1
- 当前 ID 超出范围时设置默认值;非初始化阶段弹窗提示。
- 显示规则:
- server_name: 仅 game
- tcp_port: game/login/client
- login_host/login_http_port: 仅 client
- http_port: 仅 game
- auto_reload: 全部显示
- open_time: game/center/cross
- login_node: game/client/center
- center_node: game/cross
- db_game_name/db_log_name: game/login/center/cross
- client 模式下:
- server_id_label = “连接服务器ID:”
- tcp_port_label = “连接服务器端口:”
- 其他模式恢复:
- server_id_label = “服务器ID:”
- tcp_port_label = “TCP 端口:”
- 如果 login_node 和 center_node 都不显示,则隐藏“使用默认节点配置”。
创建前校验:
- 用 get_existing_server_ids(run_dir, server_type, prefix) 检查 ID 冲突。
- 冲突时弹窗列出已存在 ID。
- 数据库配置:
- 勾选“使用默认数据库配置”时用 config.database。
- 否则用页面输入。
- game/login/center/cross 必须填写 db_game_name、db_log_name,并传给 create_server 写入 kv.config。
- 节点配置:
- game/client/center 必须有 login_node。
- game/cross 必须有 center_node。
- 缺失时弹“配置不完整”,提示到本页或项目设置填写。
- client 必须填写 login_host。
- open_time 转为 Erlang 格式:
`{{Y,M,D},{H,M,S}}`
- 确认弹窗内容按类型展示 ID、自动热更、端口、节点、开服时间等。
38. run.bat、run.sh 与发版批处理精确流程
`src/run.bat`
- `SCRIPT_DIR=%~dp0` 并去除末尾反斜杠。
- `pushd "%SCRIPT_DIR%\..\.."` 得到 SERVER_ROOT。
- 切回 SCRIPT_DIR。
- 查找打包主程序优先级:
1. 环境变量 SERVER_MANAGER_MAIN 且文件存在。
2. `%SCRIPT_DIR%\main.exe`
3. `%SCRIPT_DIR%\dist\main\main.exe`
- 无参数:
- 打包程序存在:`"%SM_MAIN%" --root "%SERVER_ROOT%" interactive`
- 否则:`python server_manager_cli.py --root "%SERVER_ROOT%" interactive`
- help/--help/-h 打印脚本帮助。
- gui
- 打包程序存在:直接 `"%SM_MAIN%"`
- 否则检查 Python 和 PyQt6;缺 PyQt6 时 pip install PyQt6;然后 `python server_manager_gui.py`
- 其他命令:
- 打印服务器根目录。
- 打包程序存在则 `"%SM_MAIN%" --root "%SERVER_ROOT%" %*`
- 否则 `python server_manager_cli.py --root "%SERVER_ROOT%" %*`
`src/run.sh`
- `set -e`
- SCRIPT_DIR 为脚本目录。
- SERVER_ROOT 为 SCRIPT_DIR/../..。
- SM_MAIN 优先:
1. SERVER_MANAGER_MAIN
2. SCRIPT_DIR/main 且可执行
3. SCRIPT_DIR/dist/main/main 且可执行
- check_python 支持 python3 或 python,要求 Python 3.6+。
- gui 模式检查 PyQt6,缺失时 pip3 install PyQt6 或 pip install PyQt6。
- 无参数进入 interactive。
- gui 启动 GUI。
- help 打印帮助。
- 其他命令转发 CLI 并附加 --root SERVER_ROOT。
`release.bat`
- 参数 `%1` 为新版本号;为空时 prompt 输入。
- 阶段 1/4:进入 srccall build.bat nopause。
- 阶段 2/4`python "%ROOT%\release.py" "%NEW_VER%"`。
- 阶段 3/4:检查 ISCC 路径,运行 `ISCC build_installer.iss`。
- 阶段 4/4:确保 output 和 output/patches 存在,执行 `python release.py --post-build`。
- 完成后提示 output 下完整安装包、version.json、patches。
`pack_all.bat`
- 不改版本号。
- Step 1src/build.bat nopause。
- Step 2ISCC build_installer.iss。
- 复制根目录 version.json 到 output/version.json。
- 确保 output/patches 存在。
`pack_local_test.bat`
- 不改版本号,不覆盖 output。
- Step 1src/build.bat nopause。
- Step 2`ISCC /DLOCAL_TEST=1 build_installer.iss`。
- Step 3:复制 version.json 到 output_local/version.json。
- 产物:output_local/ServerManager_Setup_LOCAL.exe。
- 失败时提示可改用 `python pack_local_test.py`。
39. get_local_ip 与配置解析工具函数细则
`get_local_ip()` 推荐实现:
- 创建 UDP socket。
- connect 到 `8.8.8.8:80`,不发送数据。
- getsockname()[0] 得到本机出口 IP。
- 异常时 fallback `127.0.0.1`。
- finally close socket。
`subprocess_args_to_display(args)`
- 只用于日志展示。
- 参数含空格、单双引号或 `()[]{}&|<>^` 时用双引号包裹。
- 否则原样。
`node_host_only(value)`
- 去掉空白和首尾单双引号。
- 如果包含 @,返回最后一个 @ 后面的 host。
- 否则返回原值。
`_server_id_sort_key(server_id)`
- 先尝试整体转 int,成功返回 `(0, int_value, text)`。
- 否则查找字符串中的第一个数字,成功返回 `(0, int_value, text)`。
- 都失败返回 `(1, 0, text)`。
- 用于远程服务器 ID 排序和表格项比较。
40. 复制复刻提示词时的生成约束
当把本提示词交给代码生成模型时,要求模型遵守:
- 先生成源码和文本模板,不生成 exe。
- 二进制资源可用占位文件或说明替代,但 icon.png/icon.ico 若无法生成真实图标,应提供 convert_icon.py 能从 icon.png 生成 icon.ico。
- 所有路径逻辑必须兼容 Windows 反斜杠和 Linux 正斜杠。
- 不要把旧版根目录 main.py 当成主 GUI 入口;主入口必须是 src/server_manager_gui.py。
- 不要把用户服务器项目配置写到软件安装目录;项目配置必须在打开项目的 `.server_manager/config` 下。
- `output/config` 是模板源,不是用户项目配置。
- `src/config.json` 只是示例/旧配置,不应作为当前项目配置的唯一来源。
- 打包脚本可假定 Inno Setup 安装在默认路径,但 Python 脚本版应允许 `INNO_SETUP_ISCC` 环境变量覆盖。
- 所有危险动作都必须有确认:删除服务器、清档、清理编译、应用更新、删除 _build。
41. CLI 命令实现与交互菜单细则
`src/server_manager_cli.py` 必须实现一个 `ServerManagerCLI` 类和 `main()` 分发入口,CLI 与 GUI 共用 `server_commands.py`、`server_creator.py` 中的核心函数,不能各写一套逻辑。
`start_server(server_name, use_rebar=True, foreground=False)` 的复刻规则:
- 检查 `Path(self.run_dir) / server_name` 是否存在,不存在打印 `✗ 服务器 xxx 不存在` 并返回 False。
- 调用 `generate_start_config(self.server_root, server_name)` 生成/刷新 sys.config,返回空则打印无法生成并返回 False。
- 调用 `build_start_command_by_config(self.server_root, merged_config, self.cookie, use_rebar)` 得到启动命令。
- `working_dir` 固定为 `self.server_root`,不要切到具体 run 子目录。原因是 rebar3/erl 要在根目录找到 `rebar.config` 与 `_build`。
- 前台模式调用 `run_cmd_foreground(cmd, working_dir, server_name)`,随后 `process.wait()`;捕获 `KeyboardInterrupt` 时打印中断提示并 `process.terminate()`。
- 后台模式调用 `run_cmd_in_new_window(cmd, working_dir, server_name)`,打印其返回的启动方式文本。
- 启动时输出服务器、模式、前台运行、工作目录四项,最后输出分隔线。
`stop_server(server_name)` 的复刻规则:
- 用 `get_local_ip()` 作为本机 host。
- 调用 `build_stop_command(server_name, self.cookie, local_host=game_host)`,该函数返回参数列表而不是 shell 字符串。
- 用 `subprocess_args_to_display(args)` 生成可读命令用于日志和打印。
- `subprocess.run(args, timeout=15)` 执行停止,returncode 为 0 才算成功。
- 捕获 `subprocess.TimeoutExpired` 打印停止超时,捕获通用异常打印错误文本。
`connect_server(server_name, target_ip=None)` 的复刻规则:
- 用 `build_remsh_command(server_name, self.cookie, target_ip, local_host=get_local_ip())` 构造远程 shell 命令。
- remsh 需要交互,必须用 `run_cmd_foreground(cmd, self.server_root, f"remsh-{server_name}")` 在当前窗口执行。
- 捕获 `KeyboardInterrupt` 时打印断开连接并 terminate。
`regenerate_config(server_name)` 的复刻规则:
- 检查服务器目录存在。
- 检查 `run/<server>/config/kv.config` 存在。
- 调用 `generate_start_config(self.server_root, server_name)`。
- 成功时打印 `✓ sys.config 已重新生成` 和 `_config_file` 路径;失败时提示检查 kv.config 和模板文件。
`compile(target='all', server_type='game')` 的命令映射必须精确:
- `server_type == 'login'` 使用 profile `login_server_dev`。
- 其他类型使用 profile `game_server_dev`。
- `all` 和 `code``{REBAR3_CMD} as {profile} compile`
- `proto``{REBAR3_CMD} protobuf compile`
- `table``{REBAR3_CMD} cache compile`
- `tbllog``{REBAR3_CMD} tbllog compile`
- 通过 `run_cmd_foreground(cmd, self.server_root, f"compile-{target}")` 执行,返回码 0 打印成功,否则打印失败和返回码。
`interactive_menu()` 必须是循环菜单:
- 顶部打印 `Server Manager - 交互式模式` 和服务器根目录。
- 选项固定为:1 列出所有服务器,2 创建服务器,3 启动服务器,4 停止服务器,5 连接服务器,6 编译代码,7 重新生成 sys.config0 退出。
- 捕获 `KeyboardInterrupt` 和 `EOFError`,打印 `再见!` 或 `取消操作`,不能抛出堆栈。
- 创建服务器交互:类型 1-5 映射 game/login/center/cross/client;服务器 ID 必须是数字;prefix 为空时用默认;TCP/HTTP 端口为空时为 None。
- 启动/停止/连接/重生成交互:先 `get_server_list(self.run_dir)`,把所有分类的服务器合并后排序展示,输入序号选择。
- 启动交互:启动模式 1 为 rebar3 shell2 为快速 erl;运行方式 1 为前台,2 为后台;最终调用 `start_server(server_name, use_rebar=not quick, foreground=not background)`。
- 编译交互:服务器类型 1 game、2 login;目标 1 all、2 code、3 proto、4 table、5 tbllog。
argparse 入口必须包含:
- 全局参数:`--root/-r`、`--config/-c`、`--cookie`。
- 子命令:`list`、`create`、`start`、`stop`、`connect`、`compile`、`regen`、`interactive`,其中 `interactive` 别名为 `i`。
- `create` 参数:`--type/-t` required 且 choices 为 game/login/center/cross/client`--id/-i` int required`--prefix/-p``--db-host``--db-port` int`--db-user``--db-pass``--login-node``--center-node``--server-name``--tcp-port` int`--http-port` int`--open-time``--overwrite`。
- `start` 参数:`--server/-s` required`--quick/-q``--background/-b`。
- `stop` 参数:`--server/-s` required。
- `connect` 参数:`--server/-s` required`--ip`。
- `compile` 参数:`--target/-t` choices all/code/proto/table/tbllog,默认 all`--type` choices game/login,默认 game。
- `regen` 参数:`--server/-s` required。
- 未指定子命令时进入交互式菜单。
- 指定 `--cookie` 时覆盖 CLI 实例里的 cookie。
42. server_creator 配置项发现、校验与创建算法细则
`src/server_creator.py` 是创建服务器的唯一入口,GUI 和 CLI 都必须调用这里的函数。核心常量必须包含:
- `SERVER_TYPE_MAP`game -> game_serverlogin -> login_serverclient -> client_servercenter -> center_servercross -> cross_server。
- `SERVER_TYPE_NAMES`game 游戏服,login 登录服,client 客户端测试服,center 中心服,cross 跨服。
- `BASE_REQUIRED_KEYS`:至少包含 prefix、server_id、server_type、ip、db_host、db_user、db_pass、db_port、log_dir、db_game_name、auto_reload。
- 类型额外必填:game 包含 server_name/game_host/tcp_port/http_port/open_time/login_node/center_node/db_log_namelogin 包含 tcp_portcenter 包含 open_time/login_node/db_log_namecross 包含 open_time/center_node/db_log_nameclient 包含 tcp_port/game_host/login_host/login_http_port/login_node。
- `BASE_CONFIG_KEYS` 至少覆盖 prefix、server_id、server_type、ip、db_host、db_port、db_user、db_pass、db_save_time、db_save_count、log_save_time、log_save_count、log_dir、logger_level、is_develop、is_inner、auto_reload、gm_auth。
- `SERVER_TYPE_SPECIFIC_KEYS` 中 game 需包含 server_name、game_host、tcp_port、http_port、login_node、center_node、open_time、merge_server_ids、merge_server_time、last_merge_server_ids、risk_control_server_ip、risk_control_server_postclient 需包含 game_tcp_port、login_host、login_http_port。
配置项发现函数必须按以下优先级实现:
- `_read_default_kv(server_root)` 读取 `read_merged_config(server_root)`,即合并后的项目配置,不直接只读 default.kv。
- `get_required_keys_from_kv(server_root, server_type)` 从 `required_keys_base` 和 `required_keys_<type>` 读取逗号分隔列表;若 base 不存在则返回 None。
- `get_config_keys_from_kv(server_root, server_type)` 读取所有 `config_keys_base_<key>` 与 `config_keys_<type>_<key>`,返回 `{key: 中文说明}`;一个都没有则返回 None。
- `get_editable_config_from_default_kv(server_root)` 读取所有 `editable_config_<key>`,用于编辑对话框的可添加配置项。
- `get_allowed_config_keys(server_type, server_root)` 优先用 `get_config_keys_from_kv`,否则回退 `BASE_CONFIG_KEYS + SERVER_TYPE_SPECIFIC_KEYS[server_type]`。
- `get_all_valid_keys(server_root)` 优先从 `config_keys_base_` 与 `config_keys_<type>_<key>` 推导所有 key;否则用硬编码集合。
- `get_required_keys(server_type, server_root)` 优先用 `get_required_keys_from_kv`,否则用硬编码必填集合。
- `extract_template_placeholders_with_comments(server_root, server_type)` 从 `.server_manager/config/sys_<type>.config.example` 读取模板,正则 `\$\{(\w+)\}` 提取占位符;同一行若有 `%% 注释`,取逗号、中文逗号、句号、左括号前的短说明,并限制最长 15 字;没有注释时用 key 自身。
- `get_template_config_keys(server_root, server_type)` 优先级为 editable_config、config_keys、模板占位符、硬编码默认项。若 editable_config 存在但没有 auto_reload,要补一个 auto_reload;模板推导结果必须移除 prefix、server_id、server_type。
服务器命名和默认值算法:
- `generate_server_dir_name(prefix, server_type, server_id)` 返回 `{prefix}_{server_type}_s{server_id}`。
- `get_existing_server_ids(run_dir, server_type, prefix)` 使用正则 `^{prefix}_{server_type}_s(\d+)$` 匹配目录,大小写不敏感,返回 int 集合。
- `get_id_range_from_config(default_kv, server_type)` 默认范围:game 100-9999 默认 100login 10-100 默认 10center 1-10 默认 1cross 10000-20000 默认 10000client 1-10000 默认 1。可由 `<type>_id_min`、`<type>_id_max`、`<type>_id_default` 覆盖。
- `get_default_port(server_type, server_id)` 返回 `(18000 + server_id, 19000 + server_id)`。
- `get_default_server_name(prefix, server_id)` 返回 `{prefix}_{server_id}`。
`build_kv_config(...)` 必须只生成服务器差异化配置:
- 基础字段:prefix、server_id、server_type、ip、db_host、db_user、db_pass、db_port、log_dir=log、db_game_name。
- `server_type` 写入映射后的 `game_server/login_server/...` 字符串。
- 数据库名:默认 `db_game_name={prefix}_{type_prefix}_s{server_id}`,非 login 类型默认额外写 `db_log_name={prefix}_{type_prefix}_log_s{server_id}`;若调用方传入 db_game_name/db_log_name,则使用传入值,db_log_name 非空即写入。
- auto_reload 传入 bool 时写字符串 `true` 或 `false`。
- game:可写 server_name、tcp_port、http_port、open_time;必须写 game_host=ip;写 role_log=`run/{server_dir}/log`。
- login:只追加 tcp_port/http_port 这类登录服自身端口字段,不写 db_log_name。
- center:追加 open_time 和 login_node。
- cross:追加 open_time 和 center_node。
- client:写 game_host=ip、tcp_port、game_tcp_port、login_host、login_http_portlogin_http_port 默认 19900。
- 节点字段不要额外包引号,模板负责 Erlang 字符串格式。
`create_server(...)` 的核心流程必须精确:
- 先 `read_merged_config(server_root)`,取 `ip`,没有则 `get_local_ip()`。
- `run_dir` 为空且有 `server_root` 时设为 `server_root/run`。
- 生成 `server_dir` 和 `run/<server>/config/kv.config`。
- 若 kv.config 已存在且 `overwrite` 为 False,返回 `(False, '服务器 xxx 已存在', None)`。
- 创建 config 目录,调用 `build_kv_config`,再 `write_config_file(kv_config_file, diff_config)`。
- 调用 `generate_start_config(server_root, server_dir)` 生成完整 sys.config。
- 成功返回 `(True, '服务器 xxx 创建成功', merged_config)`;异常返回 `(False, '创建服务器失败: xxx', None)`。
43. Config 类 tool.config 读写与首次打开项目细则
`server_manager_gui.py` 中 `Config` 类负责“当前项目配置”,不能把项目配置混入应用级 app_config。
初始化规则:
- frozen 模式:`_exe_dir = Path(sys.executable).parent``_script_dir = _exe_dir`。
- 非 frozen`_script_dir = Path(__file__).parent``_exe_dir = _script_dir.parent.parent`。
- 初始 `tool_dir=None`、`tool_config_path=None`、`config_error=None`、`data=_empty_config_data()`。
- `project_root` 不为空时立即 `open_project(Path(project_root))`,否则 `config_error="未打开项目"`。
`_empty_config_data()` 必须返回完整嵌套结构,避免无项目欢迎页中调用 `get()` 报错:
- workspaceserver_root、run_dir 为空。
- databasehost/user/password 为空,port=3306。
- serverprefix=ddxq2login_server_node/center_server_node 为空,default_server_id=1server_type=game_serverserver_name 为空,ip/game_host 为 `get_local_ip()`cookie=ddxq2-node。
- erlangr25_path 为空,erl/werl/escript 为命令名。
- login_dbname 为空,server_id=900host/user/password 为空,port=0。
- commandsrebar=`REBAR3_CMD`svn=svn。
`open_project(project_root)`
- `project_root = Path(project_root).resolve()`。
- 调用 `ensure_and_get_server_manager_config_dir(project_root)`,该函数负责创建/同步 `.server_manager/config` 和受管模板。
- 失败时保存 `config_error` 并返回 False。
- 成功时设置 `tool_dir=project_root``tool_config_path=cfg_dir / "tool.config"`,清空 `config_error`,调用 `_load_config()` 后返回 True。
`_load_tool_config()`
- tool.config 不存在返回空 dict。
- 逐行读取 UTF-8strip 空白,跳过空行和 `#` 注释。
- 只解析第一处 `=`,左右 strip 后存入 dict。
- 出错返回空 dict。
`_save_tool_config(config)` 的输出顺序必须固定,方便人工查看和版本比较:
- 文件头:`# Server Manager config`、`# 此文件配置工具运行所需的环境路径`。
- 工作目录配置:server_root、run_dir。
- Erlang 配置:r25_path。
- 数据库配置:db_host、db_port、db_user、db_pass。
- 登录服数据库配置:login_db_name、login_db_server_id、login_db_host、login_db_port、login_db_user、login_db_pass。
- 服务器基础配置:prefix、default_server_id、server_type、server_name、login_node、center_node、cookie。
- 写入前确保 parent 目录存在,UTF-8 写入,行之间用 `\n`。
`_load_config()` 的合并规则:
- `tool_cfg = _load_tool_config()``is_first_run = not tool_config_path.exists()`。
- `server_root` 默认当前项目目录,tool.config 中 server_root 非空则覆盖。
- `run_dir` 为空时默认 `server_root/run`。
- 首次运行且 run_dir 不存在时尝试创建运行目录,失败只忽略,不阻止 GUI 打开。
- 读取 `get_server_manager_config_dir(server_root) / 'default.kv'` 作为默认值来源。
- 辅助 `get_value(tool_key, default_key, fallback)`:优先 tool.config;为空时读 default.kv;如果 default.kv 值是首尾单引号包裹,要去掉单引号;最后仍为空则 fallback。
- database.port、login_db.server_id、login_db.port 要转 int,空值分别回退 3306、900、0。
- 每次加载都用 `get_local_ip()` 写入 server.ip 和 server.game_host。
- 首次运行或发现 db_host、db_user、db_pass、prefix、login_node、center_node、cookie 从 default.kv 补齐时,调用 `save()` 回写 tool.config。
`save()`
- tool_config_path 为空时直接返回。
- 保存前刷新本机 IP,设置到 server.ip 与 server.game_host。
- 把嵌套数据映射为扁平 tool_cfg 字段后调用 `_save_tool_config()`。
- `get(*keys, default=None)` 逐层读取 dict,缺失返回 default。
- `set(value, *keys)` 自动创建中间 dict。
- `is_configured()` 只要求 workspace.server_root 非空且路径存在。
- `get_missing_config()` 返回中文缺失项列表,至少覆盖 server_root 为空和路径不存在两种情况。
44. 应用级配置、最近项目与软件日志细则
`src/app_config.py` 负责“软件级配置”,与打开的服务器项目无关。复刻时必须把它与 `Config` 区分开。
- `get_app_config_dir()`Windows 使用 `%LocalAppData%/ServerManager`,若 LOCALAPPDATA 不存在则用用户家目录;非 Windows 使用 `~/.config/ServerManager`;创建目录并返回 Path。
- `get_app_config_path()` 返回 `get_app_config_dir() / "app_config.json"`。
- `load_app_config()`JSON 不存在、损坏或不是 dict 时返回空 dict。
- `save_app_config(data)`UTF-8 写 JSON`ensure_ascii=False``indent=2`;异常静默忽略,不影响主流程。
- `get_recent_projects()`:读取 `recent_projects` 列表,只取前 15 个候选;转字符串并去空;去重;路径必须存在且 `project_has_default_kv_for_manager(p)` 为 True;最多返回 10 个。
- `save_recent_project(project_path)`resolve 成绝对路径;若已存在则移到第一位;最多保存 10 个;写回 app_config.json。
- `get_app_log_dir()` 返回 app 配置目录下的 `logs`,并创建目录。
GUI 中 `load_recent_projects()` 和 `save_recent_project()` 只是薄封装:
- `load_recent_projects()` 调用 `app_config.get_recent_projects()`。
- `save_recent_project(project_path)` 调用 `app_config.save_recent_project(project_path)`。
- 欢迎页和文件菜单都使用这两个封装,避免 GUI 直接关心 JSON 存储细节。
45. WelcomePage 与 MainWindow 项目切换细则
`WelcomePage` 是无项目模式首页,必须提供打开项目、新建项目、帮助、最近项目列表。
UI 结构:
- 顶层 QVBoxLayoutspacing=16,边距 10/8/10/10。
- 顶部 `welcomeCard` 最小高度约 200,包含 folder 图标、标题 `打开服务器项目`、副标题 `请选择一个已有服务器项目目录,或从最近项目中快速进入`。
- 按钮行包含 `打开项目`、`新建项目`、`帮助` 三个按钮;打开项目和新建项目都弹目录选择;帮助按钮发出 `help_requested`。
- 最近项目卡片标题为 `最近项目`,左侧 calendar 图标。
- 最近项目最多展示 5 条;无最近项目时显示 `暂无最近项目`。
- 每条最近项目是一个 row:folder 图标、可鼠标选择的路径 QLabel、`打开` 按钮、`定位` 按钮。
- `打开` 按钮 emit `open_project_requested(Path(path))`。
- `定位` 在 Windows 调 `explorer <path>`,其他系统调 `xdg-open <path>`;异常用 QMessageBox.warning。
- `showEvent()` 必须刷新最近项目列表,确保从菜单打开/关闭项目后欢迎页数据最新。
`MainWindow.init_ui()` 必须建立欢迎页和项目内容的双态界面:
- 窗口标题 `Server Manager v{get_current_version()}`,最小尺寸 1000x750。
- 中央控件是 `QStackedWidget`index 0 为 `WelcomePage`index 1 为 `project_content_container`。
- 状态栏左侧消息 `● 系统运行正常`,永久控件包含版本 QLabel 和系统时间 QLabel。
- 用 QTimer 每秒刷新系统时间,格式 `YYYY-MM-DD HH:MM:SS`。
- 菜单栏强制 `setNativeMenuBar(False)`,避免 macOS/某些环境把菜单移出窗口。
- 文件菜单:打开项目、最近的项目子菜单、关闭项目、退出。
- 帮助菜单:检查更新、关于。
- `file_menu.aboutToShow` 连接 `_refresh_recent_projects_menu()`,动态刷新最近项目。
- 欢迎页 `open_project_requested` 连接 `_on_open_project``help_requested` 连接 `_on_welcome_help_requested`。
- 若启动时 `config.has_project()` 为 True,立即 `_build_project_content()`、切到 index 1、标题追加项目路径、启用关闭项目、执行 `_run_project_migrations()`。
- 否则显示欢迎页,禁用关闭项目。
项目打开/关闭:
- `_refresh_recent_projects_menu()` 清空子菜单;无项目时加禁用项 `(无最近项目)`;有项目时路径长度超过 50 显示 `...` + 最后 47 字符,tooltip/data 保存完整路径,点击调用 `_on_open_project(Path(p))`。
- `_on_open_project_menu()` 弹 `QFileDialog.getExistingDirectory(self, "选择服务器项目目录", str(Path.cwd()))`。
- `_on_open_project(path)` 调用 `self.config.open_project(path)`;失败时 QMessageBox.warning 显示 `config_error`;成功后 `save_recent_project(str(path))`、重建项目内容、切到项目页、标题追加路径、启用关闭项目、执行迁移。
- `_run_project_migrations()` 取 workspace.server_root,若空则用 config.tool_dir;调用 `run_startup_migrations(server_root, logger_func=self.console.append_output)`;异常只写 logger,不弹崩溃;有迁移输出时追加分隔线。
- `_on_close_project()` 清空 Config 的 tool_dir/tool_config_path/config_error/data;删除项目内容布局内所有 widget;切回欢迎页;标题恢复版本号;禁用关闭项目。
46. MainWindow 项目内容、状态缓存与控制台显示细则
`_build_project_content()` 必须每次打开项目时重建整个项目工作区:
- 先清空 `project_content_layout` 中旧 widget 并 deleteLater,避免切换项目后引用旧 Config。
- 创建竖向 `QSplitter`,上方是 `QTabWidget`,下方是输出日志面板。
- 创建共享 `OutputConsole`,传给所有需要输出日志的 Tab。
- Tab 顺序固定:控制台、创建服务器、服务器管理、日志查看、工具设置。
- 对应类:`CommandTab`、`CreateServerTab`、`ServerManageTab`、`LogViewerTab`、`ConfigTab`。
- `ConfigTab.config_saved` 连接 `_on_config_saved()`。
- 主标签图标类型数组固定为 `["calendar", "plus", "server", "log", "settings"]`。
- 右上角 corner widget 使用 `CopyableIpLabel(get_local_ip())` 显示本机 IP,允许复制。
- 输出日志面板标题 `输出日志`,右侧有 `独立窗口` 和 `清空` 按钮。
- `独立窗口` 调 `self.console.open_window``清空` 调 `self.console.clear_output`。
- splitter sizes 初始为 `[360, CONSOLE_PANEL_HEIGHT]``CONSOLE_PANEL_HEIGHT=345`。
- 构建完成后输出启动 banner、版本、本机 IP、服务器目录、运行目录;缺 server_root 时提示去工具设置配置。
- 500ms 后 `QTimer.singleShot(500, self._init_node_status_cache)`。
控制台显示策略:
- `_sync_console_panel_visibility(index)` 只在主 `控制台` 页签 index=0 显示底部输出日志。
- index=0 时 console_widget fixed height 为 345splitter sizes 设为 `[360, 345]`。
- 非 index=0 时隐藏 console_widgetsplitter sizes 设为 `[1, 0]`,避免日志面板占空间。
- `_on_tab_changed(index)` 必须先更新图标颜色,再同步控制台显隐。
- 切到服务器管理页 index=2 时调用 `server_manage_tab.refresh_status_from_cache()`。
- 切到日志查看页 index=3 时调用 `log_viewer_tab.refresh_server_list()`。
- `_update_main_tab_icons(current_index)` 将当前图标颜色设为蓝色 `#4f8cff`,其他设为灰色 `#aab4c3`。
节点状态缓存初始化:
- `_init_node_status_cache()` 先取 `get_node_status_cache()`,如果 `is_initialized()` 为 True,输出已初始化并返回。
- 从 Config 取 server_root/run_dirrun_dir 为空时用 `server_root/run`。
- run_dir 为空、不存在或没有服务器时输出原因,调用 `cache.set_initialized(True)` 后返回。
- 服务器列表按分类顺序 game、cross、login、client、center、other 合并。
- 启动 `NodeStatusCheckWorker(all_servers, cookie, erl_path=r25_path)`cookie 默认 ddxq2-node。
- 防止重复启动:若 `_cache_init_worker` 正在运行则直接返回。
- finished_signal 连接 `_on_init_node_status_cache_finished(results, total)`finished 后清空 `_cache_init_worker` 并 deleteLater。
- 完成后 `cache.batch_set(results)`、`cache.set_initialized(True)`,输出在线数量,并刷新服务器管理页缓存状态。
`_on_config_saved()`
- 如果存在 create_server_tab,调用 `refresh_config()`。
- 如果存在 server_manage_tab,调用 `refresh_server_list()`。
- 如果存在 log_viewer_tab,调用 `refresh_server_list()`。
47. GUI 启动入口、CLI 转发、frozen 运行与退出清理细则
`server_manager_gui.py` 既是 GUI 入口,也是打包后带参数的 CLI 入口。
`if __name__ == '__main__'`
- `len(sys.argv) > 1` 时调用 `_run_cli_from_argv()`,由 `server_manager_cli.main()` 处理 list/create/start/stop 等命令。
- 无参数时调用 GUI `main()`。
- 这样打包后的 `main.exe list` 与 `python server_manager_cli.py list` 行为一致。
GUI `main()` 规则:
- Windows frozen 模式下先隐藏控制台窗口:`GetConsoleWindow()` 后 `ShowWindow(hwnd, 0)`;不要 `FreeConsole()`,否则后续 subprocess 可能 WinError 50。
- 启动时调用 `clean_up_old_version()` 清理更新残留 `.old` 文件。
- 调用 `_suppress_pyinstaller_warnings()` 抑制 PyInstaller 临时目录清理警告。
- 调用 `_configure_frozen_qt_runtime()` 后创建 `QApplication(sys.argv)`style 设为 Fusion。
- 图标查找:frozen 用 `sys._MEIPASS`;非 frozen 优先当前工作目录下 `icon/icon.png`,没有则尝试 `cwd/src/icon/icon.png`,再回退 `Path(__file__).parent/icon/icon.png`。
- 创建 `Config(project_root=None)`,读取最近项目;若最近项目非空,尝试自动 `open_project(Path(recent[0]))`,失败则显示欢迎页。
- 创建 `MainWindow(config)`show,进入 `app.exec()`。
- frozen 模式退出时用 `os._exit(exit_code)` 跳过 bootloader 清理;非 frozen 用 `sys.exit(exit_code)`。
- 顶层异常写 loggerfrozen 异常用 `os._exit(1)`,非 frozen 继续 raise。
`_configure_frozen_qt_runtime()`
- 仅 frozen 模式执行。
- base_dir 取 `sys._MEIPASS`,否则取 exe 目录。
- 推导 `PyQt6/Qt6/bin`、`PyQt6/Qt6/plugins`、`PyQt6/Qt6/plugins/platforms`。
- qt_bin 存在时 prepend 到 PATH,避免加载系统里不兼容 Qt DLL。
- qt_plugins 存在时设置 `QT_PLUGIN_PATH`。
- qt_platforms 存在时设置 `QT_QPA_PLATFORM_PLUGIN_PATH`。
`closeEvent(event)`
- frozen + win32 下通过 ctypes 设置 Windows error mode,组合 `SEM_FAILCRITICALERRORS`、`SEM_NOGPFAULTERRORBOX`、`SEM_NOALIGNMENTFAULTEXCEPT`、`SEM_NOOPENFILEERRORBOX`。
- 尝试 `SetUnhandledExceptionFilter(None)`,异常忽略。
- 先 `event.accept()`。
- frozen 模式下用 `QTimer.singleShot(100, lambda: os._exit(0))` 延迟强制退出,让 Qt 事件循环有时间收尾。
- 非 frozen 不要强制 os._exit,方便开发调试和查看异常。
48. 更新检查 UI 与启动自动检查细则
虽然更新底层函数在 `updater.py`,但 GUI 的用户交互必须在 `MainWindow` 中实现。
启动自动检查:
- `MainWindow.showEvent()` 只在第一次显示时启动检查,用 `_startup_update_check_started` 防重复。
- 延迟 1500ms 调 `_start_startup_update_check()`。
- `_start_startup_update_check()` 从 `get_version_info()` 读取 `update_url`,为空直接返回。
- 创建 `UpdateCheckWorker(update_url, self)`,连接 `_on_startup_update_checked` 后 start。
- `_on_startup_update_checked(manifest)`manifest 为空、远端版本为空或等于当前版本时返回。
- 版本不同则弹 QMessageBox.question,显示远端版本、当前版本、release_notes,询问是否立即更新。
- 用户确认后优先 `get_delta_for_current(manifest, current)`;若存在 patch_url 和 new_exe_sha256,走增量下载;否则使用 full_installer_url 或 download_url;都没有则 warning。
手动检查:
- `_on_check_update()` 从 version.json 的 `update_url` 拉取 manifest;未配置时 information 提示在 version.json 设置。
- `fetch_update_manifest(update_url)` 失败时展示网络、地址可访问性、稍后重试三类排查提示,并显示截断后的 update_url。
- 如果 `not version_less(current, remote_version)`,提示当前已是最新版本。
- 如果发现新版本但没有 full_installer_url/download_url,也没有可用 deltawarning 提示 manifest 配置不完整。
- 有增量时弹自定义 QMessageBox,按钮为 `取消`、`立即更新(增量)`、`立即更新(全量)`;无增量时按钮为 `取消`、`立即更新`。
- 点击增量调用 `_start_update_download(delta["patch_url"], use_delta=True, expected_sha256=delta.get("new_exe_sha256"))`。
- 点击全量调用 `_start_update_download(full_url, use_delta=False)`。
下载流程:
- `_start_update_download(download_url, use_delta=False, expected_sha256=None)` 在 temp 目录创建 `ServerManager_Update`。
- 增量文件名为 `ServerManager_patch<suffix>`,全量为 `ServerManager_Setup<suffix>`suffix 优先取 URL 后缀,增量默认 `.patch`,全量默认 `.exe`。
- 创建 `QProgressDialog("正在下载更新...", "取消", 0, 100, self)`minimumDuration=0。
- 创建 `UpdateDownloadWorker(download_url, dest_file, is_delta=use_delta, expected_sha256=expected_sha256)`。
- progress_signal 更新进度条,finished_signal 连接 `_on_update_download_finished`,取消按钮连接 `_on_update_canceled`。
- `_on_update_canceled()` 若 worker 仍运行则 terminate,并关闭进度框。
下载完成:
- 文件不存在或 path 为空时 warning 下载失败。
- 增量模式且有 expected_sha256information 提示即将退出并重启应用补丁;调用 `apply_delta_patch(Path(path), expected_sha256)`。
- 增量失败时弹 warning,提供 `全量更新` 和 `确定`;点全量且 manifest 中有 full URL 时重新走全量下载。
- 全量模式:information 提示即将退出并重启完成安装;调用 `run_installer_and_exit(Path(path), silent=True)`。
49. 必须保留的函数签名与模块边界清单
为了让复刻模型生成的项目可以被 GUI、CLI、测试脚本互相调用,以下函数/类名和参数名必须保持稳定:
- `app_config.py``get_app_config_dir()`、`get_app_config_path()`、`load_app_config()`、`save_app_config(data)`、`get_recent_projects()`、`save_recent_project(project_path)`、`get_app_log_dir()`。
- `server_creator.py``generate_server_dir_name(prefix, server_type, server_id)`、`get_existing_server_ids(run_dir, server_type, prefix)`、`build_kv_config(...)`、`create_server(...)`、`get_id_range_from_config(default_kv, server_type)`、`get_default_port(server_type, server_id)`、`get_default_server_name(prefix, server_id)`、`get_required_keys(server_type, server_root='')`、`get_allowed_config_keys(server_type, server_root='')`、`get_template_config_keys(server_root, server_type)`。
- `server_commands.py``get_local_ip()`、`get_server_manager_config_dir(server_root)`、`ensure_and_get_server_manager_config_dir(project_root)`、`project_has_default_kv_for_manager(project_root)`、`read_config_file(path)`、`write_config_file(path, config)`、`read_merged_config(server_root, server_name=None)`、`generate_start_config(server_root, server_name)`、`build_start_command_by_config(server_root, merged_config, cookie, use_rebar=True)`、`build_stop_command(server_name, cookie, local_host=None, erl_path=None)`、`build_remsh_command(server_name, cookie, target_ip=None, local_host=None, erl_path=None)`、`get_server_list(run_dir)`、`check_nodes_status(server_names, cookie, target_ip=None, erl_path=None)`、`get_node_status_cache()`、`run_startup_migrations(server_root, logger_func=None)`。
- `server_manager_cli.py``class ServerManagerCLI`,方法至少包含 `list_servers()`、`create_server_cmd(...)`、`start_server(...)`、`stop_server(...)`、`connect_server(...)`、`regenerate_config(...)`、`compile(...)`、`interactive_menu()`、`main()`。
- `server_manager_gui.py``_empty_config_data()`、`class Config`、`class CommandRunner`、`class NodeStatusCheckWorker`、`class GroupedNodeStatusCheckWorker`、`class StopServerWorker`、`class OutputConsole`、`class CommandTab`、`class CreateServerTab`、`class ServerManageTab`、`class LogViewerTab`、`class ConfigTab`、`class DefaultKvTab`、`class WelcomePage`、`class MainWindow`、`check_config_and_show_dialog(config)`、`main()`。
- `updater.py``get_current_version()`、`get_version_info()`、`version_less(a, b)`、`fetch_update_manifest(update_url)`、`download_file(url, dest_path, progress_callback=None)`、`get_delta_for_current(manifest, current_version)`、`apply_delta_patch(patch_path, expected_sha256)`、`run_installer_and_exit(installer_path, silent=True)`、`clean_up_old_version()`。
模块边界要求:
- 创建服务器只通过 `server_creator.create_server()`。
- 启动、停止、remsh、配置合并、模板同步只通过 `server_commands.py`。
- 最近项目和软件日志只通过 `app_config.py`。
- GUI 不直接拼接复杂 Erlang 命令,只调用命令构造函数。
- CLI 不直接写 sys.config 模板,只调用 `generate_start_config()`。
- 打包和发布脚本不参与运行期配置读写。
50. 复刻后的实现级验收补充
在基础验收之外,复刻项目还必须满足以下实现级检查:
- 无项目启动 GUI 时,不访问空的 `tool_config_path`,欢迎页可正常显示最近项目。
- 第一次打开项目时,如果 `.server_manager/config/tool.config` 不存在,会创建 run 目录并把 default.kv 中的数据库、prefix、cookie 等默认值写入 tool.config。
- 再次打开同一项目时,不覆盖已有 tool.config 中用户手动设置的值。
- 最近项目写入的是应用级 JSON,不写到项目目录;删除或无效项目不会出现在欢迎页最近列表。
- CLI `start --background` 会打开新窗口,CLI `start` 默认在当前窗口前台运行。
- GUI 和 CLI 生成的 `run/<server>/config/kv.config` 字段一致,且随后都能生成 sys.config。
- 停止服务器命令使用参数列表执行,不能依赖 shell 拼接引号。
- `main.exe list`、`main.exe --help`、`python src/server_manager_gui.py list` 均进入 CLI,不弹 GUI。
- `python src/server_manager_gui.py` 无参数进入 GUI,并自动打开最近的有效项目;没有最近项目则停留欢迎页。
- 切换项目时,旧 tab、旧 console、旧 worker 引用不会继续显示在新项目界面。
- frozen 模式下退出不会弹 PyInstaller 临时目录删除失败的系统错误框。
- `ConfigTab` 保存后,创建服务器页、服务器管理页、日志查看页都刷新配置或列表。
- 服务器管理页进入时优先展示缓存状态;后台批量检测完成后刷新在线/离线显示。
- 更新下载可以取消;增量失败后可以回退全量安装;全量安装会退出并启动安装包。
- 代码生成模型若不能真实生成 exe、patch、installer,必须生成完整脚本和清晰占位说明,但运行期 Python 源码必须完整。
十三、代码质量要求
1. 所有文件 UTF-8。
2. GUI 操作耗时任务必须使用 QThread 或后台线程,不能卡死主线程。
3. 读写配置统一使用 pathlib 和 UTF-8。
4. 启动/停止命令涉及 shell 的地方要谨慎处理 Windows 引号和括号;停止命令优先用参数列表 subprocess.run。
5. 删除服务器、清档、清理编译、替换 exe 等危险操作必须有确认或明确流程。
6. 缺依赖、缺模板、路径不存在、rg 不存在、Erlang 不可用、数据库连接失败都要给用户可理解的错误提示。
7. 文档要说明打包产物由脚本生成,不要把 src/dist 和 output/*.exe 当成手写源码。
十四、验收标准
1. `python src/server_manager_gui.py` 无参数能打开 GUI 欢迎页。
2. `python src/server_manager_gui.py list` 或打包后的 `main.exe list` 能进入 CLI。
3. `python src/server_manager_cli.py --help` 显示完整命令。
4. 打开一个服务器项目后能生成/同步 `<项目根>/.server_manager/config`。
5. 工具设置保存后能写入 tool.config。
6. 创建服务器能生成 run/<server>/config/kv.config 与 sys.config。
7. rebar3 启动命令、快速 erl 启动命令、stop/remsh 命令格式正确。
8. 服务器管理能扫描本地 run 目录并显示分类列表。
9. 日志页能加载普通日志和归档日志;rg 存在时能全局搜索。
10. `src/build.bat` 能生成 `src/dist/main/main.exe` 与 `src/dist/mini_updater.exe`。
11. Inno Setup 能生成 output/ServerManager_Setup.exe。
12. version.json、release.py、mini_updater.py 支持全量/增量更新流程。
```