docs: pivot launcher to Launcher.exe + portable layout + ~/.cmbot data

- Launcher is a compiled Python/PyInstaller Launcher.exe (retires update.ps1
  and install_local.ps1)
- portable: extract to any writable dir; InstallRoot = Launcher.exe's folder,
  no longer tied to %LOCALAPPDATA%
- user data moves to ~/.cmbot (%USERPROFILE%\.cmbot): always writable, per-user,
  survives program updates; get_data_dir() three-tier (env -> ~/.cmbot -> dev root)
- writability check now applies to the program dir only; first-run seeds default
  templates from app\config into ~/.cmbot
- docs/10 §3/§4/§5/§8/§9/§11/§16 updated; tasks 17.19 added (docs done, code TODO)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-18 10:37:48 +08:00
co-authored by Claude Opus 4.8
parent 54e42670f7
commit 0c9415fbcd
2 changed files with 75 additions and 48 deletions
+50 -44
View File
@@ -49,41 +49,40 @@
## 4. 安装目录结构
安装到**当前用户始终可写的位置**(见第 9 节):`%LOCALAPPDATA%\CMBot`
(即 `C:\Users\<用户>\AppData\Local\CMBot`)。**不安装到 `C:\` 盘根或 `C:\Program Files`**——
两者标准用户默认不可写,会导致自动更新与数据写入失败(见第 9 节)。
**便携布局**:把发布 zip 解压到**任意可写目录**(如 `D:\CMBot`、桌面)即可运行,不限定 `%LOCALAPPDATA%`。安装根 = `Launcher.exe` 所在目录,程序自更新就地进行。**不要放在 `C:\` 盘根或 `C:\Program Files`**——标准用户默认不可写,会导致自更新失败(见第 9 节)。
**用户数据不在安装根**,统一放用户主目录的 `~/.cmbot`(`%USERPROFILE%\.cmbot`),始终可写、按用户隔离、不随程序更新/重装丢失。
```text
%LOCALAPPDATA%\CMBot\ # C:\Users\<用户>\AppData\Local\CMBot
Launcher.exe # 用户双击入口,逻辑稳定、极少变更
<任意可写目录>\CMBot\ # 例如 D:\CMBot,InstallRoot = Launcher.exe 所在目录
Launcher.exe # 用户双击入口:检查更新 → 切换 → 启动 app
app\ # 当前主程序目录,一份完整 onedir 产物
CMBot.exe
version.txt # 当前 app 版本号,如 1.1.0
_internal\
resources\
config\ # 出厂默认配置/模板,首次运行播种到 ~/.cmbot(见第 5 节)
app.old\ # 上一个可用版本,仅用于回滚,可不存在
CMBot.exe
version.txt
_internal\
resources\
data\ # 用户数据,独立于版本,更新时不动
staging\ # 下载/解压临时区,安装成功后清理
%USERPROFILE%\.cmbot\ # 用户数据,独立于程序位置,始终可写、按用户隔离
config\
app_config.json # 用户偏好(输出格式、最近文件夹、最近模板等)
app_config.json # 用户偏好(输出格式、最近文件夹、最近模板、更新源等)
templates.json # 用户自定义模板
logs\
output\
staging\ # 下载临时区,安装成功后清理
```
说明:
- `Launcher.exe`:版本检查、下载、校验、切换、启动主程序。逻辑稳定,本身不参与自更新(见第 11 节)。
- `Launcher.exe`:版本检查、下载、校验、切换、启动主程序。**Python + PyInstaller onefile** 编译,逻辑稳定,本身不参与自更新(见第 11 节)。
- `app\`:当前要启动的程序目录。更新成功后,新版目录整体替换为新的 `app\`。
- `app\version.txt`:纯文本,仅存当前 `app\` 的版本号。由发布脚本从 `src/version.py` 写入,用于启动器比较本地版本。
- `app\version.txt`:纯文本,仅存当前 `app\` 的版本号。由发布脚本从 `src/version.py` 写入,供启动器比较本地版本。
- `app\config\`:出厂默认配置/模板,仅作首次运行的播种来源,运行时不读写。
- `app.old\`:上一个可用程序目录。更新失败或新版启动异常时,可把它改回 `app\` 完成回滚。
- `data\`:所有用户可写数据集中存放,**不随版本切换变动**。
- `staging\`:下载的 zip 与解压临时目录。校验通过后,`staging\app.new\` 才会切换为 `app\`。
- 整个安装根(含 `app\`、`app.old\`、`data\`、`staging\`、`Launcher.exe`)都必须免提权可写:自动更新需要重命名程序目录,故安装根不能落在 `C:\` 盘根或 `C:\Program Files`。
- `~/.cmbot\`:所有用户可写数据集中存放,**与程序位置、版本切换均无关**。即便 `app\` 所在目录只读(更新失败),配置/模板/导出仍可正常写。
- 安装根(含 `app\`、`app.old\`、`staging\`、`Launcher.exe`)需免提权可写**才能自更新**;不可写时仅「更新失败」,不影响数据读写。
## 5. 数据目录分离(实现前置改造)
@@ -93,18 +92,18 @@
- 引入「程序根目录」与「数据根目录」两个概念:
- 程序根(`get_app_dir()`):当前运行目录 `app\`(只读,更新时整体替换)。
- 数据根(`get_data_dir()`):`data\`(可写,跨版本稳定)。
- 数据根(`get_data_dir()`):用户数据所在,**与程序位置解耦**。
- 路径函数划分:
- `get_resource_path()` 指向**程序根**下的 `resources/`。
- `get_config_path()`、`get_log_dir()`、`get_output_dir()` 指向**数据根**下的对应目录。
- 数据根解析规则(`get_data_dir()`):
1. 环境变量 `CMBOT_DATA_DIR` 非空时使用它。启动器在更新布局中将其指向安装根的 `data\` 文件夹。
2. 否则回退到 `get_app_dir()`——即开发环境(项目根)与当前扁平 `onedir` 发布(数据与程序同级)的现状布局。
- 该设计使开发、当前扁平发布、未来 `app + data` 更新布局共用同一套路径规则(呼应 `docs/09` 第 7 节),且对现状**零行为变更**:未设环境变量时,config/logs/output 仍在程序目录旁。
- 兼容旧布局:写入配置/模板/日志/输出前按需创建多级目录(`parents=True`);数据根不存在时按 `docs/09` 第 6 节规则首次创建并写入默认配置/模板。
- 用户数据文件均位于数据根 `config/` 下:`config/app_config.json`、`config/templates.json`。
- 数据根解析规则(`get_data_dir()`,三级回退):
1. 环境变量 `CMBOT_DATA_DIR` 非空时使用它(覆盖口,供测试或特殊部署)。
2. 打包态(`sys.frozen`)→ `~/.cmbot`(即 `%USERPROFILE%\.cmbot`)。**不依赖启动器注入环境变量**:即使用户绕过 `Launcher.exe` 直接双击 `app\CMBot.exe`,数据也落在 `~/.cmbot`。
3. 开发态 → 项目根(不污染开发者主目录,保持现状)。
- **首次运行播种**:`~/.cmbot/config/templates.json`(或 `app_config.json`)不存在时,从程序包内 `app\config\` 拷贝出厂默认;缺省再退回内置默认(呼应 `docs/09` 第 6 节)。
- 写入配置/模板/日志/输出前按需创建多级目录(`parents=True`)。
数据目录分离后续需同步更新 `docs/05-project-architecture.md` 与 `docs/09` 第 5、6 节的目录说明。
数据目录分离与 `~/.cmbot` 约定后续需同步 `docs/05-project-architecture.md` 与 `docs/09` 第 5、6 节的目录说明。
## 6. 更新时机
@@ -180,25 +179,30 @@ http://cm.xiapi.com/
3. 将当前 `app\` 重命名为 `app.old\`。
4. 将 `staging\app.new\` 重命名为 `app\`。
5. 若第 4 步失败,立即将 `app.old\` 改回 `app\`,记录日志并降级。
7. **启动主程序**:启动 `app\CMBot.exe`,并设置 `CMBOT_DATA_DIR=<InstallRoot>\data`。
7. **启动主程序**:启动 `app\CMBot.exe`。数据根由主程序自身解析为 `~/.cmbot`(见第 5 节),启动器无需注入 `CMBOT_DATA_DIR`(如需指定可经该环境变量覆盖)。
8. **清理**:成功切换后清理 `staging\`(zip 与临时目录);`app.old\` 默认保留一个旧版本用于回滚(见第 13 节)。
任一步失败都不得使系统进入「无可启动版本」状态:只要本地存在一个可用版本,就以它启动。
## 9. 安装位置与权限
自动更新要求**整个安装根免提权可写**:不仅写用户数据,还要写 `staging\` 并重命名 `app\` / `app.old\`。据此选择安装位置。
便携模型把「程序位置」与「数据位置」拆开,权限要求也随之分两层:
- **默认安装到 `%LOCALAPPDATA%\CMBot`**(`C:\Users\<用户>\AppData\Local\CMBot`)。
- 当前用户对该目录始终可写,**无需管理员权限、不弹 UAC**,是自更新桌面应用(如 Chrome、VS Code、Slack)的标准做法。
- 这是**每用户安装**:每个登录用户各一份程序与数据,互不影响。
- **禁止安装到以下位置:**
**用户数据(`~/.cmbot`)—— 始终可写。**
- 用户主目录对当前用户永远可写、不需提权,故配置/模板/日志/导出无论程序装在哪都能正常读写。
- 按用户隔离:同机多用户即使共用同一份程序,各自数据互不干扰。
**程序与更新(安装根)—— 需可写才能自更新。**
- 安装根 = `Launcher.exe` 所在目录。自更新要写 `staging\` 并重命名 `app\` / `app.old\`,故该目录需**免提权可写**。
- **不要放在**:
- `C:\Program Files` / `C:\Program Files (x86)`:写入需管理员权限。
- `C:\` 盘根(如 `C:\CMBot`):在盘根新建目录默认需提权;即便由管理员预建,其继承 ACL 通常只给标准用户读取权限,自动更新与数据写入会失败;域环境组策略常直接锁死盘根。
- 不采用机器级共享安装(如 `C:\ProgramData\CMBot`):默认 ACL 下多用户互写、改他人文件易失败,且共享安装目录的更新通常又需管理员权限,与「标准用户自动更新」相悖。如确有全机共享需求,作为后续扩展单独设计。
- `C:\` 盘根(如 `C:\CMBot`):盘根新建/改名默认需提权;继承 ACL 常只给标准用户读取权限;域环境组策略常锁死盘根。
- 推荐放在 `D:\CMBot`、`%USERPROFILE%\CMBot`、桌面等用户可写位置。
- 启动器与更新过程**不应要求管理员权限**。
- 若检测到安装根不可写,提示用户改装到可写目录,不静默失败(呼应 `docs/09` 第 6 节)。
- `CMBOT_DATA_DIR`(见第 5 节)由启动器指向安装根下的 `data\`;未设置时主程序回退到程序目录,保持开发与当前扁平发布的现状行为。
- 启动器应**检测安装根是否可写**;不可写时提示「请将程序移到可写目录后再运行」并降级(仍能启动现有 `app\`、仍能读写 `~/.cmbot` 数据),不静默失败。
- 数据根默认 `~/.cmbot`,与安装根无关(见第 5 节);`CMBOT_DATA_DIR` 仅作覆盖口。
## 10. 强制更新
@@ -207,7 +211,7 @@ http://cm.xiapi.com/
## 11. 启动器自身的更新
启动器逻辑稳定、极少变更,**不参与自动自更新**,以避免「更新器更新自己」的文件锁问题。
启动器(`Launcher.exe`,Python + PyInstaller onefile)逻辑稳定、极少变更,**不参与自动自更新**,以避免「更新器更新自己」的文件锁问题(运行中的 `Launcher.exe` 无法被覆盖)。
- 启动器版本与主程序版本解耦,单独维护。
- 确需升级启动器时,作为一次性手动分发处理(替换 `Launcher.exe`),并在发布说明中标注。
@@ -249,17 +253,19 @@ HTTP 在线更新比内网共享面临更高风险,按以下层次防护:
1. **地基**:数据目录分离(第 5 节)。任何更新方案的前提。✅ 已实现。
2. **只读通知**:启动时读取 `manifest.json` 比对版本,有新版仅提示。✅ 已实现——`services/update_service.py`(`check_for_update` / 版本比较,纯逻辑可测,支持 **`http(s)://` 源 + Basic Auth** 及本地路径;manifest 用 `utf-8-sig` 解码以容忍 BOM)+ 主窗口顶部通知横幅,后台线程检查(不可达不阻塞启动),更新源 / 凭据由 `update_source` / `update_user` / `update_pass` 配置。
3. **自动安装**:启动器完整流程(下载 → 校验 → `app` 目录切换 → 启动 → `app.old` 回滚)。✅ 已实现并端到端验证——`scripts/update.ps1`(HTTP 下载 zip + SHA-256 + 解压 + `app/app.old` 切换);`scripts/build.ps1` 产出 `version.txt` + zip + 写 `manifest.json`(无 BOM)+ 可选发布;`scripts/install_local.ps1` 在 `%LOCALAPPDATA%\CMBot` 铺出 `app/app.old/data/staging` 布局并迁移用户配置。已用本地 HTTP server 假发布包验证「拉清单 → 下载 → 校验 → 切换」全流程通过。**待办**:真实环境端到端实测;接入正式发布流水线。
4. **强制更新与保留策略**:补全 `mandatory` / `min_supported` 与 `app.old` 回滚策略。⛔ 未做。
3. **自动安装(PowerShell 原型)**:启动器完整流程(下载 → 校验 → `app` 目录切换 → 启动 → `app.old` 回滚)。✅ 以 `scripts/update.ps1` 实现并端到端验证(HTTP 下载 zip + SHA-256 + 解压 + `app/app.old` 切换);`scripts/build.ps1` 产出 `version.txt` + zip + 写无 BOM `manifest.json`。**此为流程验证原型。**
4. **改用 `Launcher.exe` + `~/.cmbot`(当前方向)**:🚧 将启动器改为 **Python + PyInstaller onefile** 编译的 `Launcher.exe`(复用 `services/update_service.py`),采用**便携布局**(解压任意可写目录即用,安装根 = `Launcher.exe` 所在目录),用户数据移到 **`~/.cmbot`**(`get_data_dir()` 三级回退,见第 5 节)。退休 `scripts/update.ps1` 与 `scripts/install_local.ps1`(`%LOCALAPPDATA%` 安装器)。待做:`src/launcher.py`、`build.ps1` 增产 `Launcher.exe`、安装根可写性检测、首次播种默认模板、真实环境端到端实测。
5. **强制更新与保留策略**:补全 `mandatory` / `min_supported` 与 `app.old` 回滚策略。⛔ 未做。
启动器(`scripts/update.ps1`)要点:
启动器(`Launcher.exe`,目标)要点:
- 入口参数 `-InstallRoot`(默认脚本所在目录)、`-NoLaunch`(测试用,只更新不启动)。
- 从 `<InstallRoot>\data\config\app_config.json` 读 `update_source` / `update_user` / `update_pass`,与 stage ② 同源。
- 读取本地 `app\version.txt` → HTTP GET `manifest.json` → 比较版本 → 下载 `manifest.url` 到 `staging\<ver>.zip`(带 Basic Auth,支持续传)→ 校验 SHA-256 → 解压到 `staging\app.new\` → 校验 `CMBot.exe` 与 `version.txt` → `app` 改名为 `app.old` → `app.new` 改名为 `app`。
- 启动 `app\CMBot.exe` 并设 `CMBOT_DATA_DIR=<InstallRoot>\data`(接 stage ① 数据目录分离)。
- 任何失败(网络不可达 / 4xx-5xx / SHA-256 不符 / 解压失败 / 坏 manifest)都降级启动本地现版本,绝不进入「无可启动版本」。
- 旧版本目录保留为 `app.old\`,回滚 = 将 `app.old\` 改回 `app\`。
- 安装根 = `Launcher.exe` 所在目录(便携,不限定 `%LOCALAPPDATA%`);`-NoLaunch` 等价开关用于测试。
- 从 `~/.cmbot/config/app_config.json` 读 `update_source` / `update_user` / `update_pass`,与 stage ② 同源(复用 `update_service`)。
- 读取本地 `app\version.txt` → GET `manifest.json` → 比较版本 → 下载 `manifest.url` 到 `staging\<ver>.zip`(带 Basic Auth)→ 校验 SHA-256 → 解压到 `staging\app.new\` → 校验 `CMBot.exe` 与 `version.txt` → `app` 改名 `app.old` → `app.new` 改名 `app`。
- 启动前检测安装根可写性;首次运行把 `app\config\` 默认配置/模板播种到 `~/.cmbot`。
- 启动 `app\CMBot.exe`(主程序自行解析数据根为 `~/.cmbot`,无需注入 `CMBOT_DATA_DIR`)。
- 任何失败(网络不可达 / 4xx-5xx / SHA-256 不符 / 解压失败 / 坏 manifest / 安装根不可写)都降级启动本地现版本,绝不进入「无可启动版本」。
- 回滚 = 将 `app.old\` 改回 `app\`。
## 17. 暂不做
+21
View File
@@ -917,6 +917,27 @@
- [ ] 生产前将更新源切到 HTTPS、客户端改用只读账号
- [ ] 真实环境(cm.xiapi.com)端到端实测
### 17.19 启动器改 Launcher.exe + 数据移到 ~/.cmbot(便携模型)
前置阅读:
- `docs/10-lan-update.md`(§3 架构、§4 布局、§5 数据根三级回退、§9 权限、§11、§16 阶段④)
背景:
- 改用编译的 `Launcher.exe`(Python + PyInstaller onefile)取代 `update.ps1`,对终端用户更友好。
- 程序采用便携布局:解压任意可写目录即用,安装根 = `Launcher.exe` 所在目录,不限定 `%LOCALAPPDATA%`。
- 用户数据移到 `~/.cmbot`(`%USERPROFILE%\.cmbot`):始终可写、按用户隔离、不随程序更新丢失。
任务:
- [x] 文档:`docs/10` 改为便携 + `Launcher.exe` + `~/.cmbot` 模型(§3/§4/§5/§8/§9/§11/§16)
- [ ] `get_data_dir()` 三级回退:`CMBOT_DATA_DIR` → 打包态 `~/.cmbot` → 开发态项目根;更新单测
- [ ] `src/launcher.py`:复用 `update_service`,下载 zip→SHA-256→解压→`app/app.old` 切换→启动;安装根可写性检测;首次把 `app\config\` 默认模板播种到 `~/.cmbot`
- [ ] `build.ps1` 增产 `Launcher.exe`(PyInstaller onefile),发布 zip 含 `Launcher.exe` + `app\`
- [ ] 退休 `scripts/update.ps1` 与 `scripts/install_local.ps1`
- [ ] 端到端实测(解压到 D 盘运行、自更新、回滚)
## 18. 后续暂缓任务
以下任务第一阶段暂不做,后续需要时再新增设计文档: