annotatable-html 0.0.0-stage → 1.38.3

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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 山居岁月
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 CHANGED
@@ -1,3 +1,290 @@
1
- # Temporary Holding Version
1
+ # annotatable-html(HTML4agent)
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ **可批注 HTML:人类在浏览器划线批注,AI agent 读批注并回答。** 对话追问、AI 回答可再批注、每次改动有版本快照可回滚;人还能像 Word 一样直接编辑正文(✏️ 编辑模式 + 🧭 导航窗格),人手改动同样进版本账本、可回滚。单文件、零依赖、离线可用、不绑定任何一家 AI 服务。
4
+
5
+ **Annotatable HTML docs: humans highlight and comment in the browser, AI agents read the annotations and reply — with threads, re-annotatable answers, Word-style inline editing and version history. Single file, zero dependencies.**
6
+
7
+ 产品哲学是 **annotation-first reading**:批注不是附加工具,而是阅读本身的一部分——即使是"隐藏所有按钮"的阅读模式,划线批注依然可用。
8
+
9
+ ---
10
+
11
+ ## 快速上手
12
+
13
+ ```bash
14
+ npx annotatable-html init # 目录初始化(AGENTS.md + agent 工具 + 模板 + 主题;已初始化自动跳过)
15
+ npx annotatable-html new 我的文档.html # 新建一份可批注文档
16
+ npx annotatable-html md 笔记.md # 一键 Markdown → 可批注 HTML(正文遵照原文,仅加批注层)
17
+ npx annotatable-html serve # 本地服务器:全自动保存(可选但推荐)
18
+ npx annotatable-html inject 任意.html # 给已有 HTML 注入批注层(原文件备份进同目录 .annotatable/)
19
+ ```
20
+
21
+ 使用循环:
22
+
23
+ 1. **人**:用 Edge/Chrome 打开 html,首次按提示绑定文件(serve 模式免绑定),选中文字 → ✍ 批注 / 🖍 高亮。批注自动保存进文件本体;
24
+ 2. **AI**:人类说「读批注」,agent 跑 `annotatable read` → 通读对话线逐条回答 → 写 `replies.json` → `annotatable reply`(回答追加为对话线新轮次,rev+1 并存版本快照);
25
+ 3. **人**:刷新页面,回答以绿色块出现在批注上;可「⤢ 展开大页」细读——**回答本身还能继续划线批注、页内追问**;
26
+ 4. 任何时刻可打开「⏪ 版本历史」回滚到任意版本(回滚也是一次新提交,可再撤销)。
27
+
28
+ 全部命令:`init` / `new` / `md` / `serve` / `read` / `reply` / `edit` / `poll` / `inject` / `theme` / `template` / `studio` / `skill`(`annotatable help` 查看用法)。
29
+
30
+ ---
31
+
32
+ ## 自定义外观与模板(v1.17.0)
33
+
34
+ 两条路,都通向同一套主题机制(`annotatable.theme.css` 的 `:root` 变量 + 追加覆盖 CSS):
35
+
36
+ **① 主题工坊(GUI,推荐)**:
37
+
38
+ ```bash
39
+ npx annotatable-html studio # 生成工坊页并自动开浏览器(默认 127.0.0.1:8974)
40
+ ```
41
+
42
+ 左侧按钮/滑杆调 **配色**(4 组预设 + 9 个取色器)、**字体**(正文/标题)、**根字号 / 行高 / 页面宽度**,右侧是真实模板的实时预览(划线、批注面板、🧭 导航窗格都能直接试——页宽与泊位窗格互不干扰)。调完点:
43
+
44
+ - **💾 保存到当前目录** → 写入 `annotatable.theme.css`,之后该目录下 `new` / `md` / `inject` 自动应用;
45
+ - **🌍 设为全局默认** → 写入 `~/.annotatable/theme.css`,任何目录都生效(目录级优先)。
46
+
47
+ **② 把喜欢的 HTML 一键变成默认模板**:
48
+
49
+ ```bash
50
+ npx annotatable-html template --use 我的风格.html # 整页保留原样式,注入批注层,存为全局默认
51
+ npx annotatable-html template --reset # 撤销,回到内置模板
52
+ ```
53
+
54
+ 之后 `new` / `md` 生成的新文档都长你那个 HTML 的样子,批注/导航/编辑/历史功能全量可用。规则:
55
+
56
+ - 正文容器自动择优锚定(`<article>` → `<main>` → content 系 div → body 整体包裹);
57
+ - 模板优先级:`--template` 显式指定 > 目录内 `annotatable.template.html` > 全局 `~/.annotatable/template.html` > 内置;主题同理(目录 `annotatable.theme.css` > 全局);
58
+ - 注意:你 HTML 里的相对路径图片/链接换位置后会失效(装完有告警提示)。
59
+
60
+ 只想微调配色的话:`annotatable theme` 导出主题文件手改也行,模板版式挂钩(`--wrap-max` / `--root-fs` / `--lh` / `--font-body` / `--font-head`)都可直接写进去。
61
+
62
+ ---
63
+
64
+ ## 工作原理
65
+
66
+ 三个角色,一个数据库——**HTML 文件本体就是数据库**:
67
+
68
+ ```
69
+ 浏览器(人类) HTML 文件本体 AI agent
70
+ ┌──────────────────┐ ┌───────────────────────┐ ┌─────────────────────┐
71
+ │ 批注层 JS(自包含) │─写回──▶ │ annotations-store JSON │◀─read──│ annotatable read │
72
+ │ 划线/弹窗/面板/历史 │◀─渲染── │ (哨兵注释间的数据区块) │ │ (只解析 store 区块) │
73
+ │ ✏️ Word 编辑模式 │ │ + history 版本账本 │─reply─▶│ reply / edit / poll │
74
+ └──────────────────┘ └───────────────────────┘ │ (rev+1 + 快照写回) │
75
+ ▲ └─────────────────────┘
76
+ 可选:annotatable serve(127.0.0.1 静态伺服 + 受控写回端点,全自动保存)
77
+ ```
78
+
79
+ - 批注数据只存在于 HTML 内部的 `annotations-store` JSON 区块(哨兵注释之间)——**agent 不需要读整份文档**,这是 token 消耗与文档体量解耦的关键;长答/教学全文放独立的 `answers-store` 区块(agent 用 `annotatable answer` 写入,页面回答大页渲染成教学卡),读批注永远不必吞教学正文;
80
+ - 任何写回必须 **rev+1 并维护 history 快照**(rev 铁律):浏览器靠 rev 检测冲突拒绝互相覆盖,工具链已封装;
81
+ - 状态机:批注 `status:"new"` → agent 回答后置 `answered` → 人类可追问(新轮次重新入账)。
82
+
83
+ ## 项目架构
84
+
85
+ ```
86
+ bin/cli.js 命令入口(init/new/md/theme/template/studio/skill/serve/inject/read/reply/edit/answer/teach/poll)
87
+ lib/core.js 全部核心逻辑(950 行):readPending / applyReplies / applyEdits
88
+ (含 buildHtmlIndex 锚定索引器 + bestOccurrence 消歧)/ pollAnnotations
89
+ / injectHtml(备份统一落同目录 .annotatable/)/ installSkill / 双层账本
90
+ / mdDoc+mdToBody(内置 marked/temml,GFM 转换与公式编译)/ templateUse+templateReset
91
+ / studioHtml+loadThemeChain(主题工坊与模板四级优先级)
92
+ / annotParts(注入四件套提取器,inject 与 templateUse 共用防双源)
93
+ lib/serve.js 本地伴随服务器(122 行):静态伺服 + /__annot/ping + /__annot/save 受控写回
94
+ + /__annot/studio 工坊页 + /__annot/theme 工坊保存
95
+ templates/
96
+ annotatable-doc.html 单文件模板(约 4000 行,产品本体):文档版式 + 批注层 + Word 编辑引擎 + 首绑引导;
97
+ :root 暴露 wrap-max / root-fs / lh / font-body / font-head 外观挂钩
98
+ theme-studio.html 主题工坊壳(studioHtml 装配:控件 + 真实模板实时预览 + 保存)
99
+ AGENTS.md / SKILL.md 工作流说明书(init/skill 分发,agent 读的「协议文档」)
100
+ annotatable.theme.css 默认主题(init 复制,可改后自动应用)
101
+ commands/ slash 命令定义(/html4agent 等)
102
+ agent/ init 分发给用户目录的薄封装:annot_read / annot_reply / annot_edit / annot_poll.js
103
+ test/run.js 极简自测(187 项断言,无框架)
104
+ test/token-audit.js Token 消耗审计(o200k_base 实测,仓库自包含可复现,本文数据来源)
105
+ demo/ 示例文档
106
+ ```
107
+
108
+ npm 包 `annotatable-html`(bin 命令 `annotatable`),`files` 只含 `bin/ lib/ agent/ templates/`——安装即得完整工具链。
109
+
110
+ ## 代码设计(六个核心决策)
111
+
112
+ 1. **单文件自包含**。模板一个文件装下全部:文档版式 CSS、批注层 CSS、UI 骨架 HTML、协议注释、store 数据、全部 JS(单 IIFE 闭包,函数不上 window)。数据与代码同文件 → 拷文件即拷走全部状态,邮件发送/归档/离线阅读都成立。代价是模板大(约 4000 行),由哨兵分区管理(见第 6 条)。
113
+
114
+ 2. **批注锚定 = TextQuoteSelector 四元组**。每条批注存 `{quote 原句, prefix 前文, suffix 后文, ordinal 出现序号}`(W3C Web Annotation 思路),文档小幅改版后仍能重定位。实现上 `core.js buildHtmlIndex` 与浏览器端 `buildIndex` **同规则双实现**:建索引时统一折叠空白;同一原句多处命中时 `bestOccurrence` 按前缀右对齐/后缀左对齐的贴近度打分选位,全 0 分回退 ordinal(兼容老数据)。**双实现改动必须两侧同步**(有测试钩住)。
115
+
116
+ 3. **rev 铁律 + 双层账本**。主文件 store 只留骨架 history(`{rev,time,op,n,bodyOps?}`,默认 60 条、可调 10~500),全量快照写同目录 `.annotatable/<文件名>.hist.json`(默认 1000 版、可调 50~5000,带 DOC_ID 归属校验防复制文件互染);外置档案写失败自动降级回主文件(上限 120)。快照容量规则在模板与 core.js 亦为双实现同步。
117
+
118
+ 4. **bodyOps 纸条:人手与 AI 同账回滚**。人手编辑保存时做**块级 diff**(掐头去尾取中段)折算成 `{o原文, n新文, b前30字符, a后30字符}` 的纸条,与 AI 的 `edit` 记进同一账本(op=`✏️ 正文编辑`)。回滚在**磁盘文本**上倒序逆放纸条,且回滚条目自己也记 bodyOps——「撤销的撤销」可组合,任意编辑↔回滚序列都正确;纸条上下文定位失败则整体中止,不留中间态。纸条 >20KB 只存外置档案,不撑主文件。
119
+
120
+ 5. **防烤入三重**。运行时状态(悬浮预览、尾节垫片、编辑态类、阅读态类、同步球状态)绝不写进文件:① 保存时 `buildAnnotatedHTML` 主动剥离;② 启动时 boot 清理老版本浏览器烤入的残留;③ `@media print` 隐藏。三者缺一都会在真实使用中累积垃圾(v1.16 曾吃过 4 个重复悬浮提示的亏)。
121
+
122
+ 6. **哨兵分区支撑 inject**。批注层样式与 UI 骨架各有一对 `═══ … BEGIN/END ═══` 哨兵注释,`inject` 只提取哨兵区注入宿主(v1.17.0 起抽成 annotParts 四件套提取器,inject 与 templateUse 共用防双源);注入的 CSS 变量统一加 `--annot-` 前缀(`annotVarPrefix`)防污染宿主样式;`__ANNOT_NO_TOC__` 标志让注入版不接管宿主目录(v1.16.36 起首绑引导对注入版照常弹)。给批注层加新 UI 忘了放进哨兵对 = 注入版静默缺功能(v1.12 踩过)。
123
+
124
+ ## 功能实现方法
125
+
126
+ ### 批注侧(人类 → 文件)
127
+ - **划线批注**:`mouseup` 捕获选区(正文与回答大页两个根通用)→ 提取四元组锚点 → 渲染为 `<mark class="annot">`(重叠区间合并)→ 划选工具条 → 批注弹窗以**对话线程**(thread 轮次数组)组织 → `persist()` 序列化回 store 落盘;
128
+ - **回答大页**:AI 回答渲染成一篇可批注的子文档,子批注用 `scope` 字段记父批注 id——回答可以被继续划线、页内追问,追问经 `read` 重新进入 agent 视野;
129
+ - **批注面板**:按 AI 回应轮次分组折叠、点外部空白收起、已回应轮次默认折叠;历史条目展示本轮批注摘要。
130
+
131
+ ### 写回侧(agent → 文件)
132
+ - **read**:只解析 store 区块,列 `status:"new"` 批注的完整对话线(原句+上下文+全部轮次);
133
+ - **reply**:`replies.json` → 追加 ai 轮次 + 置 answered + 快照 + rev+1;
134
+ - **edit**:`edits.json`(批语是祈使句「这里改成X」时)→ `buildHtmlIndex` 定位原句 → 替换为 newText → **重算该批注锚定**(批注跟着正文走)→ bodyOps 记账供回滚;混合批注用 reply 字段一并答疑;
135
+ - **poll**:阻塞轮询,只比对 `{rev, 总数, 待回应}` 签名(文件被写盘中时静默跳过本轮);人类批注保存后打印 `POLL_HIT <json>` 以退出码 0 结束——放进 agent harness 的后台任务,进程结束即唤醒 agent 续接读批注循环(`--timeout` 到点退出码 3)。
136
+
137
+ ### Word 编辑模式(人类直改正文)
138
+ - **✏️ 进入**:`article` 设 contenteditable + 悬浮工具栏(加粗/斜体/字号步进/五色字/五色高亮/格式刷双击连续/💬批注);批注 mark 进编辑态时整体剥离(批注是层不是正文);点五色高亮出 ✕ 小钮可删高亮(unwrapMark,文字保留,Ctrl+Z 可撤销);
139
+ - **💾 保存**:块级 diff 折算 bodyOps 纸条入账(见设计第 4 条);编辑期间批注改动只标 dirty,随 Word 保存一起落盘;编辑态绝不落盘(防烤入);
140
+ - **🧭 结构操作**(标题升降级/整节移位)在非编辑态点用时,经 `wordStructuralChange` 自动走「进入编辑 → 改 → 保存 → 退出」,用户无感;
141
+ - **查找替换**:文本节点内替换(跨标签匹配不支持,界面如实提示);
142
+ - **Markdown 快捷输入**:编辑态行首敲 `#`–`######`、`-`/`*`、`1.`/`1)`、`>` 后按空格,整块即时转为标题/无序列表/有序列表/引用——标记删除与块转换全走 execCommand,Ctrl+Z 可逐步撤销;标题行尾回车自动新建正文段(Word 习惯),标题中部回车保持浏览器原生同级拆分;```` ``` ````+空格或工具栏「</> 代码块」钮把当前段整块转为代码块——块内回车换行、``` 独占一行+空格收尾回正文段,悬停代码块右上角出「⧉ 复制」一键复制(块内 `<br>` 折算成换行符)。
143
+
144
+ ### 🧭 导航窗格
145
+ - **目录树**:现场扫描 h1–h5 生成(**零 token**,注入版同享);行与标题严格 1:1——折叠箭头是空 span + CSS 三角,不污染 `row.textContent`(外部探针/e2e 依赖行文本==标题文本的约定);
146
+ - **窗口化**:标题栏拖拽(一拖即脱离左泊位变悬浮)+ 原生 resize,开关/位置/尺寸按 `annot:nav:<SCOPE>` 记 localStorage;泊位时给 body 写入与窗格等宽的左内边距 `--annot-nav-space`(ResizeObserver 跟随),`min-width:1100px` 生效、窄屏自动悬浮;
147
+ - **章节折叠**:每行 ▾/▶ 点按折/展整棵子树(`stopPropagation` 不触发跳转);`applyFolds()` 每次从祖先链重算可见性——外层折叠吞掉内层、展开外层后内层保持折叠,嵌套天然正确;折叠状态按标题文本存(上限 60 条防撑爆),随文件记忆;搜索时忽略折叠全展开、清空恢复;
148
+ - **精准跳转**:绝对坐标 `scrollTo(top = 标题top - 88px)`(弃用 scrollIntoView center——长卡片页会甩到屏中);尾节受浏览器滚动钳制物理上滚不到 88px,点行时按缺口给文档尾垫 `#annot-tailpad`(Word 式 scroll-past-end,运行时垫片,保存剥离);
149
+ - **Del 移除书签**:窗格开着时点选标题行(`.sel` 高亮)按 Del,只把这条书签从目录移除——**正文一字不动**(连标题行都保留,文件零写入);按标题文本随窗格状态记忆(localStorage),窗格底部 ♻️ 一键恢复;焦点在输入框或弹层开着时不拦 Del。
150
+
151
+ ### Markdown 全面兼容
152
+ - **一键转换 `annotatable md <文件.md>`**:marked GFM 解析,正文完全遵照原文(表格/代码围栏/任务列表/删除线照常渲染),表格外包 `.table-wrap` 套模板容器样式,页面标题取首个一级标题(`--title` 可覆盖、`-o` 指定输出,默认与 md 同目录同名);只叠加批注层,批注/编辑/历史全部可用;
153
+ - **内置依赖(vendored)**:两处第三方库以单文件形式内置在包内 `lib/vendor/`(均 MIT):**marked 4.3.0**(Markdown→HTML,`md` 转换)、**temml 0.14.0**(LaTeX→MathML,公式编译,编译失败自动保留 LaTeX 原文)。二者均为**惰性加载**——仅 `md` 命令付出 require 成本,其余命令零加载;`npm install annotatable-html` **不拉任何外部依赖**,离线/内网环境可装。升级方式:用新版 CJS 单文件覆盖 `lib/vendor/` 对应文件后跑全量测试。
154
+
155
+ ### 📖 阅读模式
156
+ 一键隐藏全部功能按钮(fab/✏️/同步球/wordbar/查找),导航窗格**收起不隐藏**(把手可随时复开);📖 自身保持可见作为退出入口;Esc 分级退出(先关残留弹层再退阅读);与编辑模式互斥。**划线批注在阅读模式照常可用**——这是产品哲学,不是遗漏。
157
+
158
+ ### 引导、保存与自愈
159
+ - **首绑引导层**:未绑定的文档打开 800ms 后(等句柄恢复与 serve 探测落地)弹全屏两步引导:①复制文件夹路径 ②绑定文件夹(推荐)/只绑定本文件;绑定成功记 sessionStorage 本会话不再弹;卡片只放「📁 绑定文件夹(推荐)」一个绑定选项,右上角小字「先不绑定,直接浏览 →」(v1.16.39 精简);引导**文案本身可手改**——入口在批注面板底部「✏️ 引导文案」(走标准 persist 记账),inject 宿主无手改入口(v1.16.36);
160
+ - **保存三通道**:① serve 同源 POST(全自动,全浏览器)② File System Access 句柄(file:// 打开,Edge/Chrome,首次点选授权)③ 下载带批注的 HTML(降级);
161
+ - **镜像自愈**:批注/句柄在 IndexedDB 留镜像缓存,key 按 `DOC_ID:location.pathname` 隔离(复制文件不互染);文件本体永远是数据源,镜像只是加速与自愈;
162
+ - **历史面板**:日期分组、⚓回滚锚、点行聚合预览(全程 textContent 渲染防自产 XSS)、容量可调(⚙)。
163
+
164
+ ### 服务器安全(lib/serve.js)
165
+ 仅监听 127.0.0.1;`/__annot/save` 只收 `application/json`(拦跨站简单请求)、Origin 必须为空或 localhost、写入目标限定伺服目录内的 `.html`(防路径越界)、body 上限 20MB;中文文件名的百分号编码会先解码(曾因此全军覆没过的坑)。
166
+
167
+ ---
168
+
169
+ ## Token 消耗(v1.20.0 实测)
170
+
171
+ 分词器 o200k_base(GPT-4o 系词表);回答正文本身不计入「额外」——无论哪种方式 AI 都要生成同样的回答。**复现:`node test/token-audit.js`**(需 `npm i` 装 devDependency gpt-tokenizer)。完整方法论与历史版本数据见 `docs/批注工作流Token消耗测试报告.md`。
172
+
173
+ ### 使用侧:agent 每轮「读批注 → 写回」
174
+
175
+ | 待回应批注 | read 输出 | replies.json | 命令 | **每轮合计** |
176
+ |---|---|---|---|---|
177
+ | 1 条 | 141 | 41 | 30 | **212** |
178
+ | 5 条 | 684 | 209 | 30 | **923** |
179
+ | 20 条 | 2,691 | 812 | 30 | **3,533** |
180
+ | 50 条 | 6,706 | 2,018 | 30 | **8,754** |
181
+
182
+ 边际成本 **每条批注 ≈ +174 tok**,每轮固定命令开销 ≈ 30 tok——**与文档体量完全无关**(read 只输出待回应批注,不读正文)。写回包装:`replies.json` JSON 包装 ≈13 tok/条,`edits.json` ≈16 tok/条(newText 新正文属本来就要生成的内容),`poll` 唤醒签名 ≈31 tok/次。
183
+
184
+ ### 一次性成本
185
+
186
+ | 组件 | Token |
187
+ |---|---|
188
+ | AGENTS.md(init 分发,agent 读工作流) | 1,927 |
189
+ | SKILL.md(skill 安装触发壳) | 891 |
190
+
191
+ ### 生成侧:创建一份可批注文档
192
+
193
+ | 路径 | 额外 token |
194
+ |---|---|
195
+ | `annotatable new` / `inject` / `md`(推荐) | **≈ 9**(一条命令,批注层由模板免费带入) |
196
+ | agent 不用工具手写完整批注层 | ≈ **5.5 万**(批注层合计 54,789 tok,`test/token-audit.js` 可查明细) |
197
+
198
+ ### 对比基线(同样回答 N 条批注,以当前样例文档计)
199
+
200
+ | N | 本工作流 | 传统①AI 读整份 HTML | 传统②人类粘贴全文 |
201
+ |---|---|---|---|
202
+ | 1 | 212 | 61,484(省 **99%**) | 2,377(省 **91%**) |
203
+ | 5 | 923 | 61,484(省 **98%**) | 2,377(省 **61%**) |
204
+ | 20 | 3,533 | 61,484(省 **94%**) | 粘贴更省* |
205
+ | 50 | 8,754 | 61,484(省 **85%**) | 粘贴更省* |
206
+
207
+ \* 诚实注:小文档+大批注量的极端组合下,粘贴全文的原始 token 更少。但粘贴方式每轮都要重贴全文、无法增量回应、无结构化状态(谁已答/谁未答)、无版本回滚,且成本随文档体量线性增长(10 万字文档每轮 6–10 万 tok);本工作流只随批注数线性增长。批注是稀疏活动——实际使用中绝大多数轮次 ≤10 条。
208
+
209
+ ### 输出侧:锚定让回答也更省
210
+
211
+ 批注锚点已持久记录了原句与位置,线程式回复不需要聊天式的「复述问题+承接过渡」(docs 报告 §7 实测):单条回答省 **59%**,三轮使用生命周期(答→追问→回看)省 **67%**——第 3 轮无新批注时输出趋近于零。这是结构使然,不是提示词纪律。
212
+
213
+ ---
214
+
215
+ ## 浏览器支持
216
+
217
+ - **Edge / Chrome**:完整能力。file:// 打开经 File System Access API 自动写回(首次绑定授权一次);
218
+ - **Firefox / Safari**:配合 `annotatable serve` 同样获得完整能力(服务器模式自动写回);不用服务器时批注可用,写回退化为「下载带批注的 HTML」。
219
+
220
+ ## 服务器模式(真·全自动)
221
+
222
+ ```bash
223
+ annotatable serve # 伺服当前目录,http://localhost:8973
224
+ ```
225
+
226
+ 浏览器打开伺服地址即用:无需绑定、无需授权,批注经同源请求静默写回文件(安全边界见上)。左下角同步小球显示 ✓ 绿色即生效。
227
+
228
+ ## 给第三方脚本用的保存清理钩子(v1.31.0)
229
+
230
+ 批注文档欢迎叠加自定义交互壳(翻页条、悬浮目录……)。但保存=整页序列化,**运行时自建的 DOM 会被烤进文件**——下次打开静态壳与运行时壳并存,`getElementById` 命中静态死元素,控件集体失灵(重复 id 撞车)。给脚本加一行注册即可断根:
231
+
232
+ ```js
233
+ // 方式一:选择器数组——保存时从序列化结果里删掉这些运行时节点
234
+ window.__annotSaveStrip = ['.pf-bar', '.pf-toc', '#my-float'];
235
+
236
+ // 方式二:清理函数——接收整棵克隆树,自查自删(适合动态规则)
237
+ window.__annotSaveStrip = function (clone) {
238
+ clone.querySelectorAll('.my-shell[data-runtime]').forEach(n => n.remove());
239
+ };
240
+ ```
241
+
242
+ - 注册前后行为完全兼容:不注册的脚本维持原样(工具不擅自删你的节点);
243
+ - 静态写在源码里的 `<script>` 标签不受影响,只有运行时节点被剥离;
244
+ - 忘了注册也有兜底:agent 侧 `read/reply` 的结构体检会报「重复 id N 个——疑似运行时 DOM 被烤入磁盘」。
245
+
246
+ ## live-sync 磁盘漂移检测(v1.32.0)
247
+
248
+ 热载以 store rev 为准绳有个盲区:**改盘但不 bump rev 的写手**(agent 的 python 直改、维护手术、模板升级脚本)曾被页面整体无视——旧会话与磁盘静默脱钩,还继续以旧结构保存放大污染。现在页面维护一份「壳指纹」(正文区 + 批注/回答两个 store 区块剥掉后的磁盘文本),每次探盘前后对比:
249
+
250
+ - **壳变了**(脚本/样式/壳 DOM)→ 页面顶部常驻琥珀条:「磁盘已有正文之外的更新」+ **刷新对齐**(先把防抖里的最后一笔冲落盘再 reload,零丢失)/ **知道了**;
251
+ - **只有正文变**(rev 不动)→ 照旧直接热载上屏,不打扰;
252
+ - **rev 递增且壳也变** → 条上话术缓和为「正文已热同步;另有正文之外的更新,建议刷新」;
253
+ - **正常 agent 回注**(只动 store+正文)→ 壳指纹不变,零提示零噪音;页面自己保存也用写出内容续基线,绝不自报。
254
+
255
+ 局限:inject 模式(正文区含批注 UI 的宿主页)正文无法精确剥离,壳检测降级为只看 head 区。
256
+
257
+ ## 值守心跳与状态灯(v1.33.0)
258
+
259
+ 值守 poll 是一次性进程(命中即退出唤醒 agent),靠 agent 一班班续挂——**上下文耗尽 / 会话关闭的瞬间链条会静默断掉**:serve 不知道没有 watcher,页面一切正常,人对着页面喊话几十分钟无人应答(真实事故:42 分钟值守真空)。现在链条的生死变得可观测:
260
+
261
+ - **心跳**:poll 存活期间每轮写 `.annotatable/<文件>.watch.json`(pid / startedAt / lastBeat / state);命中或超时退出时改写为 `handoff`(交班)——进程死了心跳就停,文件成为「最后值岗」的物证;
262
+ - **页面状态灯**(右下角,serve 模式常驻):🟢 **值守中**(心跳 ≤15s)/🟡 **心跳超龄**(watcher 疑似暴毙)/🟡 **换班中**(交班 ≤5 分钟,agent 应答/重挂中)/🔴 **无人值守 · 最后心跳 X 分钟前**/🔴 **未挂值守**——让人在朝深渊喊话之前,先看见灯是红的。file:// 页面无值守概念,灯不出现;
263
+ - **`/__annot/status?path=…`**:serve 只读端点,报 watcher 存活与心跳年龄(撕裂心跳按无心跳保守渲染);
264
+ - **断档补捞**:`read` 与 `poll` 启动时对比待回应批注的出生时间与上一班最后心跳,报告「值守离线期收到 N 条批注(最早一条等待 X)」——两班之间收到的批注不再只靠进程考古才发现,agent 一眼定优先级。
265
+
266
+ 架构级后续(信箱 inbox.jsonl、常驻 `watch` 命令)已设计待拍板,见 issue #6。
267
+
268
+ ## 让任意 agent 触发本工作流
269
+
270
+ - **已初始化目录**:什么都不用做——agent 打开目录自动读 `AGENTS.md`,生成 HTML 默认走可批注格式;
271
+ - **任意目录**:对 agent 说「生成一份可批注的 HTML 文档《XX》:先 npx annotatable-html init(已初始化则跳过),再 new,把内容写进各 section」;
272
+ - **一步装全局**:`npx annotatable-html skill` 安装平台 skill + `/html4agent` slash 命令(支持 ZCode / Claude Code,含版本自检自动升级),之后「可批注」「读批注」等短语自动触发,零提示词。
273
+
274
+ ## 测试
275
+
276
+ ```bash
277
+ node test/run.js # 171 项断言:锚定/账本/回滚/协议/模板风格/Markdown 兼容/主题工坊/整页变模板
278
+ node test/token-audit.js # Token 审计(本文档数据来源)
279
+ node test/theme-e2e.js # 主题/自定义模板 端到端(11 项)
280
+ ```
281
+
282
+ 另有十余个无头 e2e(puppeteer-core + Edge,经 `annotatable serve` 的 http 访问)覆盖 编辑→落盘→回滚全链路、面板分组、注入宿主、首绑引导、导航窗格几何与跳转精度、尾节垫片、阅读模式、章节折叠、Markdown 全链路(md 转换→活批注面→编辑态 md 输入→导航书签移除/恢复)、块拖拽(⠿ 手柄换序→落盘,模板文档与 inject 注入宿主各测一份)、**主题工坊(studio 控件→预览烤入→保存/设全局→new/md 接入,v1.17.0)**。
283
+
284
+ ## 设计说明
285
+
286
+ 批注锚定采用 W3C Web Annotation(TextQuoteSelector)思路,数据模型与协议完整公开在包内 `AGENTS.md`——任意具备文件读写能力的 agent(Claude Code / Codex / Cursor / ZCode / OpenCode…)均可接入,不绑定任何一家 AI 服务,也不需要任何运行时基础设施:一个浏览器 + 一个能读写文件的 agent,就是全部依赖。
287
+
288
+ ## License
289
+
290
+ MIT
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ /* 用法: node agent/annot_answer.js <html路径> <answers.json>
4
+ * answers.json: [{"annId":"批注id","title":"卡片标题","rungs":[{"title":"阶标题","body":"阶正文","checkpoint":"检查点(可选)","answer":"参考答案(可选)"}]}]
5
+ * 合并语义:id 或 annId+title 命中 → 整卡替换(「继续」时全量带上旧阶+新阶);否则新增。
6
+ * 写入 answers-store 区块、批注 rev+1 入账本。逻辑在 ../lib/core.js */
7
+ require('../lib/core.js').applyAnswers(process.argv[2], process.argv[3]);
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ /* 用法: node agent/annot_edit.js <html路径> <edits.json>
4
+ * edits.json: [{"id":"批注id","newText":"改后正文","reply":"可选说明"}]
5
+ * 定位批注原句→替换正文→重算该批注锚定→快照+rev+1。逻辑在 ../lib/core.js */
6
+ require('../lib/core.js').applyEdits(process.argv[2], process.argv[3]);
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ /* 用法: node agent/annot_poll.js <html路径> [--timeout 秒] [--interval 秒]
4
+ * 阻塞等待新批注:已有待回应立即退出;否则轮询,人类划线批注保存后打印
5
+ * POLL_HIT <json> 并以 0 退出(配 harness 后台任务唤醒 agent);超时以 3 退出。
6
+ * 逻辑在 ../lib/core.js */
7
+ var args = process.argv.slice(3);
8
+ function flag(name, dft) {
9
+ var i = args.indexOf(name);
10
+ return i !== -1 ? args[i + 1] : dft;
11
+ }
12
+ require('../lib/core.js').pollAnnotations(process.argv[2], { timeout: flag('--timeout', 0), interval: flag('--interval', 3) });
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ /* 用法: node agent/annot_read.js <html路径>
4
+ * 列出待回应批注(含完整对话线)。逻辑在 ../lib/core.js */
5
+ require('../lib/core.js').readPending(process.argv[2]);
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ /* 用法:
4
+ * node agent/annot_reply.js <html路径> <replies.json> 批量写回
5
+ * node agent/annot_reply.js <html路径> --stream '段落' --id 批注id 流式开始/续段
6
+ * node agent/annot_reply.js <html路径> --stream --stdin --id 批注id < 段落.txt
7
+ * node agent/annot_reply.js <html路径> --stream-end '全文' [--id 批注id] 收尾
8
+ * 追加 ai 轮次、维护快照、rev+1。流式退出码:0=成功 4=被打断 5=批注已删 6=人类编辑中(让路) 7=没有打开中的流式轮。
9
+ * 逻辑在 ../lib/core.js */
10
+ var fs = require('fs');
11
+ var args = process.argv.slice(3);
12
+ var flags = {}, positional = [];
13
+ for (var i = 0; i < args.length; i++) {
14
+ if (args[i].indexOf('--') === 0) {
15
+ var k = args[i].slice(2);
16
+ if (k === 'stdin') { flags[k] = true; }
17
+ else if (args[i + 1] && args[i + 1].indexOf('--') !== 0) { flags[k] = args[i + 1]; i++; }
18
+ else { flags[k] = ''; }
19
+ } else positional.push(args[i]);
20
+ }
21
+ var core = require('../lib/core.js');
22
+ var sText, sEnd = false;
23
+ if (flags.stream !== undefined) { sText = flags.stream; }
24
+ else if (flags['stream-end'] !== undefined) { sText = flags['stream-end']; sEnd = true; }
25
+ if (sText !== undefined) {
26
+ if (flags.stdin) { try { sText = fs.readFileSync(0, 'utf8'); } catch (e) { sText = ''; } }
27
+ var r = core.streamReply(process.argv[2], { text: sText, id: flags.id, end: sEnd });
28
+ if (r.status === 'ok') {
29
+ console.log((r.done ? '✅ 回答完成' : r.started ? '▶️ 流式回答开始' : '➕ 已追加段落') + ' · 批注 ' + r.annId + ' · rev=' + r.rev);
30
+ if (r.kept) console.log('⚠️ 收尾没带全文——已保留已流式内容收尾(' + r.chars + ' 字)。本条回答以此为准,不再重发');
31
+ if (r.closed && r.closed.length) console.log('ℹ️ 顺手封轮(未收尾的遗留流式轮,文本保留): ' + r.closed.join(', '));
32
+ } else if (r.status === 'interrupted') {
33
+ console.log('INTERRUPTED 人类已按⏹停止(批注 ' + r.annId + ' · rev=' + r.rev + ')——本轮作答终止,不要再追加');
34
+ process.exitCode = 4;
35
+ } else if (r.status === 'target-gone') {
36
+ console.log('TARGET_GONE 批注 ' + r.annId + ' 已被删除——本轮作答终止');
37
+ process.exitCode = 5;
38
+ } else if (r.status === 'empty') {
39
+ console.log('空段落:--stream 需要文本或 --stdin');
40
+ process.exitCode = 2;
41
+ } else if (r.status === 'empty-end') {
42
+ console.log('空收尾被拒绝:--stream-end 需要完整全文(没有流式轮时收空尾=落一条空回答)');
43
+ process.exitCode = 2;
44
+ } else {
45
+ console.log('NO_STREAM 没有打开中的流式回答——首段请带 --id 批注id(退出码 7)');
46
+ process.exitCode = 7;
47
+ }
48
+ } else {
49
+ core.applyReplies(process.argv[2], positional[0]);
50
+ }