docs: 跟上 TUI 与入口重组(README 入口名 + TUI 一节 + CHANGELOG + CONTEXT 模块与入口)

README 里 24 处过期入口名(13 处 Backup.ps1、11 处 Restore.ps1)全部更新为新名字 —— 这正是 ADR-0012 里垫片存在的理由:内部实现改名断了会当场报错,而 README 里的入口名过期是**静默没用**,所以垫片留一轮等文档跟上。现在文档跟上了,删垫片的时机也就到了。

README 新增「交互界面(TUI)」一节:怎么进三个编辑器、每个编辑器能改什么(以及为什么有些字段刻意不列 —— 值跨行或本身是集合时"改一行"没有明确含义)、-Quiet 与 -InputScript 的用途、以及"只有真终端才画界面,否则明确报错并给退出码 2"。入口表从"两个入口"扩成四个(主入口 / 动作 / 配置 / 垫片)。

CHANGELOG 增加「新增:交互界面与入口重组」一节,含三项编辑器共用的外科式改写保证与那条往返断言;并写明旧命令照旧可用。

CONTEXT.md 增加「模块与入口」一节:BakNRet/ 四层的分工与规矩(清单是显式白名单、加载器是唯一点源顺序声明处、Public 一个函数一个文件、Private 放内部实现与 State),以及四个入口脚本的职责与"编排尚未下沉"这个已知的下一步。

验收:test.ps1 9/9 全绿(5.1 与 7)。
This commit is contained in:
Shuery committed 2026-09-27 22:40:54 +08:00
1 parent 400f2ad9a5
commit d72fe63c02
3 files changed
+87 -23

No files matched your search

