# 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.config` 和 `sys_*.config.example`。
- 应用级配置位于 `%LOCALAPPDATA%/ServerManager` 或 `~/.config/ServerManager`，用于保存最近项目列表和应用日志。
- 打包使用 PyInstaller 目录模式生成 `src/dist/ServerManager/ServerManager.exe`，Inno Setup 生成 `output/ServerManager_Setup.exe`。
- 实际 GUI/CLI 主入口是 `src/server_manager_gui.py`；根目录 `main.py` 是一个 Tkinter 自动更新集成示例/遗留入口。
- 更新机制支持 `version.json` 清单、全量安装包更新，以及基于 `bsdiff4` 的 `ServerManager.exe` 增量补丁；替换 exe 时由独立 `mini_updater.exe` 等待主进程退出后完成替换并重启。
- 日志查看支持单文件加载、玩家协议日志、日期归档日志、ripgrep 全局搜索、正文高亮、匹配行过滤和 less 风格快捷键。

## 核心文件树

```text
server_manager/
├── README.md
├── RELEASE.md
├── PROJECT_REPLICATION_PROMPT.md
├── version.json
├── main.py
├── updater.py
├── mini_updater.py
├── release.py
├── release.bat
├── pack_all.bat
├── pack_local_test.py
├── pack_local_test.bat
├── build_installer.iss
├── rg.exe                         # 可选，ripgrep Windows 可执行文件
├── output/
│   ├── config/
│   │   ├── default.config
│   │   ├── sys_game.config.example
│   │   ├── sys_login.config.example
│   │   ├── sys_center.config.example
│   │   ├── sys_cross.config.example
│   │   └── sys_client.config.example
│   ├── patches/
│   ├── version.json
│   └── ServerManager_Setup.exe     # 构建产物
├── output_local/                   # 本地测试包产物，通常忽略
└── src/
    ├── app_config.py
    ├── server_manager_gui.py
    ├── server_manager_cli.py
    ├── server_commands.py
    ├── server_creator.py
    ├── log_viewer_tab.py
    ├── log_viewer_pane.py
    ├── rg_search.py
    ├── updater.py
    ├── config.json
    ├── requirements.txt
    ├── run.bat
    ├── run.sh
    ├── build.bat
    ├── build_portable.bat
    ├── build_linux.sh
    └── icon/
        ├── icon.png
        ├── icon.ico
        └── convert_icon.py
```

## 完整复刻提示词

将下面提示词交给代码生成模型，即可要求其从零生成当前项目的等价实现。

