# 发布说明:怎么把插件打成一个交付包
**给谁看**:维护者(发版的人)。
**内容**:打包命令、命名规则、基线规则、校验方式,以及**每次发版必须手写的部分**。
---
## 一、先分清两个「迁移包」
这个词在本项目里指两样**完全不同**的东西,混了会出事:
| | **交付包** | **工作区快照迁移包** |
|---|---|---|
| 是什么 | 给别人装这个插件的 zip | 把**工作区**(补丁、调研、结论)打包迁到另一台机器 |
| 谁做 | `tools/build-package.py` | `rimworld_workspace_archive` `action=export` |
| 装到哪 | DSH 的 `node_modules/@local/` | 直接解压替换工作区 |
| 里面有什么 | 插件本体 + 更新日志 + 安装说明 + SHA256 | 最新那一份工作区快照 |
| 本文讲的是 | ✅ **这个** | ❌ 不是,见 `OPERATIONS.md` §4.1 |
---
## 二、一条命令
```powershell
python tools/build-package.py --baseline <上一版目录>
```
写成 `.py` 是因为它的前身就是 Python,而且**插件里的 `tools/` 本来就有 11 个 `.py`**(`probe-*.py`、`rename-author.py`、`bump-version.py`、`check-package-json.py`),Python 是这套工具的既有依赖。
**先 dry-run 看它会写哪里**:
```powershell
python tools/build-package.py --dry-run
```
### 可选参数
| 参数 | 作用 |
|---|---|
| `--baseline <目录>` | **对比基线**。省略则更新日志会写「变更清单不可用」 |
| `--out-dir <目录>` | 输出位置,默认桌面 |
| `--dry-run` | 只报计划和目标路径,**不写任何文件** |
| `--help` | 打印说明,不改任何东西 |
---
## 三、它产出什么
```
RimCrow日志分析插件_v<版本>_<日期>/
├── rimworld-log-plugin/ 插件本体(整目录,安装就装这个)
├── README.md 一页上手:装哪、怎么升、有什么新
├── 更新日志.md 本版变更 + 历史版本(累积)+ 升级步骤
├── 安装说明.md 完整安装与排错
├── 插件清单.md 每个文件干什么、哪些能删
├── 使用参考.md 能直接复制给 DSH 的一句话清单
├── 块功能介绍.md 块是什么、能干什么、怎么用
└── SHA256SUMS.txt 传输后校验
RimCrow日志分析插件_v<版本>_<日期>.zip
```
**包内固定 6 份文档,缺一不可。** 它们是 `build-package.py` 里的 `REQUIRED_DOCS`(唯一事实来源),
复制与校验共用同一份清单:**打完包立刻硬核,缺任何一份都不生成 zip 并返回退出码 2**。
为什么是硬检查而不是警告:少一份文档的包看起来是完整的,只有打开才发现 —— 而打不出来你会立刻发现。
(`REQUIRED_PLUGIN_FILES` 同理,缺 `cordis.patch.yml` 这类文件装上去直接不工作。)
**版本号从 `package.json` 实时读**,不写在脚本里 —— 否则升版本时这里会漏。
**命名带版本 + 日期**:只带日期的话,「同日重打包」和「升到新版本」会得到同名文件,而这两件事对收包的人意义完全不同。
---
## 四、基线必须显式给,脚本不猜
**这是踩过的坑,写清楚。**
脚本的第一版会从几个「可能的路径」**自动猜**基线。在这个工作区里,它猜中了 `_archive/RimCrow日志分析插件` —— 那是 **1.0 的归档**。于是打 1.6.1 的包时,更新日志写着「新增 60 / 修改 13 / 删除 6」。
**那是六个版本的改动被当成一个版本的更新,而输出里没有任何迹象表明这个对比是错的。** 一份错的更新日志比没有更新日志更糟,因为它看起来权威。
**所以现在**:
- 不给 `--baseline` → 更新日志**明确写「本节不可用」**,并列出全部文件
- 给了但路径不存在 → **直接报错退出**,不静默降级
**基线从哪来?** 上一版的 `rimworld-log-plugin/` 目录,或上一版的交付包解压后的同名目录。
---
## 四之二、基线的维护:永远只留两个版本
**这是操作约定,不是建议。** 同版本号、不同内容,是收包方最容易误解的情况——
「我拿到的 vX 和你说的 vX 不是一回事」,而两边的更新日志都写着自己是 vX。
### 两个角色
| 角色 | 含义 | 放在哪 |
|---|---|---|
| **基线** | **已经分享出去**的那一版,冻结不动 | `_archive/RimCrow日志分析插件_<版本>/` |
| **工作版** | 正在向下做的下一版,还没给别人 | 插件目录 + 交付包 |
### ⚠️ 先分清「分享」和「公开」
| 说法 | 含义 | 对基线的影响 |
|---|---|---|
| **分享** | 发给某个人 / 小范围试用,没有公开发布 | **算数**:这一版就是下一次的基线 |
| **公开** | 发到 GitHub / B站 / 任何公开渠道 | **算数**:同上为基线;但还要先过合规审查(第三方内容、Ludeon 免责声明、LICENSE) |
| 只放在本地 / 桌面 | 没给任何人 | **不算**:不能拿它当基线(本项目出过这个错:拿未公开的本地构建当基线,日志里的差异是错的) |
**约定(用户要求)**:用户说「公开」时,**先问清是「分享」还是「公开」**,再决定是否切基线、是否触发合规审查。
两者都切基线,区别在**公开**多一道合规检查。
### 什么时候切换
**你说「这一版分享出去了」的时候**,就是切换点,做两件事:
1. 把当前工作版目录**复制**进 `_archive/RimCrow日志分析插件_<刚分享的版本>/`(旧的基线可以留着或删,它就是历史)
2. `package.json` 升一个版本号,并在 `HISTORY.md` 顶部加一段
### 每次发版的固定动作
```
1. 改东西
2. node tools/check-all.mjs --python <解释器> ← 五项全绿
3. python tools/build-package.py --baseline _archive/RimCrow日志分析插件_<基线版本>
4. 你看过、发出去
5. 你说「分享了」→ 冻结当前版本为下一轮基线 + 升版本号 + HISTORY.md 加一段
```
### 为什么不能省掉「冻结」
- **基线丢了就写不出准确的更新日志**:本项目就发生过——桌面的 v1.2.0 包被删,`--baseline` 直接 `baseline not found`,
只能临时改用别的目录,那一版的对比就失真了
- **`_archive` 里的 1.0 不能当基线**:那是六个版本之前,会产出「新增 61 / 修改 13 / 删除 6」这种错到离谱的日志
### 两套基线,互相隔离(用户要求)
交付包(非 GitHub)与公开包(GitHub)是**两条独立的发布轨道**,各自冻结自己的基线,**互不引用**:
| 轨道 | 基线目录 | 冻结时机 | 用它做什么 |
|---|---|---|---|
| **交付包轨**(分享/私有) | `_archive/RimCrow日志分析插件_<版本>/` | 你说「**分享**出去了」时 | `build-package.py --baseline` 生成 `更新日志.md` |
| **公开包轨**(GitHub) | `_archive-github/RimCrow日志分析插件_<版本>/` | 你说「**公开**了」时 | `build-public.py --baseline` 生成 `RELEASE-NOTES.md` |
规则:
1. **不许交叉**:公开包的基线只放 `_archive-github/`,交付包的基线只放 `_archive/`;两个 `--baseline` 参数各喂各的目录。
2. **不许混用版本号**:同一版本号可以同时存在于两条轨道的归档里,但各归档的内容是各自那次构建的原样,不改写。
3. **首个公开版本没有公开基线**:`build-public.py` 不带 `--baseline` 时不生成 `RELEASE-NOTES.md`(首发版在 README 里注明即可)。
4. 冻结动作照旧:复制当前工作版目录进对应归档 → 升版本号 → `HISTORY.md` 顶部加一段。
### 公开包的目录布局(容器 + 固定内层)
```
公开包(github版)\ ← 容器(不装 git)
rimworld-log-plugin\ ← 仓库根;git init / commit / push 都在这里
rimworld-log-plugin_v<版本>_<日期>.zip ← 可选快照(--zip)
```
生成命令:
```powershell
python tools/build-public.py --out <容器> --layout nested --repo-name <仓库名> --zip
```
内层用**固定名**(仓库名),保证一条连续的 git 历史;版本化留给 package.json、git tag 与
_archive-github/ 归档,不靠改目录名。--layout flat 保留旧行为(直接写进 --out)。
### 当前状态(随时更新这条表)
| 轨道 | 基线版本 | 位置 | 工作版 |
|---|---|---|---|
| 交付包轨 | v1.6.1(已分享) | `_archive/RimCrow日志分析插件_1.6.1/` | v1.6.9 |
| 公开包轨 | (无——首个公开版本 v1.6.9) | `_archive-github/`(待首次公开后冻结) | v1.6.9 |
---
## 五、⚠️ 每次发版必须重写的正文
**`tools/build-package.py` 里有一大段文案是手写的,不是从 diff 生成的。**
它描述的是「新增了 `rimworld_plugin_guide` 和 `rimworld_plugin_compare`」那次发版的内容。**插件后来从 1.2.0 走到了 1.6.x,这段文字没有跟着变。**
**后果**:拿它打 1.6.x 的包,README 会把旧版本的成果当成这次的新功能 —— **版本号那一行是对的(它实时读),但「本版更新了什么」整段是错的。**
**发版时逐项检查这些位置**(脚本里标了 `STALE-PRONE REGION` 注释):
| 位置 | 里面写的是什么 |
|---|---|
| `README.md` 的「本版更新了什么(一句话)」 | 一句话总结,现在写的是 v1.6.1 |
| `README.md` 的「新增与变化的动作」表 | v1.6.1 新增/变化的三个动作 |
| `更新日志.md` 的「这一版的三件事」 | 本版叙事,**只讲这一版** |
| `HISTORY.md` | **所有版本的累积记录**(见下) |
| `更新日志.md` 的「修改的文件」表 | 每个文件改了什么(脚本里有一份手写补充说明) |
| `更新日志.md` 的「升级步骤」与「三条会失败的做法」 | 若本轮没有机制变化,可以保留 |
### 发版后必须做的一步:把本版段落搬进 `HISTORY.md`
`更新日志.md` 每次打包都会重写,**它只保留本版**。历史能留下来,靠的是 `HISTORY.md`:
1. 这个版本发出去之后,把「这一版的三件事」(以及本轮真正重要的修复)缩写成一段,**插到 `HISTORY.md` 最上面**
2. 格式见该文件现有条目:`## vX.Y.Z — YYYY-MM-DD` + 三到五条要点
3. **不要删旧条目** —— 它是读者判断「我这版缺什么」的唯一依据
漏了这一步不会报错,只会让下一版的历史停在上一版 —— 所以它列在这里,不靠记忆。
> `HISTORY.md` 里 `v1.3 – v1.5` 是「无记录」。那不是疏忽:`bump-version` / `stamp-blocks` 就地覆盖子版本号,
> 登记表里只剩当前的归属,反推不出那三版改了什么。**这条空白本身也写进了文件**,免得后人以为能找到。
**判断标准**:README 里凡是出现「新增 / 本版 / 这次」的句子,念一遍 —— **它说的是这一版的事吗?**
### 为什么不能指望登记表来救
**`blocks.json` 不是变更日志。** 它记录的是**当前**的归属与子版本号,不是历史。
```text
1233 块 → 830 块无版本(早于登记表)/331 块 1.6.0/72 块 1.6.1
```
**1.6.0 以下一个都没有。** 因为 `bump-version` / `stamp-blocks` 是**就地重写**子版本号,旧基准版本被覆盖,不留痕迹。
**所以**:
- **不能**用登记表复现「1.2.0 到 1.6.1 之间改了什么」—— 数据已经不在里面了
- **也不该**用块数的差值去编故事(「块从 900 涨到 1233,说明加了很多功能」这种推断没有依据:重切分、改名、加注释都会改变块数)
**唯一可靠的来源是写它的人的记忆与当时的记录。** 这也是为什么这一节要求「人工重写」而不是「自动生成」—— 能自动的只有文件差异,**而读者想知道的是「这些差异对我有什么意义」。
---
## 六、三条会失败的做法
**① 用 `npm pack` 当交付包**
不行。安装走的是 `install_bundle` 的**整目录 Junction**,`package.json` 的 `files` 白名单**只影响 `npm pack`**,对 Junction 安装无效。所以 `cordis.patch.yml` 不在白名单里却必须在,而 `debug-*.mjs` 不在白名单里却照样随包走。
**结论:想让某文件不随包分发,只有删掉它一条路。**(详见 `插件清单.md` 第一节)
**② 打包前不跑自测**
```powershell
node selftest.mjs
node tools/selftest-plugin.mjs
node tools/selftest-blocks.mjs
node tools/sync-manifest.mjs --dry-run
node tools/stamp-blocks.mjs --dry-run
python tools/check-package-json.py
```
> 后两条是「应该**没有**变化」。它们有变化时不会报错,只会打印变化量 —— 这正是静默漂移的形状。
> `stamp-blocks --dry-run` 还应是 `created 0 / modified 0 / missing 0`。
**②″ 生成物有依赖,顺序错了会看到假的「有变化」**
插件里有**三个**东西互相牵制:
| 生成物 | 依赖什么 | 谁改它 |
|---|---|---|
| `blocks.json`(块登记表) | 源码与文档的当前内容 | `stamp-blocks.mjs` |
| `块功能介绍.md` | 从 `blocks.json` 现算块数与孤儿块 | `gen-blocks-doc.py` |
| `插件清单.md` | 各文件当前大小(**含它自己**) | `write-plugin-manifest.py` / `sync-manifest.mjs` |
所以固定顺序是:
```
node tools/stamp-blocks.mjs # 1. 先盖章(blocks.json 变了)
python tools/gen-blocks-doc.py # 2. 块文档按新块数重算
python tools/write-plugin-manifest.py # 3. 清单按最终大小重算(可能要连跑两轮才稳定)
```
验证只需要一条命令 —— 它按同一顺序跑完五项,并给出「还需要处理什么」:
```
node tools/check-all.mjs --python <解释器路径>
```
**为什么必须现算**:清单会记录**自己**的大小,块文档会记录块数。手抄的数字一定会过期,
而且过期时**不会报错** —— 只会让你以为脚本坏了。实测过:顺序反了会剩下 4 行差异
(合计 KB 与 `blocks.json` 那一条),看着像 bug,其实是顺序问题。
**为什么清单不再记 `blocks.json` 的字节数**:那个数字每次盖章都变,记进去就形成
「盖章 → 清单过期 → 重生成 → 盖章又发现清单变了」的循环。数字对读者没有意义,去掉后循环消失。
**②′ Python 足迹不要进包**
跑过任何 `python tools/xxx.py` 之后,`tools/__pycache__/` 里会出现 `.pyc`。打包脚本已把
`__pycache__` 与 `*.pyc` 排除,`sync-manifest.mjs` 也不再统计它们(曾因此把 76 静默读成 78)。
但**自己的临时产物仍要清**:验证语法用 `compile(source, path, "exec")`,不要用 `py_compile`,
后者会在源码旁边落一个缓存目录。
**`selftest-plugin.mjs` 抓到过一类静默失效**:工具的执行函数键名写错(`handler` 而不是 `execute`),工具会出现在列表里但**永远无法调用**。这种东西不跑自测发现不了。
**③ 直接用自动生成的更新日志**
里面的文件差异是对的,但**叙事部分可能是旧版本的**(见第五节)。**玩家拿到会以为这次真加了那些东西。**
---
## 七、发版验收清单
打完之后,按这个顺序核一遍:
| # | 检查 | 怎么核 |
|---|---|---|
| 1 | 包名带版本 | 文件名里是 `v` |
| 2 | 包内源码与插件一致 | 逐文件 SHA256 比对(不一致数应为 **0**) |
| 3 | 版本号四处一致 | 包名、`package.json`、`更新日志.md` 首行、`README.md` |
| 4 | `SHA256SUMS.txt` 能验 | `Get-FileHash` 抽查几个文件 |
| 5 | **叙事正文是这一版的** | 见第五节,人工读一遍 |
| 6 | 自测全绿 | 第六节②的五条命令 |
| 7 | **zip 非空且能解开** | `(Get-Item *.zip).Length` 不为 0;`Expand-Archive` 能出顶层目录 |
| 8 | 解压试装一次 | 干净环境解开 → `install_bundle` → 重启 DSH → 跑 `/rimlog` |
| 9 | **包内 6 份文档齐全** | 打包时已硬核(缺则退出码 2);也可手工 `Get-ChildItem *.md` 数一遍 |
**第 7 条是补上的。** `build-package.py` 的 zip 步骤曾引用一个未定义的变量(`home`,搬进插件时漏改),打包在最后一步直接崩 —— `README.md` / `更新日志.md` / `SHA256SUMS.txt` 都已经写好了,所以**目录看起来是完整的,只有 zip 是 0 字节**。盯着目录的人不会发现。
**第 8 条最重要,也最容易跳过。** 前七条都能自动化,**只有它验证的是「别人拿到能不能用」**。
---
## 八、这个方案为什么放在插件里
**打包脚本原来在工作区外(`_tools/`),插件里 0 处引用它。**
后果是:别人拿到 zip 能装、能校验,**但没法复现这个包** —— 包里没有一处说明它是怎么打出来的。
搬进来时改了三件事:
| 原来 | 现在 | 为什么 |
|---|---|---|
| 硬编码开发机的工作区绝对路径 | 从脚本自身位置推 `PLUGIN` | 硬编码路径在别人机器上**一定是错的** |
| 硬编码 `_archive/...` 当基线 | `--baseline` 参数 | 见第四节 |
| 只写桌面 | `--out-dir`,默认桌面 | 桌面在别人的机器上不一定存在 |
**这个模式与 `插件清单.md` 一致**:文档 + 一个能把它重新生成/验证的工具。**光有文档会漂,光有工具没人知道怎么用。**