+15 -1
View File
@@ -1,10 +1,24 @@
# 变更日志 # 变更日志
面向使用者的变更记录。更早的历史在文末「相对旧版修了什么」一节。 面向使用者的变更记录。更早的历史在文末「相对旧版修了什么」一节。
## 2026-09 强化与重构 ## 2026-09 强化与重构
本次改造的每一处修复都有实测证据,不是"看起来更规范了"。 本次改造的每一处修复都有实测证据,不是"看起来更规范了"。
### 新增:交互界面(TUI)与入口重组
| 变化 | 说明 |
| --- | --- |
| 主入口 `Manage-Backup.ps1` | 不带参数进菜单(备份 / 恢复 / 配置);带 `-Action` 直接做该动作,可配 `-Quiet` 走无头 |
| 配置入口 `Edit-Config.ps1` | 三个编辑器:清单(改方向)、设置(改单行标量)、名录(改 `Encrypt` / `Description`) |
| 改名:`Backup.ps1` → `Backup-Data.ps1`、`Restore.ps1` → `Restore-Data.ps1` | 旧名字留**垫片**转发,退出码与输出原样传递;只留一轮 |
| 零依赖 TUI | 只用 `RawUI.ReadKey` / `[Console]` / `Write-Host`:不装模块、不带 DLL,两个 PowerShell 版本都能跑 |
编辑器改配置是**外科式改写**:只动被编辑的那一行(注释、对齐、`$( )` 表达式、跨行拼接一字节不动),
先校验再原子替换,落盘前留时间戳副本到 `logs\config-backups\`。三项都写成了判据(含"把每个字段设成它
当前的值、文件必须逐字节相同"这条往返断言),并跑在 Windows PowerShell 5.1 与 PowerShell 7 上。
旧命令(`.\Backup.ps1 -DryRun` 等)**照旧可用** —— 垫片会转发并原样带出退出码。
### 修复 ### 修复
+20 -1
View File
@@ -1,4 +1,4 @@
# BakNRet # BakNRet
一个 Windows 备份 / 恢复工具:把机器上指定的软件与目录收进归档,并能把它们放回原位。本文件是本项目的术语表——「要处理什么」「东西放在哪」这些概念在本仓库里只有一个叫法,写作与命名都照这里的词来。 一个 Windows 备份 / 恢复工具:把机器上指定的软件与目录收进归档,并能把它们放回原位。本文件是本项目的术语表——「要处理什么」「东西放在哪」这些概念在本仓库里只有一个叫法,写作与命名都照这里的词来。
@@ -71,3 +71,22 @@ _Avoid_: 无用归档、残留、垃圾文件
**BakNRet**: **BakNRet**:
本工具的名称,任何场合(文档、文件名、标识符)都一律写作 `BakNRet`。 本工具的名称,任何场合(文档、文件名、标识符)都一律写作 `BakNRet`。
_Avoid_: BakNRet、baknret、BakRet、BaknRet 备份工具 _Avoid_: BakNRet、baknret、BakRet、BaknRet 备份工具
## 模块与入口
`BakNRet/` 是 PowerShell **模块**:既能被 `Import-Module` 直接用,也是四个入口脚本背后的实现层。
| 位置 | 是什么 | 规矩 |
| --- | --- | --- |
| `BakNRet/BakNRet.psd1` | 模块清单 | `FunctionsToExport` 是**显式白名单**:没列进去的不会出现在外面 |
| `BakNRet/BakNRet.psm1` | 加载器 | **唯一**声明点源顺序的地方(Public 再 Private);别处不许自己点源模块内部文件 |
| `BakNRet/Public/` | 对外函数 | **一个函数一个文件、文件名 = 函数名**;公共函数只放这里 |
| `BakNRet/Private/` | 内部实现 | 4 个映射/读取辅助函数 + `State.ps1`(模块状态:编码、日志句柄、运行锁) |
根目录的四个入口脚本 —— `Manage-Backup.ps1`(主入口,TUI + `-Action`)、`Backup-Data.ps1`、
`Restore-Data.ps1`、`Edit-Config.ps1` —— 负责"解析参数 → 干活 → `exit` 退出码"。
**编排逻辑目前仍在 `Backup-Data.ps1` / `Restore-Data.ps1` 里**(`Invoke-BackupItem` 等),把它们下沉进模块是
已被记录、尚未实施的下一步:做完之后一份编排就能同时服务 TUI 与无头两条路,TUI 也不必再起子进程
(见 `docs/adr/0012`)。层与层的分工见 `docs/adr/0001`。
+52 -21
View File
@@ -1,6 +1,6 @@
# BakNRet # BakNRet
把 `BackupList.txt` 里列出的软件 / 目录用 **7-Zip** 打包进 `Backups/`,并且能用 `Restore.ps1` 原样恢复的 Windows 备份工具。 把 `BackupList.txt` 里列出的软件 / 目录用 **7-Zip** 打包进 `Backups/`,并且能用 `Restore-Data.ps1` 原样恢复的 Windows 备份工具。
- 清单里**直接写软件名**即可(如 `Edge`),目录映射维护在 `SoftwareCatalog.psd1` 里。 - 清单里**直接写软件名**即可(如 `Edge`),目录映射维护在 `SoftwareCatalog.psd1` 里。
- 一个软件一个归档:**归档名 = 软件名**(`Edge.7z`),归档内按名录里的 **Slot 分层** - 一个软件一个归档:**归档名 = 软件名**(`Edge.7z`),归档内按名录里的 **Slot 分层**
@@ -24,26 +24,26 @@
```powershell ```powershell
# 1. 先试运行:只打印计划,不写任何文件 # 1. 先试运行:只打印计划,不写任何文件
.\Backup.ps1 -DryRun .\Backup-Data.ps1 -DryRun
# 2. 正式备份 # 2. 正式备份
.\Backup.ps1 .\Backup-Data.ps1
# 3. 强制重打(忽略"源未更新"判断) # 3. 强制重打(忽略"源未更新"判断)
.\Backup.ps1 -Force .\Backup-Data.ps1 -Force
# 3b. 确认可以接受"有文件被占用而没打进归档"时,允许覆盖完整归档 # 3b. 确认可以接受"有文件被占用而没打进归档"时,允许覆盖完整归档
.\Backup.ps1 -Force -AcceptWarnings .\Backup-Data.ps1 -Force -AcceptWarnings
# 4. 只备份 / 只恢复某几项(通配符匹配清单条目或归档名) # 4. 只备份 / 只恢复某几项(通配符匹配清单条目或归档名)
.\Backup.ps1 -Only 'Edge','OpenSSH' .\Backup-Data.ps1 -Only 'Edge','OpenSSH'
.\Restore.ps1 -Only 'Edge' -Force .\Restore-Data.ps1 -Only 'Edge' -Force
# 5. 恢复前先看计划(恢复会覆盖真实目录,务必先看一眼) # 5. 恢复前先看计划(恢复会覆盖真实目录,务必先看一眼)
.\Restore.ps1 -DryRun .\Restore-Data.ps1 -DryRun
# 6. 只校验所有归档完整性,不解压(只读,安全) # 6. 只校验所有归档完整性,不解压(只读,安全)
.\Restore.ps1 -VerifyOnly .\Restore-Data.ps1 -VerifyOnly
``` ```
## 文件说明 ## 文件说明
@@ -53,7 +53,10 @@
| `SoftwareCatalog.psd1` | **软件名 → Slot 组**的映射:每个 Slot 是一个目录/文件,以及它的排除、追加、加密、说明 | | `SoftwareCatalog.psd1` | **软件名 → Slot 组**的映射:每个 Slot 是一个目录/文件,以及它的排除、追加、加密、说明 |
| `BackupList.txt` | 备份 / 恢复共用的清单,唯一的"要处理什么"来源 | | `BackupList.txt` | 备份 / 恢复共用的清单,唯一的"要处理什么"来源 |
| `BackupConfig.psd1` | 目录、空间阈值、校验、加密等配置 | | `BackupConfig.psd1` | 目录、空间阈值、校验、加密等配置 |
| `Backup.ps1` / `Restore.ps1` | 备份 / 恢复入口 | | `Manage-Backup.ps1` | **主入口**:不带参数进 TUI 菜单;带 `-Action Backup\|Restore\|Config` 直接做该动作(配 `-Quiet` 走无头) |
| `Backup-Data.ps1` / `Restore-Data.ps1` | 备份 / 恢复动作本身(无头,可在计划任务里直接调) |
| `Edit-Config.ps1` | 配置管理:清单 / 设置 / 名录三个界面(见下节) |
| `Backup.ps1` / `Restore.ps1` | **旧名字,转发用的垫片**:内部实现已改名为上面两个,这层只留一轮,删它的时机是大家都改用新名字之后 |
| `BakNRet/` | 模块:`BakNRet.psd1`(清单,`FunctionsToExport` 是显式白名单)+ `BakNRet.psm1`(薄加载器,点源顺序只在这里出现一次)+ `Public/`(62 个对外函数,一函数一文件)+ `Private/`(内部函数与模块级状态) | | `BakNRet/` | 模块:`BakNRet.psd1`(清单,`FunctionsToExport` 是显式白名单)+ `BakNRet.psm1`(薄加载器,点源顺序只在这里出现一次)+ `Public/`(62 个对外函数,一函数一文件)+ `Private/`(内部函数与模块级状态) |
| `test.ps1` | **唯一验收入口**:Encode + Parse + Unit + Smoke + E2E,在 7 与 5.1 上各跑一遍 | | `test.ps1` | **唯一验收入口**:Encode + Parse + Unit + Smoke + E2E,在 7 与 5.1 上各跑一遍 |
| `PSScriptAnalyzerSettings.psd1` | 静态分析配置(三条有意排除与 160 字符行长,理由见 `docs/adr/0008`) | | `PSScriptAnalyzerSettings.psd1` | 静态分析配置(三条有意排除与 160 字符行长,理由见 `docs/adr/0008`) |
@@ -118,8 +121,8 @@
| `sourceFiles` / `sourceBytes` / `archiveBytes` | 源文件数、源大小、归档大小 | | `sourceFiles` / `sourceBytes` / `archiveBytes` | 源文件数、源大小、归档大小 |
| `lastSuccessAt` / `successCount` / `failCount` / `lastRestoreAt` / `encrypted` | 历史与安全标记 | | `lastSuccessAt` / `successCount` / `failCount` / `lastRestoreAt` / `encrypted` | 历史与安全标记 |
`Restore.ps1` **优先用 manifest 定位归档**,查不到才退回"从文件名反推路径"。 `Restore-Data.ps1` **优先用 manifest 定位归档**,查不到才退回"从文件名反推路径"。
如果 `BackupList.txt` 丢了,`Restore.ps1` 会优先用 manifest 里的 `source` 自动重建。 如果 `BackupList.txt` 丢了,`Restore-Data.ps1` 会优先用 manifest 里的 `source` 自动重建。
## 备份前空间预估 ## 备份前空间预估
@@ -189,7 +192,7 @@
# 口令来源(二者取其一) # 口令来源(二者取其一)
$env:BAKNRET_PASSWORD = '...' # 或 $env:BAKNRET_PASSWORD = '...' # 或
.\Backup.ps1 -KeyFile 'D:\secret\baknret.key' # 文件首行即口令 .\Backup-Data.ps1 -KeyFile 'D:\secret\baknret.key' # 文件首行即口令
``` ```
一个软件一个归档:名录里各 Slot 的 `Encrypt` 不一致时,**整个归档按加密处理**(宁可多加密,不可漏加密), 一个软件一个归档:名录里各 Slot 的 `Encrypt` 不一致时,**整个归档按加密处理**(宁可多加密,不可漏加密),
@@ -218,14 +221,14 @@ $env:BAKNRET_PASSWORD = '...'
Encryption = @{ Enabled = $false; PasswordFile = (Join-Path $env:USERPROFILE '.baknret.key'); EncryptHeaders = $true } Encryption = @{ Enabled = $false; PasswordFile = (Join-Path $env:USERPROFILE '.baknret.key'); EncryptHeaders = $true }
# C. 单次指定 # C. 单次指定
.\Backup.ps1 -KeyFile (Join-Path $env:USERPROFILE '.baknret.key') .\Backup-Data.ps1 -KeyFile (Join-Path $env:USERPROFILE '.baknret.key')
``` ```
取不到口令时,加密条目**明确失败**,绝不退化成明文归档 —— 这条行为没有放宽。 取不到口令时,加密条目**明确失败**,绝不退化成明文归档 —— 这条行为没有放宽。
> 从旧版本迁移:如果你的口令文件已经在仓库根(`baknret.key`),**什么都不用做**。 > 从旧版本迁移:如果你的口令文件已经在仓库根(`baknret.key`),**什么都不用做**。
> 想改用仓库外的位置,把它移走后按上面 B 或 C 指过去,并先用 > 想改用仓库外的位置,把它移走后按上面 B 或 C 指过去,并先用
> `pwsh -File .\Restore.ps1 -VerifyOnly -Only "WindowsTerminal" -KeyFile <新路径>` 验一下 > `pwsh -File .\Restore-Data.ps1 -VerifyOnly -Only "WindowsTerminal" -KeyFile <新路径>` 验一下
> 口令对不对(那份归档是加密的,口令不对会报错)。 > 口令对不对(那份归档是加密的,口令不对会报错)。
## 计划任务 ## 计划任务
@@ -235,7 +238,7 @@ Encryption = @{ Enabled = $false; PasswordFile = (Join-Path $env:USERPROFILE '.b
.\tools\Register-BackupTask.ps1 -Remove # 移除 .\tools\Register-BackupTask.ps1 -Remove # 移除
``` ```
任务调用 `Backup.ps1`,脚本自身写日志并按失败数返回退出码,所以「上次运行结果」在任务计划程序里可读。 任务调用 `Backup-Data.ps1`,脚本自身写日志并按失败数返回退出码,所以「上次运行结果」在任务计划程序里可读。
## 测试 ## 测试
@@ -243,11 +246,11 @@ Encryption = @{ Enabled = $false; PasswordFile = (Join-Path $env:USERPROFILE '.b
| 套件 | 命令 | 需要什么 | 覆盖 | | 套件 | 命令 | 需要什么 | 覆盖 |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| **Pester 套件**(推荐) | `.\tests\Run-Pester.ps1` | Pester 5.0+ 与 7z | 183 项(含安全描述符套件):清单语法(方向 / `::` / `:-` / `:+` / `:encrypt` / `@ Key='值'` / 整行引号与记号边界)、Slot 结构名录、归档命名、排除翻译(`-x!` / `-xr!` / `!re:`)、Slot 前缀分配、暂存、manifest / 配置 / 名录,外加**用子进程真正跑 `Backup.ps1` / `Restore.ps1`** 的端到端与回归 | | **Pester 套件**(推荐) | `.\tests\Run-Pester.ps1` | Pester 5.0+ 与 7z | 183 项(含安全描述符套件):清单语法(方向 / `::` / `:-` / `:+` / `:encrypt` / `@ Key='值'` / 整行引号与记号边界)、Slot 结构名录、归档命名、排除翻译(`-x!` / `-xr!` / `!re:`)、Slot 前缀分配、暂存、manifest / 配置 / 名录,外加**用子进程真正跑 `Backup-Data.ps1` / `Restore-Data.ps1`** 的端到端与回归 |
| 零依赖套件 | `.\tests\Run-Tests.ps1` | 只要 PowerShell + 7z | 111 项:同样的单元面,适合没装 Pester 的机器 | | 零依赖套件 | `.\tests\Run-Tests.ps1` | 只要 PowerShell + 7z | 111 项:同样的单元面,适合没装 Pester 的机器 |
| 端到端验收 | `.\tests\Run-E2E.ps1` | 只要 PowerShell + 7z | 36 项:备份 → 确认排除生效 → 删源 → 恢复 → 逐字节对拍,含 `<Slot>\` 布局、文件 Slot、方向标记与旧布局回退 | | 端到端验收 | `.\tests\Run-E2E.ps1` | 只要 PowerShell + 7z | 36 项:备份 → 确认排除生效 → 删源 → 恢复 → 逐字节对拍,含 `<Slot>\` 布局、文件 Slot、方向标记与旧布局回退 |
| **真实归档恢复演练** | `.\tests\Restore-Drill.ps1` | 只要 PowerShell + 7z | 12 个真实归档:解到临时目录再和活源逐字节对拍(全程不碰真实目录) | | **真实归档恢复演练** | `.\tests\Restore-Drill.ps1` | 只要 PowerShell + 7z | 12 个真实归档:解到临时目录再和活源逐字节对拍(全程不碰真实目录) |
| **安全描述符套件** | `.\tests\Run-Pester.ps1`(内含 `BakNRet.Security.Tests.ps1`) | Pester 5 + 7z | 含在上面 183 项里:排除判定与 7z `-x!/-xr!` 语义对齐、SID 映射边界(前缀 SID 不被误伤)、采集与 sidecar 往返、回放(`CREATOR OWNER` + 孤儿 SID + `protected` 逐字节一致)、以及真的用子进程跑 `Backup.ps1`/`Restore.ps1` 做端到端 | | **安全描述符套件** | `.\tests\Run-Pester.ps1`(内含 `BakNRet.Security.Tests.ps1`) | Pester 5 + 7z | 含在上面 183 项里:排除判定与 7z `-x!/-xr!` 语义对齐、SID 映射边界(前缀 SID 不被误伤)、采集与 sidecar 往返、回放(`CREATOR OWNER` + 孤儿 SID + `protected` 逐字节一致)、以及真的用子进程跑 `Backup-Data.ps1`/`Restore-Data.ps1` 做端到端 |
演练会把"源在备份之后变过"和"归档/解压有问题"分开:内容不一致时看活源文件的修改时间, 演练会把"源在备份之后变过"和"归档/解压有问题"分开:内容不一致时看活源文件的修改时间,
晚于归档时间就算"源变了"(只提示),不晚于归档时间却内容不同才算失败。真实机器上的归档 晚于归档时间就算"源变了"(只提示),不晚于归档时间却内容不同才算失败。真实机器上的归档
@@ -265,7 +268,7 @@ Install-Module Pester -Scope CurrentUser -MinimumVersion 5.0.0
`Run-Pester.ps1` 优先使用 `.tools/` 里的本地副本,其次是机器上已装的 5.x; `Run-Pester.ps1` 优先使用 `.tools/` 里的本地副本,其次是机器上已装的 5.x;
`.tools/` 已进 `.gitignore`。 `.tools/` 已进 `.gitignore`。
Pester 套件里的端到端用例是**用子进程**跑 `Backup.ps1` / `Restore.ps1` 的,原因有二: Pester 套件里的端到端用例是**用子进程**跑 `Backup-Data.ps1` / `Restore-Data.ps1` 的,原因有二:
两个脚本结尾都会 `exit`,同进程 `&` 调用会把 Pester 宿主一起带走;而且子进程给出的是 两个脚本结尾都会 `exit`,同进程 `&` 调用会把 Pester 宿主一起带走;而且子进程给出的是
真正的进程退出码,正好独立验证"退出码取法"这条修复。 真正的进程退出码,正好独立验证"退出码取法"这条修复。
@@ -320,7 +323,7 @@ Hyper-V 虚拟机(`gsudo pwsh -File .\tools\lab\Lab.ps1 ...`),在那里跑
| 步骤 | 它验证什么 | 最近一次结果 | | 步骤 | 它验证什么 | 最近一次结果 |
| --- | --- | --- | | --- | --- | --- |
| `Lab.ps1 test -Suite all` | 三套仓库测试在干净系统上能不能跑 | Pester 183 / 零依赖 111 / 端到端 36,全部通过 | | `Lab.ps1 test -Suite all` | 三套仓库测试在干净系统上能不能跑 | Pester 183 / 零依赖 111 / 端到端 36,全部通过 |
| `Lab.ps1 backup` | 在 VM 内真跑 `Backup.ps1`(沙盒清单 + 配置) | 退出码 0 | | `Lab.ps1 backup` | 在 VM 内真跑 `Backup-Data.ps1`(沙盒清单 + 配置) | 退出码 0 |
| `Lab.ps1 restore` | 用真实归档做恢复演练,**逐字节对拍** | 通过 6 / 失败 0(含连接点场景 4/4) | | `Lab.ps1 restore` | 用真实归档做恢复演练,**逐字节对拍** | 通过 6 / 失败 0(含连接点场景 4/4) |
| `Lab.ps1 acl-test` | 安全描述符:属主 / `CREATOR OWNER` / 安全指纹 | 全部通过 19 项(含负对照) | | `Lab.ps1 acl-test` | 安全描述符:属主 / `CREATOR OWNER` / 安全指纹 | 全部通过 19 项(含负对照) |
@@ -376,3 +379,31 @@ Hyper-V 虚拟机(`gsudo pwsh -File .\tools\lab\Lab.ps1 ...`),在那里跑
- **跨机恢复要配 `Security.SidMap`**:本机不存在的 SID 写进 DACL 是安全的(那条 ACE 只是 - **跨机恢复要配 `Security.SidMap`**:本机不存在的 SID 写进 DACL 是安全的(那条 ACE 只是
永不匹配),但写进**属主**会让谁都没有合理所有权 —— 换域 / 换机时请给映射,或接受 永不匹配),但写进**属主**会让谁都没有合理所有权 —— 换域 / 换机时请给映射,或接受
"属主未恢复"的告警。服务账户(`NT SERVICE\X`)的 SID 是按名字算出来的,跨机一致。 "属主未恢复"的告警。服务账户(`NT SERVICE\X`)的 SID 是按名字算出来的,跨机一致。
## 交互界面(TUI)
不带参数运行主入口就会进菜单:
```powershell
.\Manage-Backup.ps1 # 主菜单:备份 / 恢复 / 配置
.\Edit-Config.ps1 # 配置菜单:清单 / 设置 / 名录
.\Edit-Config.ps1 -Target List # 直接进某个编辑器
```
三个编辑器改配置的方式是**外科式改写**:只动你改的那一行(注释、对齐、`$( )` 表达式、跨行拼接一字节不动),
先校验再落盘,落盘前把原文件按时间戳复制到 `logs\config-backups\`。所以改坏了随时能翻回去。
| 界面 | 改什么 | 说明 |
| --- | --- | --- |
| 清单 | `BackupList.txt` | 改方向(`both` / `backup` / `restore`),保留你原来的写法风格(贴着写与留空格都原样) |
| 设置 | `BackupConfig.psd1` | 单行标量设置;值跨行或本身是集合的(如 `SidMap`、`DefaultExcludes`)**不列出**,因为"改一行"对它们没有明确含义 |
| 名录 | `SoftwareCatalog.psd1` | 只有单行的 `Encrypt` 与 `Description` 可改;`Path` 常带动态表达式、`Exclude` 可能是跨行拼接,一律只读 |
### 无头与自动化
- `-Quiet`:不画界面,把被调动作的输出捕获后原样转发出来(计划任务与管道用这个)。
- `-InputScript`:用按键序列驱动界面,门禁就是靠它把"打开菜单 → 选一项 → 改一个值 → 保存"整条流程跑通的。
序列用尽而界面还在等输入时会**报错退出**,绝不退回去读真终端 —— 挂起比失败糟得多。
- 只有真终端才画界面:检测到输出被重定向且没给按键序列时,明确报错并给退出码 2,不会卡住。
设计取舍(为什么零依赖、为什么不做全屏、为什么异常不改退出码)见 `docs/adr/0010` ~ `0013`。