
"开口练"的核心能力------本地检测填充词、犹豫词、笼统词------建立在三个 JSON 词库上:实时词库(16 填充 + 14 犹豫 + 20 笼统)、情感词库(146 词)、分层候选词库(9 组 97 条)。这批数据有几个硬约束:
-
原版产品(JS 实现)已在用,行为必须 1:1,一个词都不能差;
-
离线可用是产品卖点,不能依赖网络下发;
-
词库会迭代,但迭代节奏和代码发布不同步,版本要能独立演进。
结论:打进 HAP 的 rawfile,配上严格的版本契约和加载校验。这篇讲这套机制。
1. 为什么是 rawfile
鸿蒙给静态数据几条路:写死在 ArkTS 常量里、放 resources/base/element/ 资源文件、放 resources/rawfile/、网络下发。逐个排除:
-
代码常量:数据与代码同编译,改一个词要全量构建;且几百个词条混在代码里,数据 diff 没法审。
-
element 资源:面向字符串/颜色/尺寸这类"会被系统按配置(语言、深浅色)解析"的资源,词库 JSON 不是这个语义。
-
网络下发:冷启动引入网络依赖,离线卖点直接破产。
-
rawfile :任意格式文件原样打进 HAP,运行时
ResourceManager按文件名读字节------正解。
文件布局,命名带项目统一的 sl_ 前缀:
entry/src/main/resources/rawfile/
├── sl_realtime_lexicon.json
├── sl_emotion_lexicon.json
└── sl_tiered_lexicon.json
注意路径是 resources/rawfile/ 而不是 resources/base/rawfile/------rawfile 不参与 base/dark 那套配置解析,直接放 resources 根下。
2. 三层版本号:schema、data、source 各管一件事
每个词库 JSON 的顶层是一个"信封",带三个版本号:
{
"schemaVersion": "1.0.0",
"dataVersion": "2026.07.14",
"sourceVersion": "expression-trainer@1.0.0",
"payload": { }
}
三个版本回答三个不同的问题:
| 版本 | 回答的问题 | 代码怎么处理 |
|---|---|---|
schemaVersion |
我的结构你还能读懂吗 | semver 校验 + 主版本兼容性检查,主版本不符直接拒绝加载 |
dataVersion |
这批数据是哪一版 | 透传进 Catalog,供日志/报告标注"本次分析基于哪版词库" |
sourceVersion |
数据从哪个上游移植来 | 溯源凭证,对着原版产品核对行为基线时用 |
schema 检查是硬门槛:
const SUPPORTED_SCHEMA_MAJOR = '1';
function assertSupportedMajor(version: string, file: string): void {
const major = version.split('.')[0];
if (major !== SUPPORTED_SCHEMA_MAJOR) {
throw lexErr(
SpeakLabLexiconErrorCode.UNSUPPORTED_SCHEMA_VERSION, file,
`不支持的 schemaVersion 主版本 "${major}",当前只支持 "${SUPPORTED_SCHEMA_MAJOR}"`
);
}
}
只检查主版本是刻意的:minor/patch 演进必须向后兼容(加字段不删字段),主版本变了才允许破坏性格式调整------届时旧版 App 拒绝加载新词库,而不是读错结构默默算出错误结果。数据格式契约和 API 契约是同一个道理。
3. Parser:零 I/O 的纯函数,校验严到"计较旧拼写"
解析层和 I/O 层严格分离。SpeakLabLexiconParser 文件头写着它的姿态:接受文本/字节;不含 I/O、UI、ASR 或 AI 依赖。失败时抛异常,绝不返回部分结果。
纯函数解析器的好处是测试可以脱离设备------Node 脚本拿同样的 JSON 喂同样的契约跑 oracle 对拍(B08 讲算法时细说),Hypium 里也能直接构造非法文本断言各种错误码。
校验强度远超"JSON.parse 不炸就行":
固定计数基线写进代码。词库词条数不是"读出来多少算多少",而是冻结的行为基线:
const REALTIME_FILLER_COUNT = 16;
const REALTIME_HEDGE_COUNT = 14;
const REALTIME_VAGUE_COUNT = 20;
const EMOTION_COUNT = 146;
const TIERED_GROUP_COUNT = 9;
const TIERED_TOTAL_COUNT = 97;
解析后逐组核对:分层词库 9 个分组的名字、顺序、每组条目数全部固定,连每条候选的词数(6 个)都是常量。少一个词、组序错了,都是加载失败。这批数字就是原版产品的行为基线,代码把它们变成运行时断言------词库错了不是"数据差一点",是构建事故,必须当场炸。
连历史拼写错误都显式处理 。原版数据里有个字段拼成了 vagueToPresice(正确应为 vagueToPrecise)。Parser 显式识别这个旧拼写并完成迁移,而不是把错误拼写传染进新代码:
/** 旧拼写(已显式迁移为 vagueToPrecise)。 */
const STALE_FIELD_SPELLING = 'vagueToPresice';
还有个 ArkTS 特有的小坑值得一提:ArkTS 不支持 in操作符 ,检测"JSON 对象有没有这个键"得用 Object.keys 遍历:
/** ArkTS 不支持 in 操作符;改用 Object.keys 检测键存在性。 */
function hasKey(obj: Record<string, Object>, key: string): boolean {
const keys = Object.keys(obj);
for (let i = 0; i < keys.length; i++) { if (keys[i] === key) return true; }
return false;
}
从 JS 移植数据解析代码时,这类语言差异点是最容易漏的。
4. Repository:I/O 收口与分层降级
I/O 层 SpeakLabLexiconRepository 只做两件事:读字节、定降级策略。
读取本身是 ResourceManager + TextDecoder 两行核心:
async function readRawfile(mgr: resourceManager.ResourceManager, name: string): Promise<string> {
try {
const bytes: Uint8Array = await mgr.getRawFileContent(name);
const decoder = new util.TextDecoder('utf-8');
return decoder.decodeToString(bytes);
} catch (e) {
throw new SpeakLabLexiconError(SpeakLabLexiconErrorCode.IO_ERROR, name, `...`);
}
}
真正的设计在降级策略(ADR-003 冻结):三个词库的可选性不一样,容错必须分层。
sl_realtime_lexicon.json 必需 → 失败 = 整体初始化失败,抛异常
sl_emotion_lexicon.json 辅助 → 失败 = EMOTION_DEGRADED,realtime 照常可用
sl_tiered_lexicon.json 辅助 → 失败 = TIERED_DEGRADED,realtime 照常可用
两者均失败 → AUXILIARY_DEGRADED
代码把策略写得很直白:
let emotionOk = false;
try {
emotion = parseEmotionCatalog(await readRawfile(mgr, FILE_EMOTION), FILE_EMOTION);
emotionOk = true;
} catch (_e) {
// 情感词库降级:realtime 保留
}
两个方向的红线都在这里:
-
不用空词库伪装成功。realtime 失败时宁可整体拒绝服务,也不能装一份空词库让分析"成功"地什么都检测不出来------那是拿错误结果冒充正确结果。
-
降级是显式状态,不是静默吞错。辅助词库失败会写进 Catalog 的 availability 字段,上层 UI 可以据此提示"情感建议暂不可用",日志里也有明确事件。静默 catch 和显式降级,区别是后者能被看见、被测试、被追责。
加载时机上,这套加载挂在 B03 讲的启动就绪闸门里:首屏渲染前 ensureSpeakLabShellCompositionReady 完成词库加载,就绪 promise 进程级去重、失败可重试。词库就绪后设置里的自定义词 overlay 才有附着点。
5. 小结
-
静态业务数据随包走 rawfile:
resources/rawfile/,命名带统一前缀,ResourceManager + TextDecoder 读取。 -
三层版本号各司其职:schema 管兼容性(主版本硬门槛)、data 管数据迭代、source 管移植溯源。
-
Parser 是零 I/O 纯函数:固定计数基线即行为断言,宁炸不糊弄;历史拼写显式迁移;注意 ArkTS 无
in操作符。 -
降级分层:必需数据失败=整体失败,辅助数据失败=显式 availability 降级;不用空数据伪装成功,不静默吞错。