删除旧的事件驱动章节(流水账),并同步文档到人物主线

- 移除 output/volumes/v1/chapters/001-1-9.md 与 002-10-19.md(仅普通提交,git 历史保留)
- 移除被取代的写作规范 prompts/chronicler.md 与旧的按年代切章模块
- README 全面改写为人物主线流程、参数位置与踩坑记录
- scripts/dfannals 启动器:自动挑一个装了 defusedxml 的解释器
This commit is contained in:
Chen Yi
2026-10-05 19:32:41 +08:00
parent e0073e90bd
commit 08a032120a
11 changed files with 164 additions and 833 deletions
+125 -92
View File
@@ -1,151 +1,184 @@
# 矮人要塞编年史 · Dwarf Fortress Annals
# 矮人要塞人物连载 · Dwarf Fortress Annals
把《矮人要塞》(Dwarf Fortress) 世界生成的历史数据,写成**中文战报体连载故事**,自动推送到这个仓库。
把《矮人要塞》(Dwarf Fortress) 世界生成的历史数据,变成**跟着人走的中文连载故事**,自动推送到这个仓库。
本仓库是**成品仓**:放小说章节、迷你年表、人物索引,以及生成它们的代码。
原始 `legends` 导出数据(数百 MB)与游戏存档留在本地,不入库。
不是编年史。编年史的主语是年代,读起来必然像流水账(本项目第一版就是这样被否掉的)。
这里的主语是**人**:先从史料里自动挖出一组关系密切、反复交锋的角色,再按因果链切集,
以"这些人物的传记作者"的视角贴着他们写。
---
## 一、它是怎么工作的
## 一、它怎么工作
```
Windows 侧:Dwarf Fortress 53.16 + DFHack 53.16-r2
│ ① 生成世界(Medium 129×129 / 250 年历史)
│ ② 进入 legends 模式,导出 legends.xml + legends_plus.xml
▼ ③ 经 /mnt/c 文件交换(不需要 WSL↔Windows 网络互访)
WSL2 Ubuntu 侧管道
│ ④ 预处理:剔除 DF 塞进名字里的非法控制字符,CP437 → UTF-8
│ ⑤ 解析史料:40 万条事件 / 4 万位人物 → 事件流 + 年表 + 人物索引
│ ⑥ 切章:10 年一窗,每窗按显著度取前 12 条,同类事件≤4(避免整章讣告)
│ ⑦ 写作:要塞编年史官口吻 / 战报体 / 允许自由演绎
│ ⑧ 校验:正文专名必须能在史料里找到,可疑处标注但不阻断
▼ ⑨ git commit & push
Gitea:gitadmin/dwarf-fortress-annals(本仓库)
① 线索挖掘 从 40 万条事件里建人物关系图,找出关系密切、有戏的 3–6 人小圈子
(只用有叙事含义的互动:对战/结缘/反目/绑架/构陷/迫害……)
② 切集 沿这条线按因果链切集:相邻事件间隔 >2 年断开,短链合并,过长拆上下集
③ 人物表 表格 + 每人一两句小传;"身份"由事件字段反推,不是猜的
④ 写作 主角的传记作者视角:他们的目标、算计、得失做主语;
世界大事只在影响到主角时提及
⑤ 自评闸门 5 个维度各 0–5 分,总分 <12 判"流水账" → 先重写一次 →
仍不合格就换下一条线索,并把放弃原因写进 notes/
⑥ 发布 git commit & push 到本仓库(原始史料与存档不入库)
```
## 二、目录
## 二、这一卷的主角
| | |
|---|---|
| 世界 | Mon Sagus(The Plane of Dawn),Medium 129×129 / 250 年历史 |
| 主角组 | Guspu Frillyknots(鹰人掮客)、Ral Fastenhatchets(矮人)、Alath Blottedmine(矮人) |
| 线索 | 46 年间一个掮客反复收买、构陷一位管货栈的矮人,屡屡失手 |
| 选线依据 | 挖掘器排序第一名:有效互动 11 次、转折 11 次、阴谋×8 + 构陷×3 |
详见 [`output/volumes/v1/cast.md`](output/volumes/v1/cast.md)。
## 三、目录
| 路径 | 内容 | 入库 |
|---|---|---|
| `output/volumes/v<N>/chapters/` | 连载正文 | ✅ |
| `output/volumes/v<N>/cast.md` | 人物表(表格 + 小传) | ✅ |
| `output/volumes/v<N>/timeline.md` | 迷你年表 | ✅ |
| `output/volumes/v<N>/figures.md` | 人物索引 | ✅ |
| `output/volumes/v<N>/figures.md` | 人物索引(按史料出场次数排序) | ✅ |
| `notes/switched-threads.md` | 被放弃的线索及原因 | ✅ |
| `dfannals/` | 管道源码 | ✅ |
| `scripts/` | 安装、部署、驱动脚本 | ✅ |
| `prompts/chronicler.md` | 写作规范(决定文风与人设) | ✅ |
| `tests/` | 事实校验回归测试(含合成史料样例) | ✅ |
| `prompts/biographer.md` | 写作规范(决定视角、边界、篇幅) | ✅ |
| `scripts/` | 安装、部署、驱动、启动器 | ✅ |
| `tests/` | 回归测试(解析、切集、闸门、事实校验) | ✅ |
| `data/exports/` | 原始 legends 数据与预处理缓存 | ❌ |
| `data/state.json` | 进度状态(当前世界 / 卷号 / 已写章节) | ❌ |
| `data/GRILL-ME.md` | 立项访谈记录 | ❌ |
| `data/state.json`、`data/threads.json`、`data/selection.json` | 进度、候选、选线依据 | ❌ |
## 三、依赖
## 四、依赖
| 依赖 | 版本 / 说明 |
|---|---|
| Dwarf Fortress | **53.16** 免费版(Bay 12)。必须与 DFHack 严格配对 |
| DFHack | **53.16-r2**。Windows 版经 `dfhooks.dll` 注入,不需要替换启动器 |
| Python | 3.12(用到 `X \| None` 语法) |
| defusedxml | `sudo apt install python3-defusedxml`(解析不可信 XML 用) |
| 写作模型 | 经 `https://wb2api.hajim1.art/v1`(OpenAI 兼容)调用 `cn:deepseek-v4-pro` |
| Dwarf Fortress | **53.16** 免费版(Bay 12),必须与 DFHack 严格配对 |
| DFHack | **53.16-r2**,Windows 版经 `dfhooks.dll` 注入,不必替换启动器 |
| Python | 3.12 |
| defusedxml | `sudo apt install -y python3-defusedxml` |
| 写作/自评模型 | 经 `https://wb2api.hajim1.art/v1`(OpenAI 兼容)调用 `cn:deepseek-v4-pro` |
| 发布 | 现有 `~/.ssh/id_ed25519`(已注册到 Gitea 账号 gitadmin) |
> 无独显也能跑:DF 本体不吃 GPU;写故事走云端 API,不做本地推理。
**启动器**:一律用 `scripts/dfannals ...`,不要直接 `python3 -m dfannals.cli`。
会话 PATH 上的 `python3` 可能是编辑器工具链自建的 venv(没有 defusedxml,
且关闭了 user site),启动器会自动挑一个能 `import defusedxml` 的解释器。
## 四、安装
## 五、安装
```bash
# 1) Windows 侧装游戏与 DFHack(经 WSL 的 /mnt/c 操作,免管理员权限)
scripts/install-df.sh --download # Bay12 与 GitHub 在国内较慢,会断点续传
# GitHub 直连不通时改用镜像:https://gh-proxy.com/<原地址>
# 2) 装依赖
scripts/install-df.sh --download # 装 DF + DFHack(Bay12 慢,会断点续传)
sudo apt install -y python3-defusedxml
# 3) 把驱动脚本部署进游戏目录
scripts/deploy-dfhack.sh /mnt/c/Users/$USER_WIN/DwarfFortress
scripts/deploy-dfhack.sh /mnt/c/Users/$USER_WIN/DwarfFortress # 部署观测/驱动脚本
```
## 五、产出一卷(生成新世界 → 导出 → 连载)
## 六、产出一卷
```bash
# 1) 启动游戏,停在标题界面
"/mnt/c/Users/23518/DwarfFortress/Dwarf Fortress.exe" &
# 1) 生成新世界并导出 legends(详见第九节)
scripts/make-world.sh 3 3 # 3 = Medium 大小,3 = 250 年
# 2) 自动生成世界(Medium / 250 年)并导出 legends 数据到 data/exports/
scripts/make-world.sh 3 3
# 2) 建索引:年表 + 人物索引
scripts/dfannals index
# 3) 一条命令产出下一章并推送
python3 -m dfannals.cli run
# 3) 看候选线索(含信号明细),挑一条
scripts/dfannals threads --top 8 --samples 2
# 4) 看这条线切出的剧集与预算
scripts/dfannals episodes --thread 1
# 5) 生成人物表
scripts/dfannals cast --thread 1
# 6) 逐集产出(自动过闸门;不合格自动重写,再不合格自动换线索)
scripts/dfannals episode --index 1
scripts/dfannals episode --index 2
scripts/dfannals episode --index 3
```
`make-world.sh` 全程从外部驱动游戏,不需要人工点击:
- 用 `dfannals/screentext` 把画面读成字符网格,`scripts/df-screen.py` 按文字定位按钮
- 用 `dfannals/click` 模拟鼠标、`dfannals/setparams` 直接设定世界参数
- 用 `dfannals/legends` 进入 legends 模式(做法取自 DFHack 官方 `open-legends.lua`)
- 用 `dfannals/act` / `devel/send-key` 发按键
> 备选:DF 自带命令行世界生成器 `"Dwarf Fortress.exe" -gen 1 RANDOM "MEDIUM ISLAND"`,
> 可静默生成并导出,但**不留存档**,因此无法继续进 legends 导出,只适合快速验证参数。
## 六、子命令
```bash
python3 -m dfannals.cli status # 当前世界 / 卷号 / 已写章节
python3 -m dfannals.cli plan # 只看切章方案,不调模型
python3 -m dfannals.cli index # 解析 exports → 年表 + 人物索引
python3 -m dfannals.cli next # 生成下一章(--dry-run 只生成不推送)
python3 -m dfannals.cli run # index + next,一条命令走完
python3 tests/test_factcheck.py # 校验器回归测试
```
实测:**约 2.5 分钟 / 章**(解析 33 秒 + 模型 1–2 分钟),单章 1000–1400 字。
实测:**单集约 2–4 分钟**(写作 1 次 + 自评 1 次,若判平淡会多一次重写),篇幅 800–1500 字。
## 七、配置在哪
| 想改什么 | 改哪 |
|---|---|
| 世界大小 / 历史长度 | `scripts/make-world.sh` 的两个档位参数 |
| 切章粒度(几年一章、每章几条、同类上限) | `dfannals/slice.py` 顶部常量 |
| 哪些事件值得写(显著度评分) | `dfannals/score.py` 的 `NARRATIVE` 表 |
| 文风、人设、演绎边界 | `prompts/chronicler.md` |
| 章节字数区间 | `dfannals/config.py` 的 `CHAPTER_MIN/MAX_CHARS` |
| 哪些互动算"关系"、权重、转折标记 | `dfannals/threads.py` 的 `SIGNALS` |
| 主线口径(≥5 次互动、3–6 人、跨度 ≥30 年) | `dfannals/threads.py` 顶部常量 |
| 切集参数(间隔 >2 年断开、每集预算) | `dfannals/episodes.py` 顶部常量 |
| 自评维度、阈值 12、重写/换线上限 | `dfannals/gate.py` 的 `DIMENSIONS` / `THRESHOLD` / `MAX_REWRITES` / `MAX_CANDIDATES` |
| 视角、文风、演绎边界、篇幅 | `prompts/biographer.md` |
| 同人同类事件的重复上限 | `dfannals/volume.py` 的 `PER_KEY_CAP` |
| 模型与网关 | `dfannals/config.py` 的 `PROVIDER` / `MODEL` |
| 发布目标、仓库名 | `dfannals/config.py` 的 `GITEA_*` |
**API key 不入库**:脚本运行时从 `~/.pi/agent/auth.json` 读取,只在内存中,不打印、不落盘、不进 git。
**API key 不入库**:运行时从 `~/.pi/agent/auth.json` 读取,只在内存中,不打印、不落盘、不进 git。
## 八、踩过的坑(失败排查参考)
## 八、子命令
```bash
scripts/dfannals status 当前世界 / 卷号 / 已完成集数
scripts/dfannals index 解析 exports → 年表 + 人物索引
scripts/dfannals threads 候选线索与信号明细
scripts/dfannals episodes --thread N 某条线索的剧集规划与预算
scripts/dfannals cast --thread N 人物表(--no-bios 只出表格)
scripts/dfannals episode --index N 产出第 N 集(--dry-run 不落盘)
```
测试:`/usr/bin/python3 tests/test_factcheck.py`、`test_episodes.py`、`test_gate.py`。
## 九、世界是怎么造出来的
`scripts/make-world.sh` 全程从外部驱动游戏,不需要人工点击:
- `dfannals/screentext` 把画面读成字符网格,`scripts/df-screen.py` 按文字定位按钮
- `dfannals/click` 模拟鼠标,`dfannals/setparams` 直接改内存里的世界生成参数
- `dfannals/legends` 进入 legends 模式(做法取自 DFHack 官方 `open-legends.lua`)
- 导出 `Export XML`(原生 legends.xml)与 `exportlegends`(扩展数据)
> 备选:`"Dwarf Fortress.exe" -gen 1 RANDOM "MEDIUM ISLAND"` 可静默生成并导出,
> 但**不留存档**,无法继续进 legends,只适合快速验证参数。
## 十、踩过的坑(失败排查参考)
| 症状 | 原因与解法 |
|---|---|
| `not well-formed (invalid token)` | DF 在名字里嵌了 `\x10`/`\x11` 等 CP437 控制字符,XML 1.0 禁止。管道开头的预处理会剔除并转 UTF-8,缓存到 `data/exports/clean/` |
| `not well-formed (invalid token)` | DF 在名字里嵌了 `\x10`/`\x11` 等 CP437 控制字符,XML 1.0 禁止。预处理会剔除并转 UTF-8,缓存到 `data/exports/clean/` |
| 事件数莫名翻倍 | 预处理缓存若与原始文件同目录,会被 `*.xml` 扫描当成第二份史料。缓存因此放在子目录 |
| 素材里出现 `HF#-1`、`Site#-1` | DF 用负数占位表示"无此项"。查询接口对负 id 返回空串,渲染时跳过 |
| 人物关系在素材里丢失 | 真实史料用 `hfid_target`/`group_1_hfid`/`snatcher_hfid`/`seeker_hfid`/`hfid1`… 共 40+ 个字段表达关系;只认 `hfid` 会把"谁对谁做了什么"丢掉。现按"字段名含 hfid 即为人物"分类 |
| 校验器把真名报成可疑 | 史料里是全小写程序名(`yemi deermoths`),正文做了首字母大写。已知名字集同时收录两种形式(有回归测试) |
| 候选线索全是"同一对人反复对战" | 裸互动次数会被机械重复刷满(第一名 49 次里有 35 次是"反复请求拜师被拒")。现剔除重复性请求,并对同人同类事件封顶、要求 ≥2 种信号 |
| 主角组里混进泰坦巨兽 | 用"文明种族白名单(五族 + `*_MAN`)",实测覆盖 96.5% 的历史人物 |
| 一集被均分成两个 770 字的碎片 | 均分只看条数、不看预算。改为先取最小段数,再确保每段不低于下限 |
| 自评/小传返回空正文 | 推理模型的思考与正文共用 max_tokens,喂长材料时会烧光预算。结构化输出统一用 `MAX_TOKENS_STRUCTURED` |
| `ModuleNotFoundError: defusedxml` | PATH 上的 `python3` 被编辑器工具链的 venv 抢走了。用 `scripts/dfannals` 启动器 |
| `dfhack-run "cmd a b"` 报 not recognized | 参数要分开传:`dfhack-run dfannals/click 10 2`,不要整体加引号 |
| 切出几千章 | 40 万条事件里绝大多数是流水账(`change hf state` 7.3 万条)。必须先按显著度筛选,再按时间窗分桶 |
| Bay12 下载只有 ~10KB/s | 官网无国内镜像。放后台断点续传即可(16MB 约 27 分钟) |
| GitHub release 下载 0 字节 | 直连被断。用 `https://gh-proxy.com/` 前缀 |
| 观测 / init 脚本不生效 | 53.16 只执行固定几个 init 文件。改用 `dfhack-run` 从外部驱动,更可靠 |
| Bay12 下载只有 ~10KB/s;GitHub 直连 0 字节 | 官网无国内镜像,放后台断点续传;GitHub 用 `https://gh-proxy.com/` 前缀 |
| 自定义 init 脚本不生效 | 53.16 只执行固定几个 init 文件。改用 `dfhack-run` 从外部驱动更可靠 |
## 九、写作设定
## 十一、闸门到底能不能判出流水账
- **叙述者**:世界档案库里的编年史官——少数亲历,多数事后依据档案与口述追述,因此对陈年旧账有看法
- **口吻**:战报体,严肃的骨架 + 诙谐的血肉,允许吐槽
- **语言**:中文;专有名词保留英文原文(首字母大写以便阅读)
- **演绎边界**:允许虚构对白、心理、场景;**不许发明史料里没有的人名地名**,不许改写史实骨架(年份、参与者、胜负)
- **连载**:同一世界长期连载;一部史写完则用 `make-world.sh` 生成新世界开新卷
`scripts/gate-selfcheck.py` 是这个闸门的校准证据(跑两次真实自评):
## 十、当前状态与限制
| 样本 | 总分 | 判定 |
|---|---|---|
| 故意构造的平淡稿(纯事件罗列) | 0/25 | 判平淡 ✓ |
| 场景级有戏剧本 | 16/25 | 通过 ✓ |
- **已完成**:3 卷框架 + 第 1、2 章上线(Mon Sagus 世界,1–250 年,26 章规划)
- **定时任务未启用**:`cron` / 开机自启等稳定运行一段时间后再落地;目前按需手动执行
阈值 12 在两者之间,区分有效。注意:阈值是校准出来的,不是拍出来的——
第一版用"梗概体"当好稿样本时只得 11 分,换了场景级样本才验证出尺度没偏严。
## 十二、当前状态与限制
- **已完成**:第 1 卷 3 集上线 + 人物表 + 年表;旧的两章流水账已用普通提交删除(历史保留)
- **定时任务未启用**:cron / 开机自启等稳定运行一段时间后再落地,目前按需手动执行
- **原始数据不入库**:`data/` 已在 `.gitignore` 中
- **诚实说明**:世界创建各步骤均已逐步实测通过,并已整合为 `make-world.sh`;但该脚本尚未作为**单次完整调用**重跑验证(单次约 15 分钟),首次使用时请留意各阶段日志
- **已知局限**:
- 关系图的边来自成对事件,独狼型人物(独自创作、被自然杀死)进不了线索挖掘
- 多参与者事件(比赛、集体迫害)会把同场者两两连边,圈子的"关系"偏共处而非互动
- 事实校验只拦得住凭空专名;动机、心理、对白全是允许的演绎,没有自动防线
## 十一、声明
## 十三、声明
《Dwarf Fortress》版权归 Bay 12 Games 所有。本仓库只包含由其生成数据的**衍生文本**,
不含游戏本体、美术资源或存档。