opencode-wiki-historian 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +381 -0
  3. package/dist/chronology.d.ts +36 -0
  4. package/dist/chronology.js +67 -0
  5. package/dist/config.d.ts +112 -0
  6. package/dist/config.js +158 -0
  7. package/dist/index.d.ts +31 -0
  8. package/dist/index.js +136 -0
  9. package/dist/jsonc.d.ts +17 -0
  10. package/dist/jsonc.js +131 -0
  11. package/dist/map.d.ts +58 -0
  12. package/dist/map.js +196 -0
  13. package/dist/migrate-apply.d.ts +40 -0
  14. package/dist/migrate-apply.js +144 -0
  15. package/dist/migrate-score.d.ts +29 -0
  16. package/dist/migrate-score.js +267 -0
  17. package/dist/migrate-store.d.ts +52 -0
  18. package/dist/migrate-store.js +77 -0
  19. package/dist/migrate.d.ts +65 -0
  20. package/dist/migrate.js +111 -0
  21. package/dist/templates/genres.d.ts +65 -0
  22. package/dist/templates/genres.js +228 -0
  23. package/dist/templates/skeletons.d.ts +48 -0
  24. package/dist/templates/skeletons.js +558 -0
  25. package/dist/tools/create.d.ts +9 -0
  26. package/dist/tools/create.js +77 -0
  27. package/dist/tools/local.d.ts +10 -0
  28. package/dist/tools/local.js +107 -0
  29. package/dist/tools/mutate.d.ts +11 -0
  30. package/dist/tools/mutate.js +157 -0
  31. package/dist/tools/read.d.ts +9 -0
  32. package/dist/tools/read.js +104 -0
  33. package/dist/tools/shared.d.ts +52 -0
  34. package/dist/tools/shared.js +87 -0
  35. package/dist/tools/write.d.ts +10 -0
  36. package/dist/tools/write.js +148 -0
  37. package/dist/tools.d.ts +23 -0
  38. package/dist/tools.js +43 -0
  39. package/dist/translate.d.ts +44 -0
  40. package/dist/translate.js +207 -0
  41. package/dist/wiki/assets.d.ts +42 -0
  42. package/dist/wiki/assets.js +91 -0
  43. package/dist/wiki/client.d.ts +67 -0
  44. package/dist/wiki/client.js +221 -0
  45. package/dist/wiki/locale.d.ts +66 -0
  46. package/dist/wiki/locale.js +154 -0
  47. package/dist/wiki/pages.d.ts +7 -0
  48. package/dist/wiki/pages.js +7 -0
  49. package/dist/wiki/pages.read.d.ts +114 -0
  50. package/dist/wiki/pages.read.js +114 -0
  51. package/dist/wiki/pages.write.d.ts +109 -0
  52. package/dist/wiki/pages.write.js +201 -0
  53. package/package.json +36 -0
  54. package/skills/historian/SKILL.md +294 -0
  55. package/skills/historian/references/adapting-your-own-wiki.md +53 -0
  56. package/skills/historian/references/genres.md +160 -0
  57. package/skills/historian/references/rules.md +30 -0
  58. package/skills/historian/references/style.md +84 -0
  59. package/skills/historian/references/wikijs-guide.md +87 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 opencode-historian contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,381 @@
