- Core 的双拼解码挪到 Layout:内置九套仍是编译期常量表,用户方案拼成同一个结构,走同一套解码;Layout::check 报写错的键与打不出来的音节(按全部两键组合实际解出哪些音节算) - 用户双拼方案放在 schemes/<id>/scheme.toml,写 type = "shuangpin",可拿内置方案当底子按键改;配置里认不出的 scheme 写法能当 id 就是 Scheme::Custom,找不到按全拼 - SchemeSlot 取代 CodeSchemeSlot,两条轴一起装,直接收 &Config;CLI 的 --shuangpin 可给方案目录,回放按 id 认出用户双拼 - 形码方案的 scheme.toml 改为必填,形码与双拼共用 author / version / description / license 元信息 - 设置里单用形码时出现「满码唯一自动上屏」「顶字」开关,存进 [code.<id>]、盖过 scheme.toml(随包方案只读,换码表也不丢),只改规则不重读码表 - Linux 还没接方案目录,设置里不列自定义方案(support 表登记待实现)
15 KiB
五笔(形码)支持方案
2026-09-15 定。第一期做 86 版,平台范围 Core + CLI + Windows,配置收敛到 [general] scheme 并把配置迁移一起做。
本文记录技术判断与分期;实现要点落地后同步 docs/notes/crate-notes.md。
一、为什么五笔不能按双拼的方式接
现有代码里「输入方案」的抽象是**「把按键解码成拼音串」,双拼与注音共用的是拼音这个底层,不是方案**这个抽象:
// crates/qingjian-core/src/engine/setup.rs:79
pub(super) fn decode(&self, keys: &str) -> Option<EngineDecoded> {
if self.zhuyin { Some(EngineDecoded::Zhuyin(crate::zhuyin::decode(keys))) }
else { self.shuangpin.map(|s| EngineDecoded::Shuangpin(s.decode(keys))) }
}
解出来的 EngineDecoded(engine/decoded.rs)提供 pinyin / tail / segmentation / marked / keys_for,
随后原样进 engine/query/mod.rs:79 之后的词级查找、排序、整句。
五笔在这一层就不成立:码串不是拼音,切不出音节,没有读音,没有整句。拼音耦合在三个层次上:
| 层 | 耦合点 | 五笔的情况 |
|---|---|---|
| 解析 | parser::segment(parser/mod.rs:54)+ SYLLABLES 表(parser/syllable.rs:7) |
码串无需切分,按音节切分本身就是错的 |
| 词库 | Dictionary 的键是空格分隔的拼音串,查询接口全部收 SyllablePattern(dictionary.rs:350-393) |
需要按「码」索引,前缀匹配而非音节位置匹配 |
| 排序 | ranking/scored.rs 的 full_last / coverage / abbreviated / 模糊音 penalty |
码等长时这些字段全部退化,词频主导 |
所以「在 shuangpin/ 旁边加个 wubi/ 目录」不成立。
二、方案:两条平行管线
把方案抽象提升一层,Engine 在 query_inner 顶部分派到两条平行管线,而不是让五笔伪装成拼音:
// engine/query/mod.rs:62 query_inner 的开头
if let Some(code) = self.code {
return Ok(self.query_code(keys, rest, start)); // 形码:码串 → 码表 → 候选
}
// 以下原有拼音管线不动
三条理由:
- 现成的先例。
query/mod.rs:103-129那个「第一个字母都切不动」的分支已经在返回segmentations: Vec::new()+tail: keys的Query,下游(Engine::annotate、输入日志、note_displayed、各壳渲染)本来就吃得下「无切分」的Query。五笔复用这个形状,不新造结果类型。 - 不该开的开关变成天然不存在,而不是散落一地的
if wubi。沿用现有套路 (engine/correcting.rs:9对双拼直接返回None、engine/setup.rs:70的modes()返回ModeKeys::LETTERLESS), 在一个集中判断里关掉:整句 / Viterbi、神经重排、模糊音、拼写纠错与词图敲错边、中英混输、简拼、 v / u / i 前缀快捷键(v与i在五笔里是字根键,必须让位)。 - 青简的产品特色全部自动保留。
Learner(词频 / 用户词 / 选择学习)、Translator+Glossary释义、 生词本VocabularyBook、CEFR / JLPT 等级、InputLog、UsageStats都按「上屏的词」工作,与输入方案无关。 五笔用户照样有候选旁的译文与生词统计——这是「Core 才是青简」换来的好处,也是这个功能的卖点。
数据层:静态全量码表,不做运行时取码
新增 CodeTable(crates/qingjian-dictionary/src/code_table.rs,与 Dictionary 同属「文本 → 键」的查表;
只有一个文件、没有同词干的兄弟,按约定不另开目录)。词目与 WordList 一样是排好序的 Vec,
查询靠前缀二分——比现在按音节位置逐级收窄简单。.qj 容器加 Kind::Code(crates/qingjian-format,阶段 2)。
词组取码规则(二字词 2+2、三字词 1+1+1、四字及以上 1+1+1+1)在生成数据时算好,运行时不算: 规则连带简码与识别码很绕,而静态码表够小也够全;码表没有的词靠用户词学习补,不做运行时推导。
一级 / 二级 / 三级简码不专门实现——它们就是「前缀查询 + 词频排序」的自然结果(g → 一,gg → 该前缀下词频最高的字)。
用户词
现有自动造词(连着选出的两个词合成用户词)按「词的字数 = 音节数」判断,形码没有音节,自然不触发
(try_auto_word / finish_buffer 里 chars != syllables.len() 直接返回),不必另加开关。
「同一个码下选过的词靠前」由现有的 Learner::record_choice 负责,形码不需要新的用户词表。
阶段 1 落定的三件事(2026-09-15)
- 候选是新的一种
CandidateKind::Code,不是塞进Chinese。它的syllables为空、上屏吃掉整段作用域 (commit里的whole_scope)。这样「没有音节」是类型里的事实,而不是靠空串蒙混;整段造词也随之不触发。 - 简码不用专门做。一级 / 二级 / 三级简码就是「编码打全的词排在同前缀的长编码词前面」,
由
CodeTable::lookup的exact位给出,与拼音侧「音节数与输入一致者优先」是同一个排序位。 - 码表第一期只读 TSV,不进
.qj容器(Kind::Code留到阶段 2 与pack一起做)。 段 1 的 CLI 入口是--wubi <码表.tsv>。
三、数据
| 来源 | 许可 | 结论 |
|---|---|---|
| rime-wubi86-jidian(极点 86 码表) | Apache-2.0 | 采用:宽松、词量最大 |
rime/rime-wubi 官方 wubi86.dict.yaml |
LGPL-3.0 | 备选:与本仓库 GPL-3.0-or-later 兼容,但要单独带 LICENSE |
| Gitee「大一统五笔」 | 未声明 | 不能用 |
词频不用码表自带的权重(那是码表顺序,不是语料词频)。用现有 assets/lexicon/dict.tsv 里
同一个词的语料词频交叉回填——换方案不会让同一个词的排序变奇怪,释义兜底与生词识别也对得上。
管线:tools/dict-convert 加 wubi 子命令读码表 → assets/wubi/wubi86.tsv(词\t码\t词频)
→ pack code → code-wubi86.qj。
qingjian-dictionary/src/import/rime.rs 已能把 Rime .dict.yaml 的 词\t拼音\t权重 转成青简 TSV,
五笔码表正好是这个格式(第二列是 ggll 这样的码),转换主体可以直接复用。
许可与署名(同步四处,见 docs/design/landscape.md:55):assets/wubi/LICENSE、
docs/design/landscape.md 的数据源表与随包数据清单、README 的 License 一节、偏好设置「关于」页。
四、配置与壳
现状 [general] shuangpin = "" + [general] zhuyin = false 是两个字段表达同一个维度,
再加一个并列字段就是三个字段打架。收敛成 [general] scheme:
[general]
scheme = "pinyin" # pinyin | xiaohe | ziranma | microsoft | sogou | zhuyin | wubi86
值就是现有 ShuangpinScheme::key()(shuangpin/scheme.rs:32)与 zhuyin 的统一。
旧键读取时兼容、写入时迁移,落地时把 docs/plan/todo.md 里挂着的「配置文件版本迁移」一起做掉。
| 壳 | 落点 |
|---|---|
| Core | engine/mod.rs 的 shuangpin 与 zhuyin 收敛为一个字段;engine/setup.rs:20-33 的 setter 合并;阶段 1 已加的 code: Option<CodeTable> 改为随 scheme 装载(哪个码表由配置决定,Core 不需要 CodeScheme 枚举) |
| 配置 | crates/qingjian-platform/src/config/general.rs:50-97 |
| CLI | apps/cli/src/args.rs:76 加 --wubi;apps/cli/src/main.rs:236-250 应用;回放 apps/cli/src/replay/mod.rs:95-116 认新 scheme |
| Windows 设置 | apps/windows/settings/src/panel/pages/general.rs:14-20 的 SHUANGPIN 列表扩成「输入方案」;panel/message.rs:12-13、panel/component.rs:41-44 |
| Windows Server | server/src/main.rs:188-189 启动应用;dispatch/reload/mod.rs:107-108 热加载;状态条 dispatch/status/mod.rs:93-99 用 scheme.label() |
| macOS | 本期不动。顺带记一笔:apps/macos 至今没有接注音(全目录无 set_zhuyin_mode 引用),mac 接方案时要把注音一起补上 |
中英切换仍然是布尔,不变(架构约定「输入方案是配置项,不是模式」)。
五、分期与验收
| 阶段 | 内容 | 估计 |
|---|---|---|
| 1 ✅ | Core CodeTable + query_code + Engine 分派 + CLI --wubi(2026-09-15 完成,233 + 21 个测试全绿,CLI 端到端验过) |
3–5 天 |
| 2 | dict-convert wubi + 词频回填 + pack code + 许可署名 |
1–2 天 |
└ dict-convert wubi 与词频回填已做(2026-09-15,读 Rime .dict.yaml,词频按词面从青简词库回填,4 个用例);剩下载码表、Kind::Code 打包与许可署名 |
||
| 3 | [general] scheme 收敛 + 配置迁移 + Windows 设置页与状态条 |
2–3 天 |
└ 已做(2026-09-16):Scheme 枚举与 [general] scheme;旧键 shuangpin / zhuyin 读取时推断(不自动改写文件);CLI、macOS 偏好设置与 Windows 设置页/Server/状态条都改成读写新键;Windows Server 按方案装载码表(用户目录优先、随包 assets/wubi/ 兜底,找不到只警告并按拼音跑)。打包与 macOS 接入也已做(2026-09-16 续):bundle.sh 拷进 Resources/wubi/、Windows 安装器拷进 {app}\assets\wubi\,两处署名(macOS / Windows 的「关于」页)都补了;macOS 的偏好设置那栏换成「输入方案」下拉(Scheme::ALL),apply_config 里一并设注音与码表——macOS 侧因此顺带把一直没接的大千注音也接上了。还差:macOS 与 Windows 的真机验证 |
||
└ 壳真带上码表时,署名要同步:bundle.sh 是按文件白名单拷数据的(assets/wubi/ 现在一个都没拷),偏好设置「关于」页的 ATTRIBUTIONS 也是照随包数据列的一份。现在两处都没加,因为 macOS 侧还没有输入方案,包里带它只是白占体积 |
||
| 4 | 释义 / 生词 / 统计 / 日志对齐验证、输入日志 scheme 进回放、docs/user/input/ 加页 |
1–2 天 |
└ 已做(2026-09-16):回放按每条日志的方案切换(SchemeSwitcher,形码要有 --wubi 的码表);释义 / 生词 / 词汇记录 / 输入统计在形码下照常,有测试钉住(code_commits_still_feed_translations_vocabulary_and_usage);docs/user/ 的按键表、拼写纠错、英文模式、偏好设置、模糊音与输入方案各页都补了五笔 |
合计约 1.5–2 周(单人,不含真机来回)。
验收沿用仓库规矩:改动先跑 apps/cli。--typing 看逐键延迟(形码每键应当比拼音更快,
没有整句与模糊音的开销);--replay 看既有拼音日志有没有退化(应当完全不变);
--eval-text 不适用于形码,跑一次确认没被误触发即可。
五之二、混输(2026-09-16 追加,原计划之外)
原计划里 [general] scheme 是单选的,选了五笔就完全不进拼音管线。实际用起来要的是
「双拼和五笔分开和组合」,所以拆成两条独立的轴:
scheme = "pinyin" # 拼音侧:pinyin / xiaohe / ziranma / microsoft / sogou / zhuyin / none(关)
wubi = "wubi86" # 形码侧:空为关
三种用法:双拼 alone(xiaohe + 空)、五笔 alone(none + wubi86)、混输(两边都开)。
Engine 上对应两个开关 set_code_table / set_phonetic,query_inner 按它们分派到
query_code / query_phonetic / query_mixed。
排序:编码打全的形码词在前
2026-09-18 定:编码打全的形码词 → 拼音候选 → 只命中编码前缀的形码词。一律形码在前的话,
真实数据下 kai 的首选是编码 kaik 的 中共党员、shi 是 椒,拼音那半边没法用;打全的编码是精确的,
留在最前(ga → 开、wo → 伙)。混输下模式键同双拼换成大写(V1+2),四码以内不做拼写纠错。
另外两条规则也是拍板的:
- 拼音读不出来时(
ggll切不成音节)整个按形码走,两边都空才算错。 - 五笔码最长 4 位,第 5 个字母起自然只剩拼音,不用特判。
五之三、自定义方案与顶字(2026-10-20 追加)
形码侧从「只有 86 五笔」扩成方案:Core 的 CodeScheme = 码表 + 两条按键规则(满码唯一自动上屏、顶字)+ 日志键,
平台层 code_schemes 负责发现(内置 wubi86 + 用户数据目录 schemes/<id>/,scheme.toml + 码表 TSV)。
配置收成 [general] code = "<id>"(off 为关),旧的 wubi = "wubi86" 读取时认、不再写出。
- 两条规则只在只用形码时生效:混输下满码之后的字母可能是在接着打拼音,自动上屏会替用户截断,顶字更不对。
- 顶字上屏的是壳里的高亮候选(用户可能挪过),所以 Core 只判断(
code_tops),动作在壳;满码唯一自动上屏没有高亮可挪,Core 直接上屏(commit_unique_code)。 - 满码的编码一个候选都没有时不顶:顶了只会把字母原样上屏,不如停着让用户退格。
- 标点顶字与空码清除同样只在只用形码时:标点总是先上屏高亮候选(主流五笔的缺省,拼音那边仍看
punct_commits); 空码时空格与标点只清编码,不把字母原样打进正文,回车仍原样上屏(要打字母时有路可走)。 - 满码长度缺省取码表里最常见的编码长度(
CodeTable::full_code_len),scheme.toml的code_length可覆盖;取最长的话一条写错的 5 码(极点码表的TF卡 ttfhh)就让顶字与满码上屏全部失效(2026-10-20 真机上踩到);编码只认字母,别的键(;,)留给标点与翻页,含它们的行加载时跳过。 - 内置五笔的两条规则缺省开;用户目录放
schemes/wubi86/可整个换掉内置那套(换码表或关规则)。 - 随包五笔本身也是一个方案目录(
assets/schemes/wubi86/),与用户方案同一套找法,代码里不再有内置特例。 - 配置热加载不重读没变的码表(
SchemeSlot比方案与码表文件的修改时间、长度)。 - 二进制码表(
.qj加形码类型)先不做:每键查询本来就在内存里,二进制只省加载与内存,等实测加载时间再定;分享用的打包格式另议。 - Linux 壳还没接形码,与之前一样。
六、不做的事
- 运行时取码推导(由单字码拼出词组码):规则太绕,静态表够用。
- 形码下的整句转换:五笔的整句没有意义,码长即词长。
- 临时拼音 / 反查(
z万能键):第一期不做,等有用户反馈再说。 - 98 版与新世纪版:不随包;用户自己把码表放进
schemes/就能用(见五之三)。