〖共创稿事节〗HarmonyOS 7 新特性实战(69):真机复验检索工具升级——四道预检把白跑挡在数据侧
# 〖共创稿事节〗HarmonyOS 7 新特性实战(69):真机复验检索工具升级——四道预检把白跑挡在数据侧
> 本篇是系列第 69 篇,对象是全书复验一直在用、却一直没转正的「翻页找串」工具。结论先说:四轮踩过的坑——导读抢先命中、停在篇末找不回头、预算 78 页小于目标 p135、非 BMP 字码点错位——这轮全部搬进工具的预检层,不碰设备就能回答「这一串能不能搜、在哪一页、预算够不够」,页位由应用真分页器推导,九条历史检索串「预估页 + 3 = 真机页」逐条对上。写作中还揪出一个新问题:跨郭璞夹注的检索串在设备文本属性里结构性不连续。收口后第 55 项门禁首跑通过、全量 55/55、HAP 构建 13.9 秒。
一、账本:四轮白跑,坑都出在工具层
先交代工具本身。全书复验的做法是:给一个古籍原文片段和一个页面预算,工具用 uitest 驱动设备翻页,逐页抓 dumpLayout 的文本属性找这串(uitest 官方文档把它归在命令行工具一节)。脚本一直躺在临时目录里,不入库、无校验,预算靠人拍脑袋。它踩过的坑按时间排:
| 轮次 | 坑 | 白跑的代价 |
|---|---|---|
| 第十轮(2026-10-13)坑 4 | 非 BMP 字在 UTF-16 下标空间占 2 格 | 对账管线下标错位,排查半天才定位 |
| 第十二轮(2026-10-14)教训① | 检索串被扉页导读抢先命中(犀渠、驕蟲都在导言里被点名) | 命中的不是目标页,以为内容丢了 |
| 第十二轮 教训② | 工具从当前页接着翻,停在篇末就永远找不回头部 | 必须手动回卷首再跑一遍 |
| 第十六轮(2026-10-16)B5 补验 | 页面预算 30+8+20+20=78 页,目标在 p135–157 | 4 串全 MISS,第二轮才对上 |
| 本轮(2026-11-10)新发现 | 跨郭璞夹注的检索串结构性命不中 | 历史复验记录里混进了「假命中」 |
这些坑的共同点是:全都发生在设备跑起来之后。而判断它们的材料——语料数据——本来就在仓库里。串在导读里出现过,数据知道;目标在哪一页,分页器知道;串里有没有孤立代理对,码点扫描知道。所以这轮的改造思路是一句话:把「能不能跑」的判断从设备挪回数据,设备循环只做数据侧放行之后的事。
二、页位不再猜:加载应用真分页器
最值钱的是页位。旧工具的预算靠人估,第十六轮 78 对 135 的全 MISS 就是这么来的。新工具的预检直接加载应用的真分页器 ShanhaiChapterPagination(与内容门禁 verify-source-chapters 同一加载口径),把目标串定位到分页结果的具体一页:
// scripts/lib/e2e-search-precheck.mjs(节选)
// 一页的两套口径,别混:
// nodes:设备 dumpLayout 可命中的最小文本单元(正文 Text 节点、译文、扉页字段)
// readingFlow:夹注外正文大字流 + 译文 —— 串「在内容里」即可定位页
function pageTexts(page, ch) {
if (page.kind === 'front') {
const nodes = frontNodes(ch);
return { nodes, readingFlow: nodes.map((n) => n.text).join('\u0001') };
}
const nodes = [];
const bodyRuns = [];
for (const para of page.paragraphs) {
if (para.length > 0) nodes.push({ kind: 'source', label: '正文', text: para.map((r) => r.text).join('') });
bodyRuns.push(para.filter((r) => !r.note).map((r) => r.text).join(''));
}
for (const t of page.translations) {
if (t.length > 0) nodes.push({ kind: 'translation', label: '译文', text: t });
}
return { nodes, readingFlow: [...bodyRuns, ...page.translations].join('\u0001') };
}
拿历史账本验算。第十六轮真机复验的 9 条检索串(B5 补验 4 条 + B6 新验 5 条),逐条跑预检,与当年截图上的页码对账:
| 检索串 | 预估页 | 预估 + 3 | 真机实测页 | 分类 |
|---|---|---|---|---|
| 赤理黃華青柎,名曰帝女之桑 | 128 | 131 | p131 | single-node |
| 俗謂之岣嶁山 | 129 | 132 | p132 | single-node(整串在夹注内) |
| 見則其邑有火,名曰𤝻即 | 130 | 133 | p133 | single-node(非 BMP 串) |
| 凡四十八山,三千七百三十二里 | 136 | 139 | p139 | single-node |
| 曰篇遇之山,無草木,多黃金 | 138 | 141 | p141 | 跨注拒跑(第四节) |
| 帝之二女居之 | 143 | 146 | p146 | single-node |
| 凡十五山,二千八百里 | 150 | 153 | p153 | single-node |
| 大凡百九十七山,二萬一千三百七十一里 | 152 | 155 | p155 | single-node |
| 是謂國用 | 154 | 157 | p157 | single-node |
九条串「预估页 + 3 = 真机页」逐条成立——真机版心与分页器默认版心有固定偏差,3 就是校准常数,钉进门禁。分页不是估算出来的运气,是应用自己跑出来的事实:旧预算 78 页在数据侧一眼就能判死,根本不用上设备。