1
+ # opencode-historian
2
+
3
+ > OpenCode 插件:双语 wiki.js 知识库管理 / Bilingual wiki curator as an OpenCode plugin.
4
+
5
+ opencode-historian 把 wiki.js 的读写、翻译、页型规范、迁移工具打包成一个 OpenCode 插件,让 AI agent 能直接管理双语知识库。它为什么存在、文风从哪里来、怎么一句话开始用,见下文「史官宣言」。
6
+
7
+ 功能一览:
8
+
9
+ * 10 个 `historian_*` 工具,覆盖创建、更新、追加、翻译、搜索、阅读、地图/时间轴、迁移、删除、移动
10
+ * Skill v4 随插件自动注入(config hook),无需手动安装 skill 文件
11
+ * G1 至 G5 页型契约,每种页型对应专属骨架模板
12
+ * 双语孪生页面(en/zh)自动翻译,翻译引擎可配置
13
+ * 页面地图缓存、本地镜像与时间轴聚合视图
14
+ * 开工前置查阅回路(reading loop,默认开)+ `/historian-capture` 会话留痕(默认关)
15
+ * 试点与评测均通过:pilot 7 页迁移 PASS,eval 7/7 场景首跑全过
16
+
17
+ ## 史官宣言 / The Historian's Manifest
18
+
19
+ ### 灵感 / Inspiration
20
+
21
+ 这个插件的写作规则不是发明的,是从五种语言文化的 wiki 工程传统加一种运维文化里蒸馏出来的(调研全文见仓库内 `docs/research/cross-cultural-wiki-writing-digest.md`,每条主张附原文引用):
22
+
23
+ | 文化 | 给史官的贡献 |
24
+ |---|---|
25
+ | 中文 | 序言不可侵犯(多数读者只读序言,要点前置);可供查证(举证责任在添加内容的一方);金字塔原理的结论先行;阮一峰式行文纪律(句 ≤40 字、一文一事);企业事件复盘模板(摘要→背景→时间线→影响量化→根因→改进,改进措施是复盘最重要的部分,行动项必须带负责人、期限与验证方式) |
26
+ | 英文 | WP:LEAD 导言自足、篇幅按重要性分配;Good Article 六项质量门;Diátaxis 四象限(tutorial / how-to / reference / explanation,模式混杂是文档烂的根源,参考页要像地图一样镜像系统结构);Google 技术写作"≥3 个相关字段就上表格";SRE blameless postmortem |
27
+ | 德文 | 条目开头先定义、门外汉可懂、一文一概念反碎片化、禁止"杂项"小节;引证义务 Belegpflicht:"宁要格式错的引注,不要没有引注";评价必须归属到人,不带情绪(sine ira et studio) |
28
+ | 法文 | 中立、非个人化、清晰、精确、易懂、有教学性六种品质;溢美词强制转成事实(用排名、奖项、销量替代"最伟大的"这类断言);耐久过滤:明天就会过时的内容不值得写 |
29
+ | 俄文 | ХС/ИС 条目门槛(导言普通读者可懂、术语最少、不少于 10 行);二手权威来源优先于一手(АИ);每种观点按影响力分配篇幅、禁止虚假平衡(ВЕС);风格要求精确、简明、无歧义,同时保持内容饱和 |
30
+
31
+ SRE postmortem 文化单独值得点名:它把"事件"当一等文档对待,过程/原因/后果/改进四段式正是 `/historian-capture` 命令的输出协议(`src/index.ts` 的 capture 模板),而"改进项要有负责人和可验证终态"落在了 G1 行动项表格里。
32
+
33
+ 把这些文化做成插件的直接动因,是一个机构记忆问题:这台机器由一个人加一群 AI agent 操作。会话结束,终端滚动条就没了;上下文压缩,细节就丢了。部署过什么、发生过什么事故、踩过什么坑、做过什么决定,三个月后人和 AI 都无从查起。史官要解决的,就是让这台机器的历史变得可检索。
34
+
35
+ ### 目的 / Purpose
36
+
37
+ 让"这台机器上部署过什么、发生过什么、踩过什么坑、做过什么决定"成为人和 AI 都可索引、可引用、可审计的一等知识:
38
+
39
+ * 知识落进结构化双语 wiki 页,而不是一次性的会话回复
40
+ * 每个写操作强制回报 en + zh 双 URL(`URL_MANDATE`,`src/tools/shared.ts:39`),引用链可回溯
41
+ * wiki 成为任何 agent 会话开工前先查的权威来源,而不是锁在某个会话里的私有记忆
42
+
43
+ ### 作用 / What it does
44
+
45
+ 五柱页型契约,加上读写两条自动化回路:
46
+
47
+ | 能力 | 机制 | 落点 |
48
+ |---|---|---|
49
+ | 五柱页型 | G1 事件复盘 / G2 对比选型 / G3 清单索引 / G4 概念原理 / G5 现状账本,写前声明页型,套固定骨架、过来源列检查 | `src/templates/genres.ts`,skill Phase 1.5 |
50
+ | 双视图 | 地图视图(en/zh 对应关系,Locale/Twin 列)+ 时间轴视图(ISO 周分组,支持 `days` 窗口与 `path` 前缀过滤,人读周表 + 机读 weeks JSON) | `historian_map` 的 `action:'show'` / `action:'timeline'` |
51
+ | 迁移与评分门禁 | 存量页按骨架重排:dry-run 评分在前,`apply=true` 自动 pre-image 备份;每页写入后过 10 项自检门 | `historian_migrate`,`selfReviewChecklist()` |
52
+ | 前置查阅回路 | 向每次请求的 system 提示注入"先查 wiki"指令:动这台机器的部署/历史/坑/决定之前先 `historian_search`、查 timeline、核对 G5 卡的核实日期,引用查过的页面 URL | `readingLoop` 选项(默认 `true`),`src/index.ts` 的 `experimental.chat.system.transform` 钩子 |
53
+ | 主动留痕 | `/historian-capture` 命令把当前会话总结成 G1 事件页;开启 `capture.enabled` 后额外在会话空闲时弹一次提醒,仅提醒,绝不自动写页 | `src/index.ts` 的 `config` / `event` 钩子 |
54
+
55
+ G5 现状卡回答"现在跑着什么",timeline 回答"最近两周变了什么"。比如问"`service-a` 现在监听哪个端口",应当命中现状账本里的一行(形如 `example.com:8000`,带上次核实日期与验证命令),而不是某次会话的聊天记录。这两样合起来,wiki 才从文档堆变成可查询的运维账本。
56
+
57
+ ### 使用方式 / Usage
58
+
59
+ 端到端最短路径(适配你自己 wiki 的六步完整指南见 `skills/historian/references/adapting-your-own-wiki.md`):
60
+
61
+ 1. **安装**:`opencode.json[c]` 的 `plugin` 数组加 `"opencode-wiki-historian"`(npm 发布后)或 `"file:///home/<you>/workspace/opencode-historian"`(本地开发),细节见下文「安装」。
62
+ 2. **配置**:最小三个选项;翻译腿可以不配,双语孪生会优雅降级为 pending:
63
+
64
+ ```jsonc
65
+ ["opencode-wiki-historian", {
66
+ "baseUrl": "http://<your-wiki>:3000",
67
+ "apiKeyPath": "~/.wiki-key",
68
+ "sections": ["team-notes/"]
69
+ }]
70
+ ```
71
+
72
+ 3. **口述写史**:对 opencode 说自然语言,史官完成分诊→放置→页型→骨架→自检→写入,并回报双语 URL:
73
+
74
+ > `service-a` 今天 OOM 重启,根因是缓存没设上限,已加告警,记下来。
75
+
76
+ → 分诊为事件复盘,声明 G1 → `historian_page_create`(`genre: "G1"`)→ 回报 `http://<your-wiki>:3000/team-notes/<slug>` 与它的 `/zh/` 孪生页。
77
+ 4. **检索与整理**:`historian_search` 按主题查;`historian_map` 的 `show` 看双语地图、`timeline`(可选 `days` / `path`)看最近变动;存量页不合规用 `historian_migrate` 先 dry-run 再 apply。
78
+ 5. **开关**:严格 OpenAI 兼容后端(如 vLLM,会拒绝多条 system 消息)把 `"readingLoop": false` 关掉;想要空闲留痕提醒就 `"capture": { "enabled": true }`,`/historian-capture` 命令本身与开关无关、始终注册。
79
+
80
+ ### 解耦声明 / Decoupling
81
+
82
+ 插件与任何一台机器的 wiki 内容零耦合:
83
+
84
+ * `sections` 默认空列表 = 不设路径前缀限制,实际写权限由 wiki.js token 的 page rules 决定;包内不携带任何真实主机、真实章节分类学或页面数据
85
+ * 翻译腿零内置地址:解析链只有 `translate.endpoint` 选项 → 环境变量 `HISTORIAN_TRANSLATE_ENDPOINT` → 未配置,未配置时孪生降级 pending,不发任何网络请求
86
+ * `tools/privacy-audit.mjs` 对 `npm pack` 清单里的每个随包文件跑隐私红线 regex(真实主机 / 本机路径 / 本机章节名 / 密钥形态 / 个人身份),任何命中即非零退出、拦住发布;已接入 `prepublishOnly`(build → test → audit)
87
+ * 随包文本里的示例全部占位符化:`http://<your-wiki>:3000`、`team-notes/`、`example.com`
88
+
89
+ ## 仓库 / Repository
90
+
91
+ 源代码仓库由维护者自管;本包的公开分发渠道是 npm registry(`opencode-wiki-historian`)。
92
+ Source repository is managed by the maintainer; the public distribution channel for this package is the npm registry (`opencode-wiki-historian`).
93
+
94
+ ## 安装 / Installation
95
+
96
+ 两种安装方式经过实测验证。
97
+
98
+ ### 方式一:npm 包(推荐(发布后))
99
+
100
+ 在 `opencode.json` 或 `opencode.jsonc` 的 `plugin` 数组中添加包名:
101
+
102
+ ```jsonc
103
+ {
104
+ "plugin": ["opencode-wiki-historian"]
105
+ }
106
+ ```
107
+
108
+ > 注:npm 包名为 opencode-wiki-historian(opencode-historian 已被注册表上一无关同名包占用);仓库与插件 id 仍为 opencode-historian。
109
+
110
+ 发布准备状态:包已按 0.1.0 打包就绪(`npm pack` 验证通过),npm 发布需账号 2FA 一次性完成(`npm publish --access public --otp=<code>`)。在发布完成前,方式二 `file://` 为当前可用安装路径。
111
+
112
+ ### 方式二:本地路径(开发用)
113
+
114
+ 用绝对路径指向本地构建目录:
115
+
116
+ ```jsonc
117
+ {
118
+ "plugin": ["file:///home/<you>/workspace/opencode-historian"]
119
+ }
120
+ ```
121
+
122
+ 此方式已通过 `opencode run` 验证,10 个 historian 工具全部注册成功(`.qa/12.txt`)。
123
+
124
+ ### 安装后验证
125
+
126
+ 插件注入的 skill 会注册 `/historian` 命令。验证方法:
127
+
128
+ ```bash
129
+ opencode run --command historian --message "historian_map show"
130
+ ```
131
+
132
+ 如果工具列表中出现 `historian_page_create` 等 10 个工具,安装成功。
133
+
134
+ > **升级提示**:如果你之前使用过 historian v2 的扁平 skill 文件(如 `~/.config/opencode/skills/historian.md`),需要先重命名为 `historian.md.v2-disabled` 或移到别处。插件通过 config hook 自动注入 v4 skill,两个同名 skill 不能共存。
135
+
136
+ ## 配置 / Configuration
137
+
138
+ 插件支持元组形式传入选项:
139
+
140
+ ```jsonc
141
+ {
142
+ "plugin": [
143
+ ["opencode-wiki-historian", {
144
+ "baseUrl": "http://your-wiki:3000",
145
+ "apiKeyPath": "~/.wiki-key",
146
+ "translate": {
147
+ "endpoint": "https://<your-anthropic-compatible-gateway>/v1",
148
+ "model": "qwen3.7-plus",
149
+ "apiKey": "<YOUR_KEY>",
150
+ "providerKey": "my-provider"
151
+ },
152
+ "sections": ["team-notes", "infra"],
153
+ "locales": ["en", "zh"]
154
+ }]
155
+ ]
156
+ }
157
+ ```
158
+
159
+ 不传选项时等同 `["opencode-wiki-historian"]`,使用全部默认值。插件包内不携带任何特定机器的配置(章节分类学、翻译网关地址均已清空为通用默认)。
160
+
161
+ ### 选项全表
162
+
163
+ | 字段 | 类型 | 默认值 | 说明 |
164
+ |---|---|---|---|
165
+ | `baseUrl` | string | `http://localhost:3000` | wiki.js GraphQL 端点 |
166
+ | `apiKeyPath` | string | `~/.wikijs-api-key` | wiki API key 文件路径(tilde 在读取时展开) |
167
+ | `translate.endpoint` | string | 未配置(见下方链) | 翻译 API 端点 |
168
+ | `translate.model` | string | `qwen3.7-plus` | 翻译模型 |
169
+ | `translate.apiKey` | string | 见下方链 | 翻译 API 密钥 |
170
+ | `translate.providerKey` | string | 未配置 | jsonc 兜底腿读取的 provider 名;须显式设置才会启用该腿 |
171
+ | `sections` | string[] | `[]`(不限制) | 插件可操作的 wiki 路径前缀白名单 |
172
+ | `locales` | string[] | `["en", "zh"]` | 启用的语言列表 |
173
+ | `readingLoop` | boolean | `true` | 向每次请求注入"先查 wiki"的开工前置查阅 advisory;拒绝多条 system 消息的严格 OpenAI 兼容后端(如 vLLM)须设 `false` |
174
+ | `capture.enabled` | boolean | `false` | 开启后会话空闲时弹一次 `/historian-capture` 留痕提醒;仅提醒,不自动写页 |
175
+
176
+ `translate.endpoint` 解析链(优先级从高到低):`translate.endpoint` 选项 → 环境变量 `HISTORIAN_TRANSLATE_ENDPOINT` → 未配置。包内**不**内置任何网关地址;未配置时翻译调用直接以 `translate.endpoint not configured` 失败(见降级行为)。
177
+
178
+ `sections` 默认为空列表 = 不限制路径前缀(任意合法路径可写,实际权限由 wiki.js token 的 page rules 决定)。按机器通过选项传入白名单,例如 `["team-notes", "infra", "ops"]`。
179
+
180
+ ### API Key 获取优先级
181
+
182
+ **翻译 API key**(`translate.apiKey`),按优先级:
183
+
184
+ 1. 配置对象中的 `translate.apiKey` 字段
185
+ 2. 环境变量 `DASHSCOPE_API_KEY`
186
+ 3. opencode jsonc 配置中 `provider["<translate.providerKey>"].options.apiKey`(仅当显式设置 `translate.providerKey` 时读取;包内不内置默认 provider 名)
187
+ 4. 均无则抛出 `ConfigError('missing-translation-key')`
188
+
189
+ **wiki.js API key**,按优先级:
190
+
191
+ 1. `apiKeyPath` 指向的文件内容
192
+ 2. 环境变量 `WIKIJS_API_KEY`
193
+ 3. 均无则抛出 `ConfigError('missing-wiki-api-key')`
194
+
195
+ ### 降级行为
196
+
197
+ key 缺失时 `ConfigError` 记录一次日志,插件工具全部禁用,opencode 正常启动不受影响。`translate.endpoint` 未配置时双语孪生功能降级为 pending 状态(`twinReason: 'translate.endpoint not configured — ...'`),创建页面只写入请求 locale 的内容,不会发起任何翻译网络请求。
198
+
199
+ ## wiki.js 前置检查 / Prerequisites
200
+
201
+ 安装插件前,确认 wiki.js 实例已完成以下配置。本插件不自动执行任何管理端操作。
202
+
203
+ ### 多语言
204
+
205
+ * **Admin > General > Multilingual**:启用 namespacing
206
+ * **Active Namespaces** 包含 `en` 和 `zh`
207
+ * zh 命名空间是语言切换器和 URL 路由的前提
208
+
209
+ ### API Token
210
+
211
+ * **Admin > API Access** 创建 token
212
+ * 需要的 scope:`write:pages`、`delete:pages`、`manage:pages`、`manage:system`
213
+ * 将 token 写入 `apiKeyPath` 文件或设为 `WIKIJS_API_KEY` 环境变量
214
+
215
+ ### 页面权限
216
+
217
+ * **Admin > Groups** 中对应组的 page rules 必须覆盖插件 `sections` 配置的路径前缀
218
+ * 路径不在 page rules 范围内会触发 `PermissionError`(评测场景 R-b 验证)
219
+
220
+ ### 主题与显示
221
+
222
+ * wiki.js 自动生成目录(TOC),通过 Admin > Theme 配置位置,页面内容里**不要**写 `{{toc}}`
223
+ * 自定义 CSS/HTML 注入入口在 Admin > Theme
224
+ * 建议关闭评论功能,避免知识库页面被评论干扰
225
+
226
+ ## 中文页面在哪看 / Viewing Chinese Pages
227
+
228
+ 中文页面的 URL 格式:
229
+
230
+ ```
231
+ http://<host>/zh/<path>
232
+ ```
233
+
234
+ 例如 `ops/deploy-checklist` 的中文版在 `http://<host>/zh/ops/deploy-checklist`。英文版不带 locale 前缀:`http://<host>/ops/deploy-checklist`。
235
+
236
+ **语言切换器**出现在页面右上角的前提是 namespacing 已开启且 zh 在 Active Namespaces 里。看不到切换器时,检查 Admin > General > Multilingual 设置。
237
+
238
+ **工具返回值**:每个写入操作的结果都会同时回显 en 和 zh 的 URL,格式如:
239
+
240
+ ```
241
+ en: http://<host>/ops/example
242
+ zh: http://<host>/zh/ops/example
243
+ ```
244
+
245
+ **地图视图**:`historian_map show` 输出包含 Locale 和 Twin 列,展示每个路径的双语对应关系。本地镜像文件保存在 `~/.config/opencode/historian-map.json`(`getMap` 读取,带过期秒数);`_meta/page-map` 是 wiki 端的缓存页,由 `historian_map` refresh 写入。
246
+
247
+ **时间轴视图**:`historian_map action:'timeline'` 把镜像行按 ISO 周分组(可选 `days` 窗口与 `path` 前缀过滤),输出人读周表 + 机读 `weeks` JSON,回答"最近哪些页面变过"。
248
+
249
+ ## 写作质量体系 / Quality System
250
+
251
+ ### G1 至 G5 页型
252
+
253
+ 插件根据内容形态把每页归入五种页型之一,每种有专属骨架模板:
254
+
255
+ | 页型 | 用途 | 骨架结构 |
256
+ |---|---|---|
257
+ | G1 事件复盘 | 故障、踩坑、事后分析 | 时间线 → 根因 → 影响 → 行动项 → 教训,附来源列 |
258
+ | G2 对比 | 技术选型、方案比较 | 来源声明 + 对比表格 + 结论 |
259
+ | G3 清单 | 操作步骤、检查项 | 可勾选的检查项列表 |
260
+ | G4 概念 | 架构说明、原理讲解 | 概念定义 → 图示 → 示例 |
261
+ | G5 现状账本 | 此刻的部署/运行态,回答"现在跑着什么" | 状态块 → 部署物清单(每行带「上次核实于」+ 验证命令)→ 失效策略,禁止叙事正文 |
262
+
263
+ ### 10 项自检门
264
+
265
+ 每页写入后过一遍自检清单(源码 `src/templates/genres.ts` `selfReviewChecklist()`,参考文件 `skills/historian/references/rules.md` + `genres.md`)。dry-run 阶段评 1–8,apply 后评 9–10。genre-specific 条目对不匹配的页型记 N/A=PASS;G5 页的第 4–6 项换成账本变体(`G5_CHECKLIST_VARIANTS`):
266
+
267
+ 1. **导言占比 10–15%**:导言 ≈ 正文的 10–15%,每个重要小节在导言至少占一句
268
+ 2. **句长上限**:中文句 ≤20 字、英文句 ≤25 词
269
+ 3. **表格判据**:≥3 字段的结构化枚举入表,成对数据用描述列表
270
+ 4. **对比表来源列**(G2):对比表/枚举表每行有来源列,行序固定、无合并单元格;G5 变体:部署物清单每行带「上次核实于」列
271
+ 5. **时间线来源列**(G1):时间线每行有来源列,仅日志可证事实;G5 变体:验证方法含可执行复核命令
272
+ 6. **行动项五要素**(G1):类型|负责人|期限|验证|状态 五列,措施是行内容;G5 变体:无叙事正文,只有状态块 + 表格
273
+ 7. **无杂项筐**:除 参见/附录 之外没有 "其他/杂项" 类 catch-all 小节
274
+ 8. **无溢美词**:领先/强大/灵活/高效 等 bare claim 改事实或删除
275
+ 9. **双语 URL 已回报**(写后核销):报告含 /en/ 与 /zh/ 两个可访问 URL
276
+ 10. **孪生已建或 zh_status:pending 声明**(写后核销):twin created OR zh_status pending recorded and declared in report
277
+
278
+ ### SYN 20 规则
279
+
280
+ 更细的 20 条写作规则散布在 `skills/historian/references/` 目录下各参考文件中,agent 加载 skill 时自动读取。
281
+
282
+ ## wiki.js 用法与定制化 / Usage Guide
283
+
284
+ ### Markdown 速查
285
+
286
+ | 元素 | 写法 | 说明 |
287
+ |---|---|---|
288
+ | 提示框 | `> 内容\n{.is-info}` | `.is-info` / `.is-warning` / `.is-danger` / `.is-success` |
289
+ | 紧凑表格 | 标准 markdown + `{.dense}` | 减小行距 |
290
+ | 脚注 | `[^n]` 正文 + `[^n]: 内容` 底部 | wiki.js 原生支持 |
291
+ | 定义列表 | `term\n: definition` | 术语表用 |
292
+ | 数学公式 | `$...$` 行内、`$$...$$` 块 | KaTeX 渲染,默认开启 |
293
+ | 流程图 | ` ```mermaid ` 代码块 | mermaid 渲染,默认开启 |
294
+ | 标签页 | `{tabset}` + `## Tab Name` | 多标签内容切换 |
295
+
296
+ **禁用项**:
297
+
298
+ * `{{toc}}`(目录由主题配置自动生成)
299
+ * `:::` 容器(wiki.js 不解析)
300
+ * YAML frontmatter(API 创建页面不经过 frontmatter 解析)
301
+
302
+ ### API 陷阱与插件应对
303
+
304
+ wiki.js GraphQL API 有 9 个常见陷阱。插件在内部处理了每一个(详见 `skills/historian/references/wikijs-guide.md`,原始报告路径 `docs/research/wikijs-2x-report.md`):
305
+
306
+ | 陷阱 | 插件应对 |
307
+ |---|---|
308
+ | `pages.update` 是整页替换,不是增量合并 | 工具内部做 read-modify-write |
309
+ | `pages.create` 返回值缺部分字段 | 创建后立即 readback 拿完整数据 |
310
+ | `responseResult` 嵌套结构,需取 `.id` | 解析层统一提取,上层拿到的是直接值 |
311
+ | 空 content 被拒绝 | 引擎 pre-check `ContentEmptyError`;不传 content 时走 template-only 模式,不发 GraphQL 写请求 |
312
+ | 移动页面不能用 update 改路径 | 用独立的 `pages.move` mutation |
313
+ | `singleByPath` 必须传 locale | 所有查询强制带 locale 参数 |
314
+ | 上传附件字段名必须是 `mediaUpload` | 内部固定字段名(`src/wiki/assets.ts`) |
315
+ | `isPublished: false` 的页面匿名访问报 404/403 | 创建草稿时在工具输出中标注 |
316
+ | 路径不能以 locale 前缀开头 | `validatePath` 拒绝 `zh/...` 形式的输入 |
317
+
318
+ ## 从 historian v2 迁移 / Migrating from v2
319
+
320
+ 旧版 historian v2 依赖 `/opt/wiki-ops/` 下的 Python 脚本(`wiki-ops.py`、`wiki-biling.py`)。插件完全替代了这些脚本。旧脚本保留在原位不动(标记 LEGACY),不需要删除。
321
+
322
+ ### 命令映射表
323
+
324
+ | wiki-ops.py 子命令 | historian 工具 | 备注 |
325
+ |---|---|---|
326
+ | `list` | `historian_map` (show) | 地图缓存替代全量列表 |
327
+ | `read <id>` | `historian_read` (path) | 改用 path 定位,不再用数字 ID |
328
+ | `append <id> <file>` | `historian_page_append` | 直接传 section 内容,不用写临时文件 |
329
+ | `create "Title" "path" <file>` | `historian_page_create` | content 参数直接传,可省文件 |
330
+ | `rebuild-tree` | `historian_map` (refresh) | 从 wiki 重建地图缓存 |
331
+ | `search "keyword"` | `historian_search` (kind=title) | 标题搜索 |
332
+ | `search-content "keyword"` | `historian_search` (kind=content) | 全文搜索 |
333
+ | `fetch <id>` / `fetch-paths <path>` | `historian_read` | 按 path + locale 读取 |
334
+ | `cache-read` | `historian_map` (show) | 本地镜像 |
335
+ | `cache-refresh` | `historian_map` (refresh) | 重建缓存 |
336
+ | `delete <id> --confirm` | `historian_delete` | confirm 参数必须为 `"yes"` |
337
+
338
+ | wiki-biling.py 子命令 | historian 工具 | 备注 |
339
+ |---|---|---|
340
+ | `create-bilingual` | `historian_page_create` (twin=true) | twin 默认开启 |
341
+ | `append-bilingual` | `historian_page_append` + sectionZh | sectionZh 参数携带中文内容 |
342
+ | `translate-snippet` | `historian_translate_snippet` | 纯本地翻译,不写 wiki |
343
+ | `status` | `historian_map` (show) | 双语状态在地图中展示 |
344
+ | `checkpoint` | migrate store 文件 | 迁移进度持久化 |
345
+ | `run` / `run --path` | `historian_migrate` (apply=true) | 支持 dry-run(apply=false) |
346
+
347
+ ### 升级步骤
348
+
349
+ 1. 重命名旧 skill 文件:`mv ~/.config/opencode/skills/historian.md ~/.config/opencode/skills/historian.md.v2-disabled`
350
+ 2. 在 `opencode.json[c]` 的 `plugin` 数组中添加 `opencode-wiki-historian`
351
+ 3. 重启 opencode,`/historian` 命令可用即表示 v4 skill 已注入
352
+
353
+ ## 运维 / Operations
354
+
355
+ ### 迁移工具
356
+
357
+ `historian_migrate` 把现有页面重新格式化为对应页型骨架:
358
+
359
+ 1. **先 dry-run**:`apply=false`(默认),查看骨架预览和自检评分
360
+ 2. **再 apply**:`apply=true`,引擎在首次写入前自动生成 pre-image 备份
361
+ 3. **回滚**:读备份 JSON,用 `historian_page_update` 推回原始内容
362
+
363
+ 备份文件位于 `results/pilot-backup-*.json`。迁移进度存储在 `~/.config/opencode/historian-migrate.json`,支持断点续跑。
364
+
365
+ ### 评测框架
366
+
367
+ harness repo(与本插件仓库同工作区)提供 7 个行为验收场景,覆盖创建、追加、重叠整合、整理、价值门控、G1 格式、zh URL 报告。v3 插件评测结果:7/7 PASS,51/58 适用分,G=1,零重试(`results/2026-09-02-v3-plugin.md`)。
368
+
369
+ ## 开发 / Development
370
+
371
+ ```bash
372
+ npm run build # tsc 编译到 dist/
373
+ npm test # vitest run(331 tests, 15 files)
374
+ npm pack --dry-run # 检查打包文件列表
375
+ ```
376
+
377
+ 打包文件(`files` 字段):`dist`、`skills`。加上 npm 自动包含的 `README.md` 和 `LICENSE`。`tools/`(pilot-run、finish-publish、install-skill)为仓库开发工具,不随包分发。
378
+
379
+ ## 许可证
380
+
381
+ MIT
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Chronology aggregation over the page-map rows (todo: timeline action) —
3
+ * ISO-week grouping of already-fetched MapRow values, dual human/JSON form.
4
+ * Kept out of map.ts to hold both modules under the 250 pure-LOC ceiling.
5
+ */
6
+ import type { MapRow } from './map.js';
7
+ import type { Locale } from './wiki/pages.read.js';
8
+ export interface ChronologyOptions {
9
+ readonly days?: number;
10
+ /** Reference time for the `days` window (tests inject a fixed clock). */
11
+ readonly now?: Date;
12
+ }
13
+ export interface ChronologyItem {
14
+ readonly path: string;
15
+ readonly locale: Locale;
16
+ readonly title: string;
17
+ readonly updatedAt: string;
18
+ }
19
+ export interface ChronologyWeek {
20
+ /** ISO week key `YYYY-Www` (Monday-start, correct ISO week-year). */
21
+ readonly week: string;
22
+ readonly items: readonly ChronologyItem[];
23
+ }
24
+ export interface Chronology {
25
+ readonly markdown: string;
26
+ readonly weeks: readonly ChronologyWeek[];
27
+ }
28
+ export declare function renderChronologyMarkdown(weeks: readonly ChronologyWeek[]): string;
29
+ /** Chronology aggregation over already-fetched map rows: group by ISO week of
30
+ * updatedAt (weeks and items newest-first), zh+en rows kept distinct. Dual
31
+ * output — human markdown table + machine-readable `weeks` JSON. Unparseable
32
+ * timestamps are dropped; `days` (relative to `now`) bounds the window. */
33
+ export declare function buildChronology(rows: readonly MapRow[], opts?: ChronologyOptions): Chronology;
34
+ /** Section-aware path filter for timeline: a prefix matches the whole path, a
35
+ * path subtree, or a first-segment section (`ops` → `ops/foo`). */
36
+ export declare function filterRowsByPath(rows: readonly MapRow[], prefix: string): readonly MapRow[];
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Chronology aggregation over the page-map rows (todo: timeline action) —
3
+ * ISO-week grouping of already-fetched MapRow values, dual human/JSON form.
4
+ * Kept out of map.ts to hold both modules under the 250 pure-LOC ceiling.
5
+ */
6
+ import { classifyGenre } from './templates/genres.js';
7
+ const DAY_MS = 86_400_000;
8
+ const WEEK_MS = 7 * DAY_MS;
9
+ /** ISO-8601 week key: the Thursday inside the row's Monday-start week fixes
10
+ * both the week number and the ISO week-year (late Dec may land in W01 of
11
+ * the next year, early Jan in W52/53 of the previous one). */
12
+ function isoWeekKey(d) {
13
+ const dayIso = ((d.getUTCDay() + 6) % 7) + 1;
14
+ const thursday = Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()) + (4 - dayIso) * DAY_MS;
15
+ const year = new Date(thursday).getUTCFullYear();
16
+ const week = Math.floor((thursday - Date.UTC(year, 0, 1)) / WEEK_MS) + 1;
17
+ return `${year}-W${String(week).padStart(2, '0')}`;
18
+ }
19
+ /** Title-only genre tag for the human table; '—' when no keyword fires
20
+ * (an empty body gives zero signals, so untagged rows stay untagged). */
21
+ function genreTag(title) {
22
+ const c = classifyGenre({ title, body: '' });
23
+ return c.signals.length > 0 ? c.genre : '—';
24
+ }
25
+ export function renderChronologyMarkdown(weeks) {
26
+ if (weeks.length === 0)
27
+ return '';
28
+ const blocks = weeks.map((w) => [
29
+ `## ${w.week}`,
30
+ '',
31
+ '| 日期 | 章节 | 路径 | 标题 | 页型 |',
32
+ '| --- | --- | --- | --- | --- |',
33
+ ...w.items.map((it) => `| ${it.updatedAt.slice(0, 10)} | ${it.path.split('/')[0]} | ${it.path.replaceAll('|', '\\|')} | ${it.title.replaceAll('|', '\\|')} | ${genreTag(it.title)} |`),
34
+ ].join('\n'));
35
+ return `${blocks.join('\n\n')}\n`;
36
+ }
37
+ /** Chronology aggregation over already-fetched map rows: group by ISO week of
38
+ * updatedAt (weeks and items newest-first), zh+en rows kept distinct. Dual
39
+ * output — human markdown table + machine-readable `weeks` JSON. Unparseable
40
+ * timestamps are dropped; `days` (relative to `now`) bounds the window. */
41
+ export function buildChronology(rows, opts) {
42
+ const cutoff = opts?.days === undefined
43
+ ? Number.NEGATIVE_INFINITY
44
+ : (opts.now ?? new Date()).getTime() - opts.days * DAY_MS;
45
+ const kept = rows
46
+ .map((r) => ({ r, t: Date.parse(r.updatedAt) }))
47
+ .filter((x) => !Number.isNaN(x.t) && x.t >= cutoff)
48
+ .sort((a, b) => b.t - a.t ||
49
+ (a.r.path < b.r.path ? -1 : a.r.path > b.r.path ? 1 : a.r.locale < b.r.locale ? -1 : 1));
50
+ const byWeek = new Map();
51
+ for (const { r } of kept) {
52
+ const key = isoWeekKey(new Date(r.updatedAt));
53
+ const bucket = byWeek.get(key);
54
+ const item = { path: r.path, locale: r.locale, title: r.title, updatedAt: r.updatedAt };
55
+ if (bucket === undefined)
56
+ byWeek.set(key, [item]);
57
+ else
58
+ bucket.push(item);
59
+ }
60
+ const weeks = [...byWeek.entries()].map(([week, items]) => ({ week, items }));
61
+ return { weeks, markdown: renderChronologyMarkdown(weeks) };
62
+ }
63
+ /** Section-aware path filter for timeline: a prefix matches the whole path, a
64
+ * path subtree, or a first-segment section (`ops` → `ops/foo`). */
65
+ export function filterRowsByPath(rows, prefix) {
66
+ return rows.filter((r) => r.path === prefix || r.path.startsWith(`${prefix}/`) || r.path.split('/')[0] === prefix);
67
+ }
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Configuration model for the opencode-historian plugin.
3
+ *
4
+ * The plugin keeps a zero-runtime-dependency surface: no zod, no bun, only
5
+ * node builtins. Secrets are resolved from explicit sources (raw plugin
6
+ * options, env vars, the machine's opencode.jsonc) and never from the network.
7
+ *
8
+ * Home directory and env are injectable parameters on every resolver so unit
9
+ * tests drive fixtures in tmp dirs — the real ~/.config is never touched by
10
+ * the suite (see test/config.test.ts).
11
+ */
12
+ export { ConfigError } from './jsonc.js';
13
+ export type { ConfigErrorCode } from './jsonc.js';
14
+ /** Fully resolved plugin options. All fields are non-optional; = what the
15
+ * rest of the plugin (client, translate engine) consumes. */
16
+ export interface HistorianOptions {
17
+ readonly baseUrl: string;
18
+ /** Raw path; may contain a leading `~`, which `readWikiApiKey` expands at
19
+ * read time (the key file is not read during option resolution). */
20
+ readonly apiKeyPath: string;
21
+ readonly translate: Readonly<{
22
+ /** Empty string = NOT CONFIGURED. Resolution chain: raw option →
23
+ * env HISTORIAN_TRANSLATE_ENDPOINT → unset. The shipped package
24
+ * deliberately carries no endpoint default; every translate call
25
+ * degrades to TranslateError('config') before touching the network. */
26
+ readonly endpoint: string;
27
+ readonly model: string;
28
+ readonly apiKey: string;
29
+ /** jsonc provider whose `options.apiKey` is the third translation-key
30
+ * trust leg (see resolveTranslationApiKey). Empty string = NOT
31
+ * CONFIGURED: the jsonc leg is opt-in via `translate.providerKey`;
32
+ * the shipped package carries no default provider name. */
33
+ readonly providerKey: string;
34
+ }>;
35
+ /** Path-prefix whitelist. Empty (the shipped default) = no restriction —
36
+ * any syntactically valid path is allowed; wiki.js page-rules remain the
37
+ * real authorization gate. Consumers must treat [] as allow-any. */
38
+ readonly sections: readonly string[];
39
+ readonly locales: readonly string[];
40
+ /** v2 reading-loop gate: true (the shipped default) makes the plugin push a
41
+ * consult-the-wiki advisory block into every chat request's system array via
42
+ * the experimental.chat.system.transform hook. false = hook is a pure no-op. */
43
+ readonly readingLoop: boolean;
44
+ /** v2 capture gate: reminder-only. When enabled the event hook fires one
45
+ * TUI toast per idle session nudging the agent/user to run the
46
+ * /historian-capture command; the plugin NEVER auto-writes a page from
47
+ * the hook — recording stays an explicit tool call (historian_page_create).
48
+ * Disabled by default: the event hook then has zero side effects and makes
49
+ * no client call. */
50
+ readonly capture: Readonly<{
51
+ readonly enabled: boolean;
52
+ }>;
53
+ }
54
+ /** Raw, user-supplied plugin options (the opencode PluginOptions shape).
55
+ * Every field optional — unresolved fields fall back to defaults / env. */
56
+ export interface HistorianPluginOptions {
57
+ readonly baseUrl?: string;
58
+ readonly apiKeyPath?: string;
59
+ readonly translate?: Readonly<{
60
+ readonly endpoint?: string;
61
+ readonly model?: string;
62
+ readonly apiKey?: string;
63
+ /** jsonc provider name for the apiKey fallback leg. No default: when
64
+ * unset the jsonc leg never runs. */
65
+ readonly providerKey?: string;
66
+ }>;
67
+ readonly sections?: readonly string[];
68
+ readonly locales?: readonly string[];
69
+ readonly readingLoop?: boolean;
70
+ readonly capture?: Readonly<{
71
+ readonly enabled?: boolean;
72
+ }>;
73
+ }
74
+ export declare const DEFAULT_BASE_URL = "http://localhost:3000";
75
+ export declare const DEFAULT_API_KEY_PATH = "~/.wikijs-api-key";
76
+ /** Env leg of the endpoint chain: raw option → this env var → unset (''). */
77
+ export declare const ENV_TRANSLATE_ENDPOINT = "HISTORIAN_TRANSLATE_ENDPOINT";
78
+ export declare const DEFAULT_TRANSLATE_MODEL = "qwen3.7-plus";
79
+ /** Empty = no path-prefix restriction (see HistorianOptions.sections). */
80
+ export declare const DEFAULT_SECTIONS: readonly string[];
81
+ export declare const DEFAULT_LOCALES: readonly ["en", "zh"];
82
+ /** Reading loop is on unless explicitly disabled (plan v2 todo 8). */
83
+ export declare const DEFAULT_READING_LOOP = true;
84
+ /** Capture reminders are opt-in (plan v2 todo 9). */
85
+ export declare const DEFAULT_CAPTURE_ENABLED = false;
86
+ /**
87
+ * Resolve raw plugin options against defaults, env vars and the machine's
88
+ * `~/.config/opencode/opencode.jsonc` into a fully typed HistorianOptions.
89
+ *
90
+ * Translation key priority (highest wins):
91
+ * 1. raw.translate.apiKey
92
+ * 2. env DASHSCOPE_API_KEY
93
+ * 3. jsonc provider[translate.providerKey].options.apiKey — only when
94
+ * `translate.providerKey` is set explicitly (no default provider name)
95
+ * 4. throw ConfigError('missing translation key')
96
+ *
97
+ * Translation endpoint priority: raw.translate.endpoint →
98
+ * env HISTORIAN_TRANSLATE_ENDPOINT → unset (''). There is no baked-in
99
+ * gateway URL; an unset endpoint degrades translate calls to
100
+ * TranslateError('config') (twins go pending, see translate.ts).
101
+ *
102
+ * There is deliberately NO anthropic fallback leg (removed by plan — the
103
+ * provider layout varies per machine; the jsonc leg is opt-in via
104
+ * translate.providerKey).
105
+ */
106
+ export declare function resolveOptions(raw: Partial<HistorianPluginOptions>, env?: NodeJS.ProcessEnv, homeDir?: string): HistorianOptions;
107
+ /**
108
+ * Read the wiki.js api key. Priority: key file (path from options.apiKeyPath,
109
+ * `~` expanded) first; env WIKIJS_API_KEY as fallback; ConfigError otherwise.
110
+ * The key file content is trimmed (a trailing newline is common).
111
+ */
112
+ export declare function readWikiApiKey(options: HistorianOptions, env?: NodeJS.ProcessEnv, homeDir?: string): string;