PRD 原型标注(v1.1.1)
目的
让 HTML 原型自带可交互的需求说明:数字角标定位到具体元素,点击弹出需求详情;
同一份文件对作者是可编辑工作台,对读者是只读文档。
v1.1.0 在旧版基础上整合了 html-annotation-pro 的优势:独立图层角标(免疫 overflow 裁剪)、
隐藏式作者工具栏(快捷键唤出)、点击新建(拾取模式)、标注管理面板(拖拽排序/按位置重排)、
按页面独立的动态序号与页面级导航/筛选;并去除一切动效,保持零依赖单文件。
交互模型(谁看到什么)
| 角色 | 操作 | 结果 |
|---|---|---|
| 读者 | 单击角标 | 只读富文本弹层(链接高亮、新标签页打开);无任何编辑入口 |
| 作者 | Mac ⇧A / Windows Alt+A | 唤出/隐藏右下角工具栏(读者默认看不到任何作者 UI) |
| 作者 | 忘记快捷键 | 工具栏已唤出时,最左端灰底小字显示当前快捷键;看它就知道 |
| 作者 | Ctrl/⌘ + 点击角标 | 打开富文本编辑弹窗;「保存」后提示保存成功并自动关闭 |
| 作者 | 工具栏「点击新建」 | 拾取模式:点页面任意元素 → 自动生成 CSS 选择器 → 打开编辑弹窗 |
| 作者 | 工具栏「上一个 / 下一个」 | 只在当前页面的标注间循环(计数 = 当前页面条数);单页原型即全文件 |
| 作者 | 工具栏「☰ 管理」 | 右侧滑出管理抽屉:默认只列当前页面的标注(可切「全部页面」);定位/编辑/删除/拖拽排序/按位置重排;底部两个 Word 导出按钮 |
| 作者 | 管理抽屉「导出本页面 PRD」 | 只导出当前页面标注的 Word 文档(docx) |
| 作者 | 管理抽屉「导出文件 PRD」 | 导出本文件全部标注的 Word 文档,按页面分组 |
| 作者 | 工具栏「保存并导出」 | 把最新 JSON 固化回 HTML 并下载新文件(仅此一处导出 HTML) |
| 作者 | 工具栏「⧉ 复制全部JSON(M)」/「⧉ 复制本页JSON(N)」 | 复制一份信封(scope + items + baseHash + 页面名/序号/是否为空)到剪贴板,粘贴给 AI 即可回写进文件(v1.1.0 首选通道,不用导出)。括号里是条数,随切页实时刷新 |
| 作者 | 工具栏「⟲ 恢复缓存(N)」 | 只在检测到浏览器缓存与文件内容不一致时出现;点击把缓存里的标注找回内存(多用于误关页面后),需再「保存并导出」才固化 |
| 作者 | 工具栏「✕ 丢弃缓存」 | 与「⟲ 恢复缓存」成对出现;确认磁盘文件才是最新的,清掉浏览器里的旧缓存,避免误点恢复把它盖回去(只清缓存,不动文件) |
| AI | 离线读浏览器 leveldb | 兜底通道:用户没复制 JSON 时,AI 直接取回标注(见「从浏览器读回标注」) |
> 为什么 Mac 改用 ⇧A:Mac 内置键盘上 ⌥Option 在 ⌘Command 外侧,而外接 Windows
> 布局键盘插到 Mac 后,那个外侧位置是 Win 键、真正的 Option 落在贴着空格的 Alt 键上。
> 也就是说「同一个手指位置,内置键盘是 ⌥、外接键盘是 ⊞」,外接用户按习惯位置常常按错。
> Shift 键在所有键盘上的位置都一致(左下/右下最外侧),换成 ⇧A 就不存在这个歧义。
> Windows/Linux 的 Alt 位置没有该问题,沿用 Alt+A。
>
> 快捷键的例外(都是刻意设计,别当成 bug):
> 1. 焦点在 输入框/文本域/可编辑区 时不触发——否则原型里打不出大写字母 A;点页面空白处
> 或按 Esc 让焦点离开输入框即可恢复
> 2. 编辑弹窗开着时不触发——避免写标注文案时手滑把工具栏叫出来
> 3. Windows/Linux 上 ⇧A 不触发(那边只认 Alt+A),跨平台不会误唤出;
> Mac 的 ⌥A 是备用键,会触发
>
> Mac 上 ⇧A 唤不出时的排查:先看焦点是不是卡在输入框;再看「系统设置 → 键盘 → 键盘快捷键」
> 是否占用了 ⇧A。另外 macOS 自带中文拼音输入法默认用 Shift 切换中/英,⇧A 可能顺带把输入法
> 切成英文(preventDefault 拦得住字符输入,拦不住输入法状态切换)——这种情况改用备用键 ⌥A。
所有作者操作(新增/编辑/删除/排序)在内存生效并即时镜像到浏览器 localStorage:
镜像只是「找回 + 让 AI 读回」用的中转,交付仍以文件里的 JSON 数据区为准,
改完必须「保存并导出」固化。Word 导出是只读行为,不会改动内存数据。
回写(v1.1.0):把浏览器里的标注写回原型文件
作者标完 → 快捷键唤出工具栏(Mac ⇧A / Windows Alt+A)→ 点「⧉ 复制全部JSON(M)」或「⧉ 复制本页JSON(N)」→ 把信封粘贴到对话 → AI 写回目标 HTML 文件。
两个按钮只差范围:全部装整套标注(scope:"all");本页只装当前可见页面那几条(scope:"page",多带一个 page 字段)。
单页原型(没有 data-prd-page)下"本页"恒等于"全部",此时「本页」按钮会自动隐藏。
这是首选通道:比「AI 去读浏览器 leveldb」可靠(不依赖 Chrome 版本、存储格式、snappy 压缩),
也比「用户点保存并导出、再把新文件发回来」少一步,且不产生重复文件。
三种指令(第 3 条只是第 2 条加限定范围)
| 指令 | AI 行为 |
|---|---|
| 「已标注,请回写」 | 只把浏览器标注原样写回文件,不做任何预填 |
| 「已标注,请回写并预填PRD」 | 写回 + 为内容为空的角标预填 title/body;已有内容的角标一字不动 |
| 「…并预填「XX页面」第N个角标的PRD」 | 同上,但只预填指定页面 + 指定序号那一条,其余保持原样 |
用户没说页面名 → 按当前页面理解;单页原型可省略页面名。
「空则预填、非空不动」让它天然幂等:同一份信封反复回写,结果都一样。
信封结构(作者复制出来的东西)
{
"tool": "prd-annotation",
"version": "1.1.1",
"scope": "all",
"file": "/C:/Users/xxx/原型.html",
"baseHash": "h1a2b3c4d-520",
"at": 1758000000000,
"items": [ { "anchor": "#tbl th:nth-child(9)", "pos": "tr", "page": "设备管理", "title": "", "body": "" } ],
"view": [ { "page": "设备管理", "no": 1, "anchor": "#tbl th:nth-child(9)", "title": "", "isEmpty": true } ]
}
scope——"all"(复制全部JSON)或"page"(复制本页JSON;此时多一个page字段写明页面名)items—— 标注内容,写回时就是它;scope:"page"时只是文件里的一部分,不是全部view—— 给人(和 AI)读的清单:页面名 + 页面内序号 + 是否为空。
序号运行时才现算、不落盘,所以"第N个角标"的 N 只能以view.no为准(同页面内从 1 开始)baseHash—— 页面加载那一刻,文件数据区里标注的规范化指纹(决定覆盖还是合并)。
⚠️ 它只在加载时算一次:此后在页面里新增/编辑标注都不会重算,所以它恒等于"文件基线",
不能用它推断items是不是完整的(加过角标后 items 比 baseHash 对应的多)file—— 浏览器里的页面路径,用来对上磁盘文件(Windows 下形如/C:/...,去掉开头那个/)
⚠️ 两种信封:all 与 page(处理方式不同,别混)
信封 scope |
来源按钮 | items 范围 |
AI 必须怎么做 |
|---|---|---|---|
"all" |
⧉ 复制全部JSON(M) | 全部标注 | baseHash 与文件一致 → 可整份覆盖;不一致 → merge |
"page" |
⧉ 复制本页JSON(N) | 只有当前页那 N 条 | 禁止整份覆盖(会把其他页面的标注删掉);一律走 merge 局部合并 |
scope:"page" 时,文件里"信封内没有"的标注属于其他页面,不是被删除的。
writeback.js 已据此设防:plan 明确禁止覆盖、merge 跳过删除清扫并原样保留其他页面、apply 直接拒收局部信封(要求先 merge)。
空角标判定
title 与 body 去掉 HTML 标签、 、空白后都是空串 → 空角标(view.isEmpty: true),
也就是"作者只点了位置、还没写内容"。预填只作用于这些条目;任何一边有内容就算已写,不动。
baseHash 比对算法(AI 侧必须一字不差地复刻)
组件内实现(djb2 + 长度,作用在规范化后的 JSON 上):
function normalizeList(list) {
return JSON.stringify(list.map(x => ({ anchor: x.anchor || '', pos: x.pos || 'tr',
page: x.page || '', title: x.title || '', body: x.body || '' }))); // 字段顺序固定、无缩进
}
function hashText(s) {
let h = 5381;
for (let i = 0; i < s.length; i++) h = (((h << 5) + h + s.charCodeAt(i)) >>> 0);
return 'h' + h.toString(16) + '-' + s.length;
}
算「文件当前基线」时,还要先复刻组件的加载处理:
读数据区 JSON → 剔除 deleted 为真的条目 → 若存在数字 id 则按 id 升序排序 → normalizeList → hashText。
(s.length 与 charCodeAt 在 Node 与浏览器里同为 UTF-16 语义,可直接对齐。)
合并规则(哈希自适应)
| 情况 | 判定 | 做法 |
|---|---|---|
payload.baseHash === 文件hash |
浏览器加载的就是当前最新文件 | 整份覆盖:items = payload.items(绝大多数情况走这条,最省心) |
| 两者不等 | 浏览器页面是旧文件(AI 改过文件后作者没刷新),或文件被别的流程改过 | 按 anchor 合并,规则见下 |
合并的匹配键 = anchor;同一 anchor 有多条时按各自出现顺序配对;page 不同视为不同条目。
- 浏览器非空 + 文件空 → 取浏览器的
- 浏览器空 + 文件非空 → 保留文件(AI 之前预填的内容不会被空条目冲掉)
- 两边都非空 → 以浏览器为准(那是作者最后编辑的)
- 两边都空 → 要预填就预填,否则保持空
- 只在文件里、浏览器已没有 → 视为作者在浏览器里删掉了;默认删除,但必须在回显清单里单独列出让用户确认
- 只在浏览器里、文件没有 → 新增,直接采纳
写回步骤(不得跳步)
落地一律用 scripts/writeback.js(plan → merge → apply),下面 7 步就是它内部做的事;
手工时必须逐步照做:
- 定位文件:用
payload.file对上磁盘路径(去掉前导/);对不上就问用户,别猜。 - 算文件 hash,按上表决定「整份覆盖 / 合并」——
writeback.js plan会替你算并直接给出结论。 - 回显清单再写(红线):先把将要写入的内容列给用户——
每条一行页面名 / 第N个 / 锚点 / 状态(新增·更新·预填·保留·删除),
末尾给统计:总条数、其中预填 N 条、删除 N 条。用户确认后才落盘。 - 写前自动备份(
apply默认已做):原文件复制为<原名>.bak-<时间戳>.html(同目录),确认备份成功再写。 - 只替换数据区(
apply只做这一件事,其余字节零改动):用id="prd-annotations"定位那个 JSON script,
只替换两个标签之间的内容(JSON.stringify(items, null, 2)),其余字节一个都别动。
⚠️ 不要写id/number字段——序号不落盘。 - 校验:重新读回文件,确认 JSON 可解析、条数正确、无重复锚点;
再用浏览器打开验证角标数 = 条数、控制台无报错。 - 提醒用户刷新:AI 改的是磁盘文件,浏览器里还是旧页面。
刷新后工具栏若出现「⟲ 恢复缓存(N)」,说明 localStorage 里那份是旧的 →
点「✕ 丢弃缓存」以文件为准。
⚠️ 千万别点「⟲ 恢复缓存」——那会把旧缓存盖回刚写好的文件。
预填模板
见 references/annotation-writing.md 的「回写预填」小节:
title 是与该元素一一对应的短标题(≤12 字),body 按
需求描述 / 业务规则 / 数据来源 / 业务限制 / 边界异常 五段组织。
预填必须基于原型里真实存在的元素与字段来写;原型里看不出来的(接口名、表字段、枚举值)
写「待确认」,绝不编造。
从浏览器读回标注(leveldb 兜底通道)
> 优先让作者复制 JSON(上一节)。本节只在作者没复制、或需要核对浏览器实际状态时用。
用户往往会直接说「我在 xxx.html 上标好了,你读回来 / 预填 PRD」。
不要问用户导出文件,也不要临时另造通道——组件已经把标注镜像进 localStorage,按下面步骤读即可。
数据在哪
| 键 | 内容 |
|---|---|
prd-annos::<encodeURIComponent(location.pathname)> |
该项目原型当前全部标注(JSON 数组,字段同数据区) |
prd-annos::__last__ |
{ path, n, at }:最后一次写入的文件路径、条数、时间戳 → 用它定位目标文件 |
prd-annos::__snapshot__::<路径编码> |
检测到未固化缓存时自动留的历史快照(主键后来被覆盖时从这里救)。v1.1.1 起按路径隔离——旧版是不带路径的全局单键,同时打开多个原型时,后加载的文件会把先加载文件的快照覆盖掉 |
prd-annos::__view__ |
{ path, at, baseHash, count, view }:带页面名 + 页面内序号 + isEmpty 的可读清单,序号不落盘,只能从这里拿到;baseHash 与复制信封里的同源 |
__view__ 让你不用打开浏览器也知道「第 N 个角标在哪一页、锚点是什么、是不是空角标」,
是「预填第N个角标」这类指令在兜底通道下的定位依据。
键里含完整文件路径 → 不同原型互不串味;file:// 打开时所有本地页面共用 file:// 桶。
读回步骤(Windows + 本机 Chrome 实测通过)
- 复制 leveldb(Chrome 正在跑也能复制,文件被独占锁不影响
copyFileSync):
<Chrome User Data>/Default/Local Storage/leveldb/*→<tmp>/Default/Local Storage/leveldb/。
⚠️ 必须落在Default子目录下,否则 Chromium 认不到这套存储。 - 用 Chromium 打开任意
file://空白页(这一步同时解决.ldb是 snappy 压缩、纯文本读不出值的问题):
js const require = createRequire('<node 工作区路径>/'); // ESM 不读 NODE_PATH const { chromium } = require('playwright-core'); const ctx = await chromium.launchPersistentContext(tmp, { channel: 'chrome', headless: true }); const page = await ctx.newPage(); await page.goto('file:///<任意本地空白页>'); const data = await page.evaluate(() => { const out = {}; for (let i = 0; i < localStorage.length; i++) { const k = localStorage.key(i); if (k.startsWith('prd-annos::')) out[k] = localStorage.getItem(k); } return out; }); - 筛
prd-annos::键:非__后缀的那个就是目标文件的标注数组;
先读__last__确认 path 与用户说的文件一致(不一致说明他标的其实是另一个文件);
再读__view__拿「页面名 + 页面内序号 + isEmpty」和baseHash。 - 写回:走上一节「写回步骤」的第 2~7 步(算文件 hash → 回显清单 → 备份 →
只替换数据区JSON.stringify(items, null, 2)→ 校验 → 提醒刷新)。
序号不落盘,不要写id/number字段。 - 若用户还要「预填 PRD」(他标了位置、没写内容),按
references/annotation-writing.md
的「回写预填」小节补title/body——只补view.isEmpty为真的条目。
环境坑(踩过的,别再试)
- 企业 DLP 加密下载目录:
Downloads里的文件(浏览器下载产物)在磁盘上是密文
(文件头固定为17 da 5f a0 16 33 cd 9a bc 5b 6c bf 14 aa 1e 33),readFileSync得到乱码、zipfile报
"not a zip file"。但copyFileSync复制到项目目录后就是明文(DLP 在非受控目录落盘时透明解密)
→ 用户真点了「保存并导出」时,先复制一份再读,别断言"文件坏了"。 .ldb读不出值:leveldb 已 compaction 的数据被 snappy 压缩,纯文本只能搜到键名;
用第 2 步的 Chromium 解析即可拿到全部键值。新写入的数据在.log(WAL)里可明文读。- localStorage 有提交延迟(约 5s):刚标完就复制可能拿不到最新值,等 7s 以上更稳。
- 别用 WorkBuddy 内置预览标注:预览页是
srcdociframe、所有原型共用同一个存储桶(必串味),
且无 CDP 端口、无法被 playwright 接管。统一引导用户走 present_files 卡片的「在浏览器中打开」
(系统 Chrome +file://地址,键按文件区分)。 channel: 'chrome'若报错,改用 executablePath:
WindowsC:/Program Files/Google/Chrome/Application/chrome.exe
macOS/Applications/Google Chrome.app/Contents/MacOS/Google Chrome
命令行脚本(防呆层,优先使用)
skill 目录下有三个脚本,把最容易出错的三件事(注入/升级、回写、验证)变成一条命令。
凡是要注入、升级、回写,一律先走脚本,不要手工改文件——下面每个坑都是踩出来的,
脚本已经把它们变成硬拒绝或自动规避。
node 用绝对路径,按系统取:
- macOS: ~/.workbuddy/binaries/node/versions/<版本>/bin/node
- Windows: C:/Users/<你>/.workbuddy/binaries/node/versions/<版本>/node.exe
(旧版写死了某台开发机的 C:/Users/<某用户名>/...,换机即失效。)
scripts/inject.js —— 注入 / 升级组件
| 命令 | 用途 |
|---|---|
node scripts/inject.js <目标.html> |
首次注入;文件里已有标注层会直接拒绝并解释原因 |
node scripts/inject.js <目标.html> --upgrade |
换新版组件,保留数据区里的标注内容 |
node scripts/inject.js <目标.html> --upgrade --dry |
只报告会做什么,不写盘 |
node scripts/inject.js <目标.html> --no-backup |
不备份(不建议) |
它挡掉的坑:
| 坑 | 脚本怎么挡 |
|---|---|
| 重复注入 → 两份数据区 → 新标注全部看不到 | 检测到 id="prd-annotations" 直接退出并提示改用 --upgrade |
用 indexOf('<style>') 定位组件样式 → 把原型自己的样式块删掉 |
只用界标 PRD-CSS-START/END 或 .prd-marker 特征定位 |
| 升级时用备份覆盖 → 连原型改动和标注一起冲掉 | 先取出数据区 JSON,剥组件 → 注入新版 → 原样写回并比对是否一致 |
| 忘记备份 | 默认备份到同目录 _备份/<原名>.bak-<时间戳>.html |
</head> / </body> 取第一次出现 → 注释里的同名标签毁掉文件 |
一律 lastIndexOf |
| 注入完没验证、写了没生效 | 落盘前 5 项自检 + 写后重新读盘复核(防外部进程回滚) |
自检任一项不过就不写盘、退出码非 0,此时原文件一字未动。
scripts/writeback.js —— 回写标注
三步走,plan 不写盘,先看清楚再动:
node scripts/writeback.js plan <目标.html> <信封.json>
node scripts/writeback.js merge <目标.html> <信封.json> -o merged.json
node scripts/writeback.js apply <目标.html> <items.json> [--base-hash h] [--no-backup]
| 坑 | 脚本怎么挡 |
|---|---|
| 拿到信封就闷头写 → 用旧页面数据盖掉文件里已预填的内容 | plan 先打印文件 hash vs 信封 hash、逐条清单(页面/序号/锚点/状态)、统计;hash 不一致就明确建议走 merge |
| 拿「⧉ 复制本页JSON」的局部信封去整份覆盖 → 静默删掉其他页面的标注 | plan 识别 scope:"page" 并明确禁止覆盖;merge 跳过删除清扫、其他页面原样保留;apply 直接拒收局部信封并要求先 merge |
| 两边都有改动不知道听谁的 | merge 按哈希自适应规则合并:浏览器非空→以浏览器为准;浏览器空+文件非空→保住文件;只在文件里→视为删除并显式提醒 |
| 顺手改坏了原型 | apply 只替换数据区两个标签之间的内容,其余字节零改动 |
| 忘记备份 | 默认备份到 _备份/ |
| 写完没生效 / 被回滚 | 写后重新读盘复核条数与数据区份数,不符直接报错让你从备份恢复 |
| 忘了提醒用户刷新 | apply 结尾固定打印两条必须转告用户的话(刷新 / 别点「⟲ 恢复缓存」) |
⚠️ apply 吃的是 items.json(纯数组),不是信封 —— 把信封直接喂给 apply 会被防呆拦下
(scope:"all" 的信封也一样,脚本只看"是不是数组")。完整链路:
plan → 拿到纯数组:hash 一致时把 env.items 落成一个数组 json(或直接跑 merge 产出,二者等价;
hash 不一致、或 scope:"page" 的局部信封则必须 merge)→ AI 只为 isEmpty 的条目预填 title/body → apply。
scripts/verify.js —— 验证(三档)
| 档 | 命令 | 端到端 | 做什么 |
|---|---|---|---|
| L1 | verify.js <文件> |
~0.3 s | 静态:数据区份数 / 起止界标 / JSON 可解析 / 组件版本 / 运行时节点是否固化进原型 / 页面标记 / 字段完整性。不开浏览器 |
| L2 | verify.js <文件> --browser |
~4.5 s | 打开页面做 DOM 断言:版本一致 / 条数一致 / 每个锚点命中数(0 = 失配,>1 = 可能串页) / DOM 实际所在页 vs 数据里声明的页 / 可见角标数 / 控制台报错。不点击、不写数据 |
| L3 | verify.js <文件> --full |
~15 s | 真的点:给每个页面临时各建 1 条 → 逐页切换验证「每页只显示本页角标」→ 新建/保存/复制 JSON → 刷新 → 验证「⟲ 恢复缓存」出现 → 点「✕ 丢弃缓存」→ 断言按钮消失 + 磁盘文件一字未动 |
各档抓不到什么(别把低档当高档用):
| 档 | 抓不到 |
|---|---|
| L1 | 锚点能否命中 DOM、角标位置、跨页串页、任何交互(快捷键唤出 / 新建 / 保存 / 复制 / 导出 / 缓存按钮) |
| L2 | 交互功能(新建、保存、复制、缓存按钮)、导出产物是否干净、角标是否被容器裁剪、跨页切换后的隔离(L2 只看当前页) |
| L3 | 拖拽排序 / 按位置重排 / 管理抽屉细节 / Word 导出内容正确性 / 真实导航点击(L3 用强制显隐切页,不是点菜单) |
什么时候必须跑 L3 —— 不靠记忆,脚本自己会提醒:
L3 通过后会把「组件指纹」写进 scripts/.verify-state.json;之后跑 L1/L2 时,
若「从未跑过 L3」或「组件代码改过」,会打印 ⚠ 并以退出码 2结束,提示必须重跑 --full。
组件不稳定期就靠这个兜底:只要组件动过,系统自己要求跑完整档。
退出码:0 = 通过且组件已完整验证 / 1 = 有 FAIL / 2 = 本档通过但组件未经 L3。
⚠️ L3 会在每个页面临时建 1 条标注(只进内存与浏览器缓存),跑完点「✕ 丢弃缓存」清掉,
并断言磁盘文件一字未动——可以放心在真实交付文件上跑。
改文件后的复核习惯(agent 侧,别偷懒)
- 用脚本或 node
fs写文件,写完立刻readFileSync断言关键字符串确实在里面。
本环境实测:Edit 工具的写入有被外部进程吞掉的情况(返回成功、磁盘没变),
不复核就会把"改好了"当交付,实际跑的还是旧内容(曾因此连出两次假失败)。 - 不要在
node -e "..."里写反引号:bash 先做命令替换,把内容搅乱,
表现为退出码 0 但文件被写坏。要改文件就写成.js脚本再跑。 - 注入 / 升级 / 回写之后,浏览器打开验证(角标数 = 条数、控制台无报错)不能省。
工作流程
- 用户说「我已经标好了」时:先问一句"方便的话点工具栏「⧉ 复制全部JSON」把信封粘贴给我"(首选通道);
若他只想让你看某一页,让他点「⧉ 复制本页JSON」——那份信封是局部的,只能 merge,别整份覆盖;
他没复制就走「从浏览器读回标注」兜底。拿到标注后按指令分流:
- 「请回写」 = 原样写进数据区,不预填;
- 「请回写并预填PRD」 = 保留他的锚点与已写内容,只为isEmpty的角标补title/body;
- 「…第N个角标」 = 只补指定页面 + 序号那一条。
写完一律走「写回步骤」(回显清单 → 备份 → 只替换数据区 → 校验 → 提醒刷新)。
不要回头再问他要标在哪个元素上。 - 确定标注位置(用户还没标、需要 AI 代劳时):用户用两种方式指明——
- 文字说明(如"在表格的操作列标注")→ 解析为目标 HTML 中对应元素
- 截图并圈画标记 → 根据截图中元素的文字/表头/按钮内容,映射到 HTML 中的对应节点;
被圈选的元素,角标默认放在该元素右上角(pos: tr),除非用户另行说明
若目标元素有多种可能,选择最语义化的一个并说明依据,不反复追问。 - 注入标注层:用脚本,不要手工改 ——
node scripts/inject.js <目标.html>
(文件里已有标注层会自动拒绝;升级组件加--upgrade)。详见「命令行脚本(防呆层,优先使用)」。
脚本不可用必须手工时,这三条是硬要求:
- 读取assets/annotation.html,把其中<style>段放入目标<head>末尾,
数据区和逻辑<script>段一起放在</body>之前。
- 注入定位必须用最后一次出现(如 Python 的rfind):注释/字符串里可能含有
</head>、</body>字样,取第一次出现会把内容注入到错误位置,毁掉整个文件。
- 两段各加一层注释界标(便于日后升级/移除时精确定位):
样式段包在 <!-- PRD-CSS-START --> 与 <!-- PRD-CSS-END --> 之间,
数据区+逻辑段包在 <!-- PRD-LAYER-START --> 与 <!-- PRD-LAYER-END --> 之间。
- 旧版 → v1.1.0 升级:直接用inject.js --upgrade;手工时以"含 prd-marker 样式的旧 style 块 +
id 为 prd-annotations 的数据块 + 其后的旧逻辑 script"整体识别替换。
- 注入后必须在浏览器中实际打开验证:角标数量 = 标注条数、无 JS 报错,方可交付。 - 多页面 / 多级页面原型:若原型有多套页面(菜单切换),先给每套页面的容器加
data-prd-page="页面名"(页面间通常互斥显隐),再给每条标注写上对应的page字段。
二级页面用父/子命名并嵌在一级容器内(见「多页面 / 多级页面机制」)。
⚠️ 页面名只写在页面容器上,菜单项等导航元素不要用data-prd-page。
不分页的单页原型不需要这些标记。 - 写标注数据:在 JSON 数据区追加条目,字段见 assets/annotation.html 注释:
anchor用 CSS 选择器锚定元素(优先 #id、语义化 class;nth-child 兜底);
page填所属页面名(多页面原型;多级页面写父/子)。
序号不落盘,且每个页面各自从 1 开始:展示序号 = 该标注在本页面内的顺序;
新增直接追加到数组末尾;删除、拖拽排序、按位置重排后,各页面内序号自动连续重排,
AI 不需要维护任何编号字段。 - 预写需求说明(body 字段):基于原型上下文预写,结构按
references/annotation-writing.md的「结构模板」(需求描述 / 业务规则 / 数据来源 /
业务限制 / 边界与异常),用<p><strong><ul><li>基础标签组织;用户可再改。
回写场景下的预填用同一文件里的「回写预填」小节(含 title 生成与"待确认"约定)。
这些内容会直接出现在导出的 Word PRD 里,避免写 web 专属样式。 - 交付说明:告知用户——读者直接点击角标查看;作者用快捷键唤出工具栏(Mac ⇧A、Windows Alt+A,Mac 上 ⌥A 也可作为备用),
Ctrl(Mac 为 ⌘)点击角标编辑,编辑器内点「保存」即存内存并关闭弹窗;
固化需在工具栏点「保存并导出」;管理抽屉底部两个按钮导出 Word PRD。
标注完要让 AI 写回文件时:工具栏点「⧉ 复制全部JSON」(或只给某一页时点「⧉ 复制本页JSON」),把内容粘贴给 AI 即可
(比导出文件更快,也不会多出一份副本)。AI 改完文件后刷新浏览器;
若工具栏冒出「⟲ 恢复缓存(N)」,点旁边的「✕ 丢弃缓存」以文件为准。
修复记录
v1.1.1(2026-09-22 全面回归后修复,共 5 项)
verify.js浏览器档硬编码channel:'chrome'(跨机器必炸):机器上只有系统 Chromium
(Linux 的chromium/ Mac 的 Chromium.app)时,--browser/--full直接抛未捕获异常退出码 1,
把 L1 结果一起掩盖,还违背「退出码 3 = 浏览器档没跑起来」的语义。修复:launch 多候选降级
(PRD_CHROMIUM_EXECUTABLE环境变量 → playwright 自带浏览器 → PATH 上的
chromium / chromium-browser / google-chrome → 最后才是channel:'chrome'),
全失败时按原语义退出码 3 并给出提示。候选解析用which/where取绝对路径
(playwright 的executablePath不解析 PATH 裸命令名,这是修的时候踩到的第二层)。inject.js --upgrade剥旧逻辑段用lastIndexOf('</script>')(数据丢失级):
无界标旧文件(v1.0 注入)升级时,若组件之后还有原型自己的<script>,或原型脚本字符串里
含假</script>,会把原型脚本一起剥掉 → 升级后原型功能静默丢失。修复:逻辑段终点改为
「数据区</script>之后、含window.PRDAnnotation的主逻辑 script 的</script>」。- Word 导出丢高亮:
execCommand('hiliteColor')在 Chrome 下把background-color写到
任意元素的 style 上(选中文本含链接时直接落在<a>自身),而旧解析只认<span>的内联样式
→ 链接上做高亮导出 Word 后静默丢失。修复:把内联样式解析提取为styleMods(),
对每个元素都先并入(加粗/斜体/下划线/删除线/高亮/颜色),再叠加标签语义。 - 数据区写
</script>字面量即坏(健壮性缺口):标注 body 里含</script>(粘贴代码示例、
AI 预填都可能出现)时,<script type="application/json">提前闭合,浏览器加载、
writeback.js/inject.js/verify.js的读取全部截断 → 标注整体丢失。修复:所有写数据区的入口
(组件exportFile、writeback.js apply、inject.js --upgrade写回旧数据)统一把
</script>转义为<\/script>(JSON 合法转义,JSON.parse自动还原,读取端零改动);
inject.js升级后的写回一致性复核改为按「解析后的语义」比较(转义后字符串不再逐字相等)。 - localStorage 快照键串味:
prd-annos::__snapshot__是不带路径的全局单键,同时打开多个
原型时,后加载的文件会把先加载文件的未固化快照覆盖掉,主键一旦被新编辑覆盖就永远找不回。
修复:快照键改为prd-annos::__snapshot__::<路径编码>,与主键一致按文件隔离;
「✕ 丢弃缓存」同步按路径清理。
复验:全部修复后在同一 3 页面原型上 L1 / L2 / L3 全档 FAIL 0,退出码 0;
另跑 51 项深度测试(读者只读红线、Ctrl 编辑、富文本、管理抽屉拖拽/重排、Word 导出含高亮、
导出 HTML 洁净度、复制信封结构、多级页面、单页原型、overflow 裁剪免疫)、14 项边界测试
(公共区域锚点、拾取 Esc、隐藏角标、恢复缓存、旧版数据兼容、hash 跨端一致)、9 项富文本测试
全部 PASS。版本号统一升至 1.1.1。
v1.1.0-patch2(2026-09-22 拆按钮:「复制本页JSON」/「复制全部JSON」)
原工具栏只有一个「⧉ 复制JSON」(整份)。拆成两个,信封加 scope 区分全量 / 局部。
- 组件:
⧉ 复制本页JSON(N)(data-op="copy-page")+⧉ 复制全部JSON(M)(data-op="copy-all")。
条数随切页实时刷新 —— 刷新函数refreshCopyBtns()挂在updateCounter()里,
而updateCounter()由syncPageState()(约每 10 帧 ≈160ms)在页面名变化时调用,所以切菜单立刻跟上。
新增scopeItems(scope)(取子集)与viewOf(list)(子集的可读清单;页内序号按子集重算,与全量口径一致)。
单页原型下"本页"恒等于"全部" → 自动隐藏「本页」按钮,避免两个一模一样的按钮。 writeback.js防呆(关键,缺了会静默删数据):scope:"page"是局部信封,
若仍按"baseHash一致 ⇒ 可整份覆盖"处理,会把其他页面的标注删掉。三处设防:
plan识别后明确禁止覆盖;merge对局部信封跳过"文件里有、浏览器没有 → 视为删除"的清扫、
把其他页面条目原样push回结果;apply拿到非数组时统一拦下 ——scope:"page"直接报错并要求先 merge,
连scope:"all"的全量信封也一并拦住并提示"apply 只吃纯数组、请先跑 merge"。
(后半条是回写实战中自己撞到才补的:plan的结束语写着"hash 一致可直接 apply",
容易被理解成"可以把信封直接喂给 apply",实际apply只接受纯数组 —— 现在报错信息会说清楚。)verify.jsL3 第 3 段:从"点[data-op="copy"]"改为断言两个按钮的文字与条数
(⧉ 复制全部JSON(总数);有页面容器时⧉ 复制本页JSON(本页数),否则断言「本页」按钮已隐藏),
再分别点击两个按钮验证 toast / 降级复制框。- 教训(这次差点判错):
baseHash是页面加载那一刻算的,此后页面内新增/编辑标注都不重算。
所以它恒等于"文件基线",不能用它推断items是否完整;也不能靠「恢复缓存」按钮的显隐去反推内存里有几条
——那个按钮只证明"缓存 ≠ 内存"。要判断条数,直接读信封的items.length和baseHash尾部的长度位。
v1.1.0-patch(2026-09-22 装机复验时修的两处脚本缺陷)
inject.js --upgrade在「空数据区」文件上必然失败(假失败,脚本保护生效、原文件未动):
判据原为logic === logicBody,把「替换后没有变化」误当成「没匹配到空数组占位」。
旧数据区本身就是[](还没标任何东西)时,dataJson也是'[]',替换是无变化操作,
于是脚本报「写回旧数据失败」退出 —— 任何空数据区的原型都升不了级。
改为先判占位符是否存在(logicBody.includes('[]\n' + '</script>')),再做替换。- L2「输入框里大写 A 能正常输入」在 Windows 上恒假 FAIL:
该用例原先拿平台主键HOTKEY去断言「能打出大写 A」—— Mac 的 HOTKEY 是 ⇧A(确实产出字符),
但 Windows 的 HOTKEY 是 Alt+A,而带 Alt 的组合浏览器本就不产出字符,
于是 Windows 档永远失败,把正常组件误判成有 bug。
改为显式按Shift+A验字符输入,并追加一项断言「输入框里按 ⇧A 也不唤出工具栏」
(Mac 上 ⇧A 是主键、必须被输入框挡住;Windows/Linux 上本不该触发)。 .verify-state.json不再写绝对路径:原来记path.resolve(target),绝对路径含本机账号名,
属不该落盘的个人信息(打包发布时本就排除该文件)。现在只记path.basename(target)。
该字段仅作审计留痕,指纹判定只读compHash,改动不影响逻辑。
复验:在 3 页面原型(设备管理 / 设备详情 / 折旧方案)上 L1 / L2 / L3 全档 FAIL 0 / WARN 0,退出码 0。
v1.1.0-mac2(2026-09-21 快捷键分平台)
- Mac 端快捷键 Alt+A → ⇧A:外接 Windows 布局键盘插到 Mac 后,⌥Option 落在贴着空格的 Alt 键上,
与内置键盘位置不同(内置 ⌥ 在 ⌘ 外侧),作者按习惯位置常按错。Shift 键在所有键盘上位置一致,
故 Mac 主用 ⇧A;Windows/Linux 无此歧义,沿用 Alt+A。三者由IS_MAC自动判定。 - ⇧A 的两个副作用已挡:① 焦点在 input/textarea/可编辑区时不吃快捷键(
inEditableField()),
否则原型里打不出大写字母 A —— 已用真实断言守住「输入框里 A 能正常输入」;② 编辑弹窗开着时不触发。 - Mac 保留 ⌥A 作备用键:macOS 自带中文拼音输入法用 Shift 切中/英,
preventDefault()拦得住字符、
拦不住输入法状态切换,此时 ⇧A 会抖输入法。⇧A 与 ⌥A 在 Mac 上都能唤出,任一可用。 - 工具栏显示当前快捷键:唤出后最左端有一枚灰底等宽小字(⇧A / Alt+A),作者不必记住。
- L2 新增 11 项快捷键常驻用例:双平台主/备用键可用性、toggle 开关、输入框不被抢键、
焦点恢复后可用、「L2 收尾把工具栏复原为隐藏态」——最后这条是实测踩出来的:
快捷键是 toggle,若上一段把工具栏留在显示态,下一段那次按下会变成「关掉」,L3 会连锁失败。
v1.1.0-mac(2026-09-21 本机实测后修复,均可重新投入使用)
跨平台 / 脚本层(scripts/verify.js):
- 硬编码 Windows 路径导致浏览器档静默失效:
createRequire('C:/Users/<某用户名>/...')在非开发机上必然失败,
且失败时只打印 SKIP、结论却仍写「L1+L2 通过」——最容易误判交付。
现改为多候选解析(环境变量PRD_PLAYWRIGHT_DIR→~/.workbuddy/binaries/node/workspace→ 常规 require),
并区分新退出码 3(想跑浏览器档但没跑起来),同时结论不再谎报通过。 - L2 角标数断言误报:把「当前页条数」直接与可见角标数比较,页面里只要有未打开弹窗的标注就恒失败
(这类标注角标本就不该渲染)。改为只比对该可见条数,并 INFO 提示「另有 N 条在隐藏容器/弹窗属正常」。 - L3 序号断言误报:断言「新建标注必须是本页第 1 个」,只在空页面成立;
迭代中的文件(页面已有标注)必然假 FAIL —— 而 L3 的用途恰恰是验收正在迭代的文件。
改为校验「落到本页且序号 = 本页总条数」,并用组件自己的buildView()口径计数(弹窗外元素靠 declared page 兜底归属)。
组件层(assets/annotation.html):
- Word 导出丢加粗/斜体/下划线:编辑器调
execCommand且开了styleWithCSS,
产出的是<span style="font-weight:bold">而非<b>;导出解析只认标签 → 加粗在 Word 里静默消失
(实测<w:rPr>为空)。已补 span 内联样式解析(含 font-weight / font-style / text-decoration / line-through / background-color / color)。
打包 / 发布注意:
- zip 用反斜杠做路径分隔符(Windows 打包):macOS 解压会报「appears to use backslashes as path separators」并可能写不出文件,
中文文件名还会乱码(未置 UTF-8 flag)。发布前建议用zip -X -r(macOS/Linux)或让打包工具用/分隔。
复验结论
修复后在同一份 7 页面 + 10 弹窗原型上:L1 / L2 / L3 全档 FAIL 0,
另跑 24 项独立测试(读者只读红线、快捷键唤出、⌘点击编辑、富文本、弹窗显隐、管理抽屉、Word 导出、导出 HTML 洁净度、回写、组件升级保数据)全部 PASS。本次又补 11 项快捷键用例(双平台主/备用键、toggle、输入框不被抢键、跨阶段状态不污染),随 L2/L3 常驻。
原型演进与组件升级(什么时候才需要重新注入)
一句话:改原型内容不用重新注入,只有升级组件本身才需要。
| 你做了什么 | 要重新注入吗 | 说明 |
|---|---|---|
| 改文案 / 改数据 / 改样式 / 改原型自己的 JS | 不用 | 标注住在数据区里,与原型内容完全独立。实测:注入后把正文改掉,2 条标注与角标都原样还在 |
| 新增 / 删除页面 | 不用 | 给新页面容器加 data-prd-page="页面名",下次打开组件自动识别(实测新增页被识别为第 4 个页面)。不加页面标记的话,新页面上的标注会被归到"无页面",序号与页面筛选会乱 |
| 改了被标注元素的 id / class / 结构 | 不用 | 但锚点选择器可能失配 → 控制台 [PRD] 未找到锚点: xxx,角标消失(不会错位到别的元素上)。手工改 anchor 即可,不用动组件 |
| 组件升级 / 组件修了 bug | 要 | 走下面的升级流程 |
| 文件里出现两份数据区 / 组件代码损坏 | 要 | 先备份,再按升级流程重做 |
⚠️ 组件修了 bug,不会自动同步到已经注入过的原型 —— 每个原型文件里都内嵌了一份组件代码,
不是运行时引用。新注入的文件自动带修复;老文件必须跑 inject.js --upgrade 才会换上新组件
(标注数据会保住)。判断某个原型带的是不是修复版:node scripts/verify.js <文件> 看版本与自检结果,
或直接搜组件源码里的特征函数(如修复跨页串页的 isUniqueFor / scopeOf)。
升级流程(保住标注,也保住你对原型的改动)
⚠️ 千万不要"用备份覆盖当前文件再注入"——那会连你后来改的原型内容一起冲掉。
组件代码与标注数据是两回事,升级只换前者。
直接跑 node scripts/inject.js <目标.html> --upgrade 即可(它做的正是下面 7 步)。
手工时照此执行:
- 备份当前文件
- 取出数据区 JSON:
id="prd-annotations"两个标签之间的内容,原样留着 - 按界标剥掉旧组件:
<!-- PRD-CSS-START -->…<!-- PRD-CSS-END -->、
<!-- PRD-LAYER-START -->…<!-- PRD-LAYER-END --> - 没有界标的老文件用特征定位:样式块找含
.prd-marker的那个<style>;
逻辑段从<script type="application/json" id="prd-annotations">起到组件主逻辑 script
(含window.PRDAnnotation的那个)的</script>结束。
⚠️ 别用indexOf('<style>')——那会命中原型自己的样式块,把原型样式删掉;
⚠️ 也别用整份文件的lastIndexOf('</script>')(v1.1.0 的旧做法)——组件之后若还有
原型自己的<script>(或原型字符串里的假</script>),会被一起剥掉,升级后原型功能静默丢失。
inject.js --upgrade已按此实现,手工时请勿倒退。 - 注入新版组件(样式 →
</head>前;数据区+逻辑 →</body>前),并补上界标 - 把第 2 步取出的 JSON 写回数据区
- 浏览器打开验证:条数一致、标题内容一致、无 JS 报错
实测:升级组件后 2 条标注完整保留,原型自己新增的页面也没被冲掉。
耗时实测(2026-09-21 本机,3 次中位数)
| 环节 | 自身耗时 | 端到端(含 node 启动 ~280ms) |
|---|---|---|
| 注入(读文件 + 字符串替换 + 写文件) | 28 ms | 298 ms |
| L1 静态验证 | 5 ms | 0.3 s |
| L2 浏览器巡检(打开 + DOM 断言) | — | 4.5 s |
| L3 完整模拟(逐页切换 + 真实点击) | — | 15 s |
| 常规一次「注入 + L1 + L2」 | — | 约 5 s |
| 组件改过后「注入 + L1 + L3」 | — | 约 15 s |
让人觉得久的是另外两件事,都不是注入本身:
- 发现 bug → 修 → 重跑的循环(今天大部分时间花在这);
- 我跑的完整模拟:新建标注 → 保存 → 切页 → 复制剪贴板 → 等 toast,几十秒。
分级策略:
- 只改标注数据(回写)→ L1 即可,不用开浏览器;
- 常规注入 / 原型改动 → L1 + L2,约 5 秒;
- 组件代码改过 → 必须 L3(verify.js 会以退出码 2 主动提醒,不用靠记忆);
- 首次给某个结构复杂的原型注入 → 建议直接 L3。
前期不稳定时,不要为了省时间停在 L1/L2 —— 那两档抓不到交互与跨页问题。
省时间的正确做法是让 L3 自动化(现在就是),而不是跳过它。
多页面 / 多级页面机制
- 标记页面:给每套页面的容器加
data-prd-page="页面名"(如菜单切换的内容区),
切换时各容器互斥显隐。组件自动把「当前可见的那个」认作当前页。
⚠️data-prd-page只加在页面容器上——侧边栏菜单项等导航元素请换个属性
(如data-prd-nav)。若把页面名写在菜单上,菜单会被当成页面容器:
既干扰「当前页」判定,也会在按属性做互斥显隐时把菜单一起隐藏掉。 - 多级页面(二级/三级):页面名用
父/子表达层级,例如
data-prd-page="设备管理/设备详情",并让二级容器嵌在一级容器内部;
进入二级页时隐藏一级页里的列表内容(由原型的切换脚本负责)。
此时两个容器同时可见,组件取层级最深(最具体)的那个作为当前页,
因此菜单高亮、页面级导出都能落在正确的二级页面上。 - 菜单高亮:建议切换脚本用前缀匹配(
name === nav || name.startsWith(nav + '/')),
这样停在二级页面时父级菜单保持高亮。 - 标注归属(决定序号与筛选):组件按「锚点元素落在哪个页面容器内」判定归属
(多级取最深容器,与角标跟着元素显隐的表现完全一致);锚点在容器外(页头、侧边栏
等公共区域)或锚点已失效时,退回该标注page字段(创建时的当前页)。
用「点击新建」建的标注会自动写入当前页。 - 序号每页独立:每个页面的角标都从 1 开始编号,不是整个文件 1..N;
管理抽屉的行号、角标数字、读者弹层标题、Word PRD 里的编号全部用同一套页面内序号。 - 工具栏计数与上一个/下一个:只在当前页面的标注里循环(如「本页 3 条」「2 / 3」),
不会跳到别的页面去。 - 管理抽屉默认只看当前页:抽屉顶部「显示」分段控件默认停在「本页面」,
可切到「全部页面」查看/整理整个文件(此时行内会附带页面名,便于区分)。 - 角标自然联动:不在当前页面的锚点元素变为不可见 → 其角标自动隐藏,切回来自动出现。
进入二级页面时,一级页面里的标注会随其容器一起隐藏。 - 页头/侧边栏等公共区域(不在任何
data-prd-page容器内)的标注,在所有页面都显示。 - 单页原型:没有任何
data-prd-page时不分页,全部标注算同一页,序号就是 1..N,
抽屉不显示范围切换,「导出本页面 PRD」与「导出文件 PRD」结果相同。 - 按位置重排(告别手排):按页面分组各自排序(页面块不会互相穿插),
组内不可见元素(未打开的弹窗、别的视图)排到本页末尾,不参与视觉排序。
Word PRD 导出
管理抽屉底部两个按钮,导出为真正的 .docx(组件内置手写 ZIP + OOXML,零依赖):
- 导出本页面 PRD:仅当前页面的标注,文件名
<原型名>-<页面名>-PRD.docx - 导出文件 PRD:全部标注,按页面分组,文件名
<原型名>-PRD.docx - 排版:一级标题 = 页面名称;二级标题 =
角标序号.角标标题;正文 = 该标注的需求说明。
序号与页面上看到的角标完全一致(页面内序号,每页从 1 开始),每次导出按当前数组顺序重算。 - 富文本映射:加粗/斜体/下划线/删除线保留;行内代码用等宽字体;链接显示为蓝色下划线;
无序列表加•前缀、有序列表加1. 2.前缀并缩进;引用块用 Quote 样式。
Word 的导航窗格可直接按标题层级跳转。
锚定与渲染机制
- 每条标注存一个
anchorCSS 选择器;角标渲染在独立固定图层(position fixed,
不挂在锚点元素下),每帧按锚点元素的视口位置重摆—— - 锚点元素移动/变宽变窄 → 角标自动跟随,始终贴其右上角(pos 可选 tr/tl/bl/br)
- 不受任何容器 overflow 裁剪(旧版锚定表头被表格滚动容器裁半截的问题已根治)
- 锚点元素 display:none(如未打开的弹窗)→ 角标自动隐藏,元素显示后自动出现
- 元素被删除或选择器失配 → 渲染层打印
[PRD] 未找到锚点: xxx并跳过该条,
不会错位到其他元素上,需人工确认新锚点 - 相邻角标自动避让:优先向元素内部横向错开(最多 52px),同元素内仍冲突才向上错开
- 多页面原型的选择器会自动带页面作用域(v1.1.0):各页结构常常雷同(都有
table > thead > th之类),
不带作用域的选择器会命中别的页面 → 标在 A 页、角标却归到 B 页(串页)。
组件生成后做双重唯一性校验(匹配数 = 1 且确实命中该元素),
不唯一就向上找最近的id或data-prd-page容器加前缀(祖先链最多扫 10 层);
仍不唯一才原样返回。手工写anchor时同样建议以页面容器 id 开头。 - 原型结构大改后:重跑一次注入(幂等,只重建角标层、不动原型内容),
必要时把失配条目的anchor更新为新结构的选择器
历史经验(旧版遗留文件排查时仍适用)
v1.1.0 之前旧版的角标是锚点元素的子节点、absolute 定位,会被带 overflow 的容器
(典型:表格滚动容器)裁掉上半截。若必须维护旧版遗留文件:把 anchor 指向被裁剪
元素内层的文字节点容器(没有包裹元素时先注入 span 再锚定)。
排查用 document.elementFromPoint 在角标矩形多点探测命中链,再对比
getBoundingClientRect 与各层祖先矩形,区分"溢出裁剪"还是"层级遮挡"。
红线(必须遵守)
- 读者模式零编辑入口:不按 Ctrl 点击时不出现任何编辑 UI;工具栏默认隐藏,
仅快捷键唤出(Mac ⇧A / Windows Alt+A),且编辑器打开、或焦点在输入框时不触发 - 角标无动效:角标(prd-marker)不加任何动画、脉冲、hover 缩放;弹层即点即显
(管理抽屉/工具栏的滑入过渡属界面外壳,不受此限) - 运行时节点必须全部带
data-prd-runtime="1",导出时从克隆文档中整体剥离——
否则序列化活 DOM 会把角标/工具栏固化进文件,重开一次叠一层(旧版已踩坑);
组件内置的「保存并导出」已按此实现,导出后的文件必须验证不含 prd-marker、
prd-tb、prd-mgr、prd-editor 等运行时节点 - 修改只在内存:交付前必须在工具栏点「保存并导出」。
localStorage 镜像只是「误关找回 / AI 读回」的中转,不是交付载体 - 读回优先走镜像:用户说「标好了,你读回来」时,先请他点工具栏「⧉ 复制全部JSON」(首选);
他不方便就按「从浏览器读回标注」读 localStorage 兜底;
只有镜像里确实没有该文件的数据(例如他标的是没装本组件的旧文件)才另想办法,别一上来就要求用户导出 - 组件代码改过之后必须跑
verify.js --full,不能停在静态检查;脚本会用组件指纹自动提醒(退出码 2) - 注入 / 升级 / 回写优先走
scripts/下的脚本:重复注入、误删原型样式、覆盖已预填内容、
忘记备份、写完不复核这些坑,脚本里都是硬拒绝或自动规避;手工只在脚本不可用时使用 - 改完文件必须复核:写盘后用
readFileSync断言关键字符串确实存在,别把"工具返回成功"当成已生效 - 回写必须先回显清单再落盘:把每条的「页面 / 序号 / 锚点 / 状态(新增·更新·预填·保留·删除)」
连同统计列给用户,确认后才写文件。禁止"拿到信封就闷头写" - 回写必须写前自动备份:
<原名>.bak-<时间戳>.html放同目录,备份成功才动原文件 - 回写只替换数据区:只允许改
<script type="application/json" id="prd-annotations">两个标签之间的
内容,文件其余字节一个都不动;不写id/number字段 - 回写完必须提醒刷新,且警告别点「⟲ 恢复缓存」:AI 改的是磁盘文件;
刷新后若出现「⟲ 恢复缓存(N)」,正确操作是「✕ 丢弃缓存」,点恢复会用旧缓存盖掉刚写回的内容 - 预填只填空角标:
title与body去标签后都为空才算空;已有内容的角标一字不动 - 预填不编造:原型里看不出来的接口名、表字段、枚举值一律写「待确认」
- 导出 HTML 只有一处入口:工具栏(管理抽屉里的旧导出按钮已移除,改为两个 Word 导出)
- 链接一律
target="_blank" rel="noopener noreferrer"并高亮可点 - 序号页面内连续:每个页面的角标都从 1 开始,删除/排序后页内自动 1..N,无需人工维护;
多页面原型里不要按"全文件序号"写文案(如"第 7 条"),编号只在页面内有意义 - 零外部依赖:不引入任何 CDN/外部库、不写任何外部文件,单文件可离线打开
- 标注内容只写需求说明,不写入敏感业务数据、内部系统地址、人名
排障速查
- 数据解析失败 → 控制台
[PRD] 标注数据解析失败 - 锚点失配 → 控制台
[PRD] 未找到锚点: <selector>(每条只报一次) - 角标跑到别的页面去了 → 锚点全局不唯一(多页面原型各页结构雷同)。组件已有作用域兜底;
仍错就把anchor手工改成以页面容器 id 开头(如#page-scheme > ...) - 注入后标注总是不生效 / 一直为空 → 文件里可能有两份数据区(重复注入导致),
getElementById只取第一个,新写的数据全部看不到。
注入前必须先查是否有id="prd-annotations",有则只改 JSON,绝不重复注入 - 读回时找不到目标文件的键 → 看
prd-annos::__last__的path,用户很可能标的是另一个文件(或那个文件没装本组件) - 打开页面后角标是旧内容 → 不是 bug:加载时绝不自动套用缓存(防止旧缓存盖住文件里刚更新的数据);
工具栏上会出现「⟲ 恢复缓存(N)」,点它才把缓存取回内存 - 误关页面丢了未固化的标注 → 同上,点「⟲ 恢复缓存」;若主键已被新编辑覆盖,从
prd-annos::__snapshot__::<路径>里找 - AI 回写后页面还是旧内容 → 正常:AI 改的是磁盘文件。刷新页面;
刷新后工具栏出现「⟲ 恢复缓存(N)」= 浏览器缓存是旧的 → 点「✕ 丢弃缓存」,不要点恢复 - 复制 JSON 没反应 → 组件会退化为弹出文本框让你手动全选复制(Ctrl/⌘+A 再 Ctrl/⌘+C);
也可能是 file 协议下剪贴板权限被拦,换手动复制即可,不影响数据本身 - 信封 file 字段对不上磁盘文件 → 用户可能标的是另一个副本;别猜,直接问他要路径
- 预填后角标变多/重复 → 检查是不是把运行时 DOM 序列化进了数据区,或组件被重复注入
(组件有运行时哨兵,重复注入会自动 return,多半是文件被手工复制了一份组件代码) inject.js报「自检失败,未写入文件」 → 这是保护生效(原文件一字未动)。
按它打印的失败项排查:数据区恰好 1 份 / 起止界标 / 样式段内含.prd-marker/ 逻辑段内含window.PRDAnnotation/ 文件应变大inject.js说「该文件已包含标注层」 → 不是报错。要换组件加--upgrade;
只想改标注内容就直接改数据区 JSON,或用writeback.js- 旧版数据兼容:
deleted为真的条目自动剔除;带数字id的按升序作初始顺序 - 需要在页面里程序化获取某元素的选择器时,可用
window.PRDAnnotation.getSelector(el)
(拾取模式内部同款逻辑);window.PRDAnnotation.items为当前内存数据(只读用途),
window.PRDAnnotation.persistNow()可手动触发一次镜像写入(调试用)