```text
你是资深 Python 桌面工具工程师。请从零创建一个名为 Server Manager 的项目，目标是复刻一个用于 Erlang/rebar3 游戏服务器工程的跨平台管理工具。输出完整仓库源码、配置模板、运行脚本、打包脚本和文档。不要手工生成 exe、dist、installer 这类二进制产物；这些产物必须由脚本重新构建。

一、项目目标

1. 实现一个 Python 3.8+ 项目。
2. Windows 下提供 PyQt6 GUI；Windows/Linux 下提供 CLI。
3. GUI 无参数启动；命令行带任意参数时进入 CLI，行为与 src/server_manager_cli.py 一致。
4. 工具管理一个外部 Erlang 游戏服务器项目，项目根目录通常含 run、config、rebar3、apps 等。
5. 首次打开服务器项目时，将软件自带模板 output/config 下的 default.config 与 sys_*.config.example 同步到 <项目根>/.server_manager/config/。
6. 所有项目级配置保存在 <项目根>/.server_manager/config/user.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 退出后替换 ServerManager.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/ServerManager/*、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.config：默认配置和配置项定义。
- 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. 软件自带模板目录：
   - 打包后：ServerManager.exe 同级的 config/。
   - 源码运行：仓库根目录 output/config/。
3. 受管模板文件：
   - default.config
   - sys_center.config.example
   - sys_client.config.example
   - sys_cross.config.example
   - sys_game.config.example
   - sys_login.config.example
4. 打开项目时必须确保 .server_manager/config 存在并同步上述模板；保留用户自己的 user.config。
5. 配置合并优先级从低到高：
   - .server_manager/config/default.config
   - .server_manager/config/user.config
   - run_dir/config/user.config 或 run_dir/user.config
   - run_dir/<server_dir>/config/kv.config
6. 合并配置后强制用当前机器 IP 覆盖 ip 和 game_host。
7. user.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.config 或 config/project.config。
   - 应用日志写到同目录 logs/，日志文件名 server_manager_YYYYMMDD.log 或 server_manager_cli_YYYYMMDD.log，保留最近 7 天。

六、default.config 要求

default.config 必须含以下默认值和配置定义：

- 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.config 中是 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. 读取 user.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.config 的 <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.config 中声明的可编辑配置项；保存后重新生成 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.config 填入表单。
5. “保存配置”写入 user.config；若 Erlang 路径变化，询问是否删除 server_root/_build 并执行 rebar3 as game_server_dev compile。

欢迎页：
1. 无项目时显示欢迎页，提供打开项目、最近项目、定位项目、帮助入口。
2. 最近项目来自应用级 app_config.json，只显示仍存在且含 default.config 的项目。

更新机制：
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 优先从 ServerManager.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(当前 ServerManager.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/ServerManager/rg.exe。
   - 复制 ../output/config 到 dist/ServerManager/config。
   - 再用 PyInstaller onefile windowed 打包 ../mini_updater.py 到 dist/mini_updater.exe。
2. src/build_linux.sh 与 build.bat 等价，但使用 python3、Linux add-data 冒号分隔，输出 dist/ServerManager/ServerManager。
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/ServerManager/* 到 {app}，安装 mini_updater.exe、version.json、output/config/*。
   - 创建开始菜单/可选桌面快捷方式。
   - 安装完成后运行 ServerManager.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/ServerManager/version.json 和 _internal/version.json。
   - set_version_post(old_exe)：复制 version.json 到 output；若提供旧版 ServerManager.exe 且安装 bsdiff4，生成 output/patches/v<old>_to_v<new>.patch 并填 new_exe_sha256。

入口脚本：
1. src/run.bat：
   - 自动计算 SERVER_ROOT 为脚本目录向上两级。
   - 优先使用 SERVER_MANAGER_MAIN、同目录 ServerManager.exe、dist/ServerManager/ServerManager.exe。
   - 无参数进入 interactive。
   - gui 启动 GUI。
   - 其他参数转发 CLI，并附加 --root SERVER_ROOT。
2. src/run.sh 同理，优先 SERVER_MANAGER_MAIN、同目录 ServerManager、dist/ServerManager/ServerManager。

十二、核心代码复刻细则

下面是必须复刻的核心代码结构、关键函数签名和关键实现逻辑。生成代码时可以按这些骨架实现，允许适度重构，但外部行为、配置文件路径、命令格式、函数职责必须保持一致。

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.config", "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.config 存在、每个 MANAGED_CONFIG_TEMPLATE_FILES 都存在；逐个 copy2 到项目 .server_manager/config。
    - read_config_file 读取 key=value，忽略空行、# 注释、% 注释；去除空白和首尾双引号。
    - read_merged_config 合并顺序为 default.config -> config/project.config -> user.config -> run_dir user.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.config").exists():
                merged.update(read_config_file(str(cfg_dir / "default.config")))
            project_cfg = Path(server_root) / "config" / "project.config"
            if project_cfg.exists():
                merged.update(read_config_file(str(project_cfg)))

            if (cfg_dir / "user.config").exists():
                merged.update(read_config_file(str(cfg_dir / "user.config")))

            if run_dir:
                for p in (Path(run_dir) / "config" / "user.config", Path(run_dir) / "user.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 优先级：user.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：按固定分组写 user.config。
    - _load_default_kv(server_root)：读 .server_manager/config/default.config。
    - _load_config：user.config 为空时从 default.config 补齐；首次运行自动创建 run_dir；需要保存补齐后的 user.config。
    - save：写 user.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.config 编辑。
    - 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 环境变量。
    - 打包后 ServerManager.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：打包后优先 ServerManager.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/ServerManager/_internal/version.json 或 src/dist/ServerManager/version.json 存在，则同步复制。

    set_version_post 必须：

    - 找新 exe：src/dist/ServerManager/ServerManager.exe，找不到再找 src/dist/ServerManager.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/ServerManager。
    - 安装说明：Windows 安装包、源码运行 Windows/Linux。
    - 快速上手：打开项目时选择服务器项目根目录，不要选 run/config/.server_manager/config。
    - 首次打开项目会复制模板到 .server_manager/config。
    - 重点路径：服务器根目录、运行目录、Erlang 路径。
    - CLI 用法表：ServerManager.exe/ServerManager 无参数 GUI，带参数 CLI。
    - 命令行参数完整说明。
    - version.json 字段说明、download/update URL 规则。
    - 配置说明：项目级配置、单服配置、模板来源。
    - 文件结构。
    - 系统要求。
    - Linux 后台运行 screen/tmux 说明。
    - 注意事项与版本历史。

    RELEASE 必须覆盖：

    - 推荐流程：本地测试包 -> 本地安装验证 -> 正式发版。
    - pack_local_test 不改版本、不覆盖 output。
    - release.bat 顺序：PyInstaller -> 改版本 -> Inno -> post-build。
    - 本地不保存 ServerManager_*.exe；若要增量，需手动提供上一版 ServerManager.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/user.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 ServerManager`
    - `--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\ServerManager\rg.exe`。
    - 如果 `..\output\config` 存在，复制到 `dist\ServerManager\config\`。
    - 生成 mini_updater：
      `pyinstaller --noconfirm --onefile --windowed --name mini_updater --distpath dist --workpath build_mini --specpath build_mini ..\mini_updater.py`
    - 如果 `config\user.config` 存在，复制到 `dist\ServerManager\config\user.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/ServerManager/ServerManager`。
    - 复制 `../output/config/.` 到 `dist/ServerManager/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 "ServerManager.exe"。
    - Files:
      - Source "src\dist\ServerManager\*" 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}\ServerManager.exe`
    - UninstallDelete:
      - 删除 `{app}`
      - 删除 `{localappdata}\{#MyAppName}`

33. default.config 与 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.config').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/user.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.config`。

    UI：

    - 标题：`默认配置管理 (.server_manager/config/default.config)`，颜色 #25c889，16px，加粗。
    - 描述：`此文件定义创建服务器时的默认值，支持 ${key} 占位符替换`。
    - 横向 QSplitter：
      - 左侧：分类列表，最大宽 200；底部 `+ 新增配置项`。
      - 右侧：配置项编辑区，QScrollArea + QFormLayout。
    - 底部按钮：
      - 重新加载
      - 保存配置

    load_default_kv：

    - server_root 为空时控制台输出 `[警告] 请先配置服务器根目录`。
    - default_kv_path = get_server_manager_config_dir(server_root) / "default.config"。
    - 文件不存在时输出警告。
    - 读取文件时：
      - 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.config，因此注释和空行不会完全保留；复刻时应保持这个行为。

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"。
    - 登录/中心节点优先 user.config，其次 default.config，去掉首尾单引号。

    `_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%\ServerManager.exe`
      3. `%SCRIPT_DIR%\dist\ServerManager\ServerManager.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/ServerManager/ServerManager 且可执行
    - 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.config。
    - `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 类 user.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 / "user.config"`，清空 `config_error`，调用 `_load_config()` 后返回 True。

    `_load_tool_config()`：

    - user.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` 默认当前项目目录，user.config 中 server_root 非空则覆盖。
    - `run_dir` 为空时默认 `server_root/run`。
    - 首次运行且 run_dir 不存在时尝试创建运行目录，失败只忽略，不阻止 GUI 打开。
    - 读取 `get_server_manager_config_dir(server_root) / 'default.config'` 作为默认值来源。
    - 辅助 `get_value(tool_key, default_key, fallback)`：优先 user.config；为空时读 default.config；如果 default.config 值是首尾单引号包裹，要去掉单引号；最后仍为空则 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.config 补齐时，调用 `save()` 回写 user.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()`。
    - 这样打包后的 `ServerManager.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/user.config` 不存在，会创建 run 目录并把 default.config 中的数据库、prefix、cookie 等默认值写入 user.config。
    - 再次打开同一项目时，不覆盖已有 user.config 中用户手动设置的值。
    - 最近项目写入的是应用级 JSON，不写到项目目录；删除或无效项目不会出现在欢迎页最近列表。
    - CLI `start --background` 会打开新窗口，CLI `start` 默认在当前窗口前台运行。
    - GUI 和 CLI 生成的 `run/<server>/config/kv.config` 字段一致，且随后都能生成 sys.config。
    - 停止服务器命令使用参数列表执行，不能依赖 shell 拼接引号。
    - `ServerManager.exe list`、`ServerManager.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` 或打包后的 `ServerManager.exe list` 能进入 CLI。
3. `python src/server_manager_cli.py --help` 显示完整命令。
4. 打开一个服务器项目后能生成/同步 `<项目根>/.server_manager/config`。
5. 工具设置保存后能写入 user.config。
6. 创建服务器能生成 run/<server>/config/kv.config 与 sys.config。
7. rebar3 启动命令、快速 erl 启动命令、stop/remsh 命令格式正确。
8. 服务器管理能扫描本地 run 目录并显示分类列表。
9. 日志页能加载普通日志和归档日志；rg 存在时能全局搜索。
10. `src/build.bat` 能生成 `src/dist/ServerManager/ServerManager.exe` 与 `src/dist/mini_updater.exe`。
11. Inno Setup 能生成 output/ServerManager_Setup.exe。
12. version.json、release.py、mini_updater.py 支持全量/增量更新流程。
```