三、唯一性闸:犀渠在数据里就是两处
第十二轮「导读抢先命中」的坑,根源是犀渠这个异兽在扉页导言里被点名,正文里又出现。跑一遍预检,数据侧直接复现:
$ node scripts/e2e-page-search.mjs --precheck "犀渠"
预检 [犀渠]
语料 2026.10.18-v29-shanhaijing-hainei · 模式 both · 章节 《山海经·中山经》
拒跑:《山海经·中山经》内有 2 处命中:p1(扉页·阅读导言)、p30(正文/译文)。
换更长的串,或确认就要第一处时加 --force-first。
两处命中、每处的页位和来源面都报出来。工具语义也就清楚了:设备翻页会停在第一个命中的页,所以多页命中时要么换更长的串,要么用 --force-first 显式接受「要的就是第一处」——放行必须留警告,不许静默。这个设计里有一条底线:预检只在数据侧拒绝,一旦放行,设备上发生什么与预检结论的偏差就是渲染层问题,不再混入「串本身歧义」的变量。

四、新发现:跨夹注串是结构性命不中的
写作中对账第九条历史串时抓到一个此前没人注意的问题。那段原文的 run 结构是:
正文段(一个 Text 节点,夹注是其中的 Span):
run[0] body 「《中次十二經》洞庭山之首,曰篇遇之山」
run[1] note 「或作肩」 ← 郭璞夹注,与 B6 复验同批入库(813145a)
run[2] body 「,無草木,多黃金。」
合并文本(设备 Text 节点的属性口径):
《中次十二經》洞庭山之首,曰篇遇之山或作肩,無草木,多黃金。
检索串: 曰篇遇之山,無草木,多黃金 → 不连续
夹注插在「篇遇之山」和「無草木」中间,检索串在合并文本里永远不连续。对照第十六轮的复验记录:九条串里唯独这条没有工具命中的佐证,页位是靠截图 07 在屏留证的——「9/9 HIT」的账本里混了一条「截图在屏」,当年没人发现。预检现在会直接拒掉这类串,并给出不跨注的改串建议:
$ node scripts/e2e-page-search.mjs --precheck "曰篇遇之山,無草木,多黃金"
拒跑:p138 的命中跨郭璞夹注 Span:正文与夹注同处一个 Text 节点,
合并文本里夹注插在中间,dumpLayout 属性文本不含完整串。
改用不跨注的片段:「無草木,多黃金」 或 「曰篇遇之山」
两条建议都是单节点连续,复检预估同在 p138。顺便把另一个坑也解释了:「俗謂之岣嶁山」整串躺在夹注 run 内部,单节点连续,所以当年 p132 能命中——夹注内可搜,跨夹注不可搜,这条边界如今成了工具的分类输出。

