148 KiB
148 KiB
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 风格快捷键。
核心文件树
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
完整复刻提示词
将下面提示词交给代码生成模型,即可要求其从零生成当前项目的等价实现。
你是资深 Python 桌面工具工程师。请从零创建一个名为 Server Manager 的项目,目标是复刻一个用于 Erlang/rebar3 游戏服务器工程的跨平台管理工具。输出完整仓库源码、配置模板、运行脚本、打包脚本和文档。不要手工生成 exe、dist、installer 这类二进制产物;这些产物必须由脚本重新构建。
一、项目目标
1. 实现一个 Python 3.8+ 项目。
2. Windows 下提供 PyQt6 GUI;Windows/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、rebar3;Linux 后台启动优先 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.py:Tkinter 示例/遗留入口,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.iss:Inno 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.py:PyQt6 主 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.py:ripgrep 封装。
- updater.py:GUI 使用的自动更新工具函数。
- config.json:旧版/示例配置,可包含 workspace、database、server、erlang、commands、login_db。
- run.bat 与 run.sh:源码/打包入口自动选择脚本。
- build.bat、build_portable.bat、build_linux.sh:PyInstaller 打包脚本。
- 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.example:Erlang 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_*。至少包含当前项目中的这些 key: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_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. 按类型动态显示字段:
- game:server_name、tcp_port、http_port、auto_reload、open_time、login_node、center_node
- login:tcp_port、auto_reload
- center:auto_reload、open_time、login_node
- cross:auto_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_dev,app login_server。
- client_server 使用 profile client_server_dev,app client_server。
- game/center/cross 使用 profile game_server_dev,app 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_config:tool.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:本地/远程节点连接选择。
- KvConfigEditDialog:kv.config 编辑与 sys.config 重新生成。
- CreateServerTab:创建服务器。
- ServerManageTab:本地/远程服务器管理。
- ConfigTab:工具设置。
- DefaultKvTab:default.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-strings;whole_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>.log,role_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_manifest:HTTP GET update_url,兼容 full_installer_url 与旧 download_url。
- get_delta_for_current:从 manifest["delta_updates"][current_version] 取 patch_url/new_exe_sha256。
- download_file:requests stream 下载,progress_callback(percent)。
- apply_delta_patch:bsdiff4.patch 当前 exe + patch,校验 sha256,写 TEMP/ServerManager_new.exe,调用 apply_update_and_restart。
- run_installer_and_exit:Windows 下静默参数启动安装包,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 必须:
- 找新 exe:src/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.Popen,stdout=PIPE,stderr=STDOUT,text=True,encoding="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。
- 添加配置项 QGroupBox:NoWheelComboBox + 添加按钮。
- 底部按钮:保存配置、关闭。
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。
- 创建 QApplication,style=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)`。
- 单批次直接执行;多批次用 ThreadPoolExecutor,max_workers 不超过 4。
- 每个 subprocess.run 使用 creationflags=CREATE_NO_WINDOW(Windows)并设置 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 并发查询每个 host,max_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)` 写入 NamedTemporaryFile,suffix=".erl",UTF-8,newline="\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" + stderr;Windows 下 -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_name,utf-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=True,timeout 连接 `_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)`,颜色 #25c889,16px,加粗。
- 描述:`此文件定义创建服务器时的默认值,支持 ${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_port,fallback 18001。
- http_port 默认 default_kv.http_port,fallback 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:进入 src,call 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 1:src/build.bat nopause。
- Step 2:ISCC build_installer.iss。
- 复制根目录 version.json 到 output/version.json。
- 确保 output/patches 存在。
`pack_local_test.bat`:
- 不改版本号,不覆盖 output。
- Step 1:src/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.config,0 退出。
- 捕获 `KeyboardInterrupt` 和 `EOFError`,打印 `再见!` 或 `取消操作`,不能抛出堆栈。
- 创建服务器交互:类型 1-5 映射 game/login/center/cross/client;服务器 ID 必须是数字;prefix 为空时用默认;TCP/HTTP 端口为空时为 None。
- 启动/停止/连接/重生成交互:先 `get_server_list(self.run_dir)`,把所有分类的服务器合并后排序展示,输入序号选择。
- 启动交互:启动模式 1 为 rebar3 shell,2 为快速 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_server,login -> login_server,client -> client_server,center -> center_server,cross -> 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_name;login 包含 tcp_port;center 包含 open_time/login_node/db_log_name;cross 包含 open_time/center_node/db_log_name;client 包含 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_post;client 需包含 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 默认 100;login 10-100 默认 10;center 1-10 默认 1;cross 10000-20000 默认 10000;client 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_port,login_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()` 报错:
- 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/game_host 为 `get_local_ip()`,cookie=ddxq2-node。
- erlang:r25_path 为空,erl/werl/escript 为命令名。
- login_db:name 为空,server_id=900,host/user/password 为空,port=0。
- commands:rebar=`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-8,strip 空白,跳过空行和 `#` 注释。
- 只解析第一处 `=`,左右 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 结构:
- 顶层 QVBoxLayout,spacing=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 为 345,splitter sizes 设为 `[360, 345]`。
- 非 index=0 时隐藏 console_widget,splitter 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_dir;run_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)`。
- 顶层异常写 logger;frozen 异常用 `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,也没有可用 delta,warning 提示 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_sha256:information 提示即将退出并重启应用补丁;调用 `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 支持全量/增量更新流程。