五、起点状态与码点:两道契约
剩下两坑是纯纪律问题,就用契约钉死。起点状态:设备循环必须显式声明 --from-start(回翻到卷首,上限 260 次)或 --resume(从当前页继续,自担漏检风险),不带起点语义直接退出——第十二轮停在篇末的教训变成了硬门槛。码点安全:检索串先过孤立代理对扫描,输出预览按码点截断(Node.js 的 execFileSync 撑起设备调用层,文本处理层不用它操心编码):
// scripts/e2e-page-search.mjs(节选)
if (!fromStart && !resume) {
console.error('起点状态必须显式声明:--from-start(翻回卷首)或 --resume(从当前页继续,自担漏检风险)。');
process.exit(2); // 第十二轮教训②:起点没约定,翻到篇末就永远 MISS
}
for (let i = 1; i <= budget; i += 1) {
dump();
const hit = texts().find((x) => x.includes(needle));
if (hit) {
// 码点安全:按码点截断预览,𤝻 这类非 BMP 字不会被劈成半个代理对
console.log(`HIT page ${i} :: ${codePointSafePreview(hit)}`);
process.exit(0);
}
swipeNext();
}
预检库里的码点扫描是个状态机,逐 UTF-16 码元配对高位代理与低位代理,配不上就拒收并报位置。第十轮踩的是对账管线的下标错位,这里是同一根源在工具层的防御:入口拒收孤立代理,出口按码点切字符串,两头都不给 UTF-16 下标空间留陷阱。
六、门禁与反向验证
工具转正不算完,规则要钉住。新增第 55 项门禁 verify-e2e-search-precheck.mjs,四组断言:
| 断言组 | 条数 | 钉什么 |
|---|---|---|
| 工具契约 | 5 | 设备循环前必须过预检;起点语义必须显式;预览码点安全;页位必须来自真分页器 |
| 历史对账 | 9 | 九条历史串逐条:8 条 single-node 且「预估 + 3 = 真机页」,1 条跨注拒跑且建议可复检 |
| 教训复现 | 3 | 犀渠 2 处拒跑 + force-first 放行留警告;预算 78 拒跑并报最低 151;孤立代理拒收 |
| 码点输出 | 2 | 𤝻 串放行;预览截断不劈代理对 |
首跑即 PASS。反向验证两组,都成立:把扉页扫描面清空(模拟旧工具的盲区),犀渠变成「唯一命中」,门禁当场报红 犀渠必须因多页命中拒跑 true !== false——这正是第十二轮白跑的复刻;把校准常数从 3 改成 2,九条对账全红 预估 128 + 2 ≠ 真机 131——页位表过期不再是没人知道的事。还原后复跑:单门禁 PASS、全量 55/55 PASS、HAP 构建 13.9 秒。
| 动作 | 结果 |
|---|---|
| verify-e2e-search-precheck 首跑 | PASS(21 条断言) |
| 反向验证:清空扉页扫描面 | 报红成立(唯一性盲区复刻第十二轮) |
| 反向验证:校准常数 3→2 | 报红成立(页位表过期立刻显形) |
| 全量门禁 | 55/55 PASS |
| HAP 构建 | BUILD SUCCESSFUL,13.9 s |

收个尾
如实记两条限制。其一,设备循环(hdc 抓取、翻页、回卷首)是旧工具已验证逻辑的移植加新契约,本轮没有在设备上重跑——模拟器磁盘映像被残留进程锁着,真机也不在场;但预检层离线可复跑,验收证据全部落在数据侧,这本来就是这次改造的用意。其二,附记页的异兽名在图鉴数据里,预检扫不到,检索串若是异兽名仍有被尾页卡片抢先命中的可能,工具注释里明写这条盲区。
下欠一笔新账:校准常数 3 是当前语料上的实测值,下次真机复验首跑时复核一遍,语料演进导致门禁报红时按门禁注释处置(重推页位、真机重校准、更新对账表)。工具从临时目录搬进 scripts 这件事拖了四轮——坑在工具层,账就该记在工具层,这轮把四条坑位和一条新发现一起钉住了。
证据清单
- 预检库与工具:
app-harmony/scripts/lib/e2e-search-precheck.mjs、app-harmony/scripts/e2e-page-search.mjs - 第 55 项门禁:
app-harmony/scripts/verify-e2e-search-precheck.mjs(21 条断言,首跑 PASS) - 历史对账留证:
docs/development/acceptance/2026-11-10-e2e-search-precheck/precheck-matrix.txt(九串页位对账) - CLI 预检留证:同目录
cli-quxi-refused.txt、cli-cross-note-refused.txt、cli-ok-p154.txt、cli-start-contract-refused.txt - 验收记录:同目录
ACCEPTANCE_e2e_search_precheck_2026-11-10.md(55/55 全绿、HAP 构建 13.9 秒、反向验证两组) - 十六轮真机复验原始留证:
docs/development/acceptance/2026-10-16-shanhaijing-zhongshan-b6/(13 张截图,篇遇串页位由截图 07 留证)
更多推荐



所有评论(0)