987 Commits
Author SHA1 Message Date
Jia Sui 54c41d34b2 fix(engine): reset_cache 连带失效 caret 前缀候选缓存 2026-09-23 21:32:59 +08:00
fanlusky 7b7b19f449 fix(wubi): 前缀提示不挡混合拼音、候选去重,修复顶字/自动上屏后快打丢字母
- 只有精确码行才算五笔表已回答,前缀提示不再阻止混合拼音回退;拼音无结果时保留提示
- 五笔前缀查询按词去重,保留精确码行
- HideCandidate 抑制改为推送成功记账、Hide 消费(带 1s 时限),不再被下一个按键撤销;
  四码唯一自动上屏路径同样记账
2026-09-23 15:41:04 +08:00
AaGMix 902989abc4 feat(server): 显示光标输入状态提示 2026-09-22 23:52:28 +08:00
Jia Sui 4f26f35ac3 fix(server): 光标移回串尾时从引擎重建候选页
RefreshCandidatePageUi 只重排旧 candidate_ui.items、不重读引擎,而结算读的
正是 items。回串尾时引擎已整串重算,页面却停在旧前缀候选:选词只消费前缀,
ni'hao'ya 回串尾得「你好ya」而非「你好呀」。提取
ResolveCaretArrowCandidatePublish(前缀空收起;门控启用含回串尾一律
PrepareCandidateList 重读并复活窗口;未启用维持只刷页面,R7/AC8 零差异)。
2026-09-22 18:49:37 +08:00
Jia Sui fceb0692cb fix(server): 造词帧的 caret 字段只发给协商过组合恢复的客户端
未协商的旧版 DLL 会把第 2 个制表符后的整个尾部当 display_preedit,
追加的 caret 字段会污染一次 inline preedit。提取
ShouldCreateWordFrameCarryCaret 谓词(协商 && caret<剩余串),
未协商时帧输出与旧 Server 字节一致;CompositionRestored 帧本就只在
协商路径产生,无需改。
2026-09-22 16:39:50 +08:00
Jia Sui 096c0ccb10 feat(server): 前缀重算接线与仅前缀选词结算
编辑键后按门控(协商 composition-restore 且非 UILess、非英文/特殊模式)
把 caret 喂给会话做前缀重解;caret 回到串尾显式清 caret_,串尾行为
与现状一致。前缀选词复用 advance_composition_after_selection 的头部
消费语义:剩余 raw 即后缀、caret 归 0,NeedToCreateWord 帧带契约既有的
可选 caret 字段(协议零新增);学习/调频/AI 上下文走既有链路。
空前缀时收起 Server 自有候选窗并守卫假候选兜底与 ShowCandidate。
门控谓词与边界补真值表单测;回归测试钉住「移回串尾恢复整串解码」。
2026-09-22 16:09:05 +08:00
Jia Sui 1cfba58eba feat(engine): 光标驱动的拼音前缀解码与候选重算
caret 从纯显示状态升级为查询管线输入:set_caret 量化到最后一个完整
音节单元边界(只消费 segment_raw_boundaries,#187 契约),前缀候选
独立解码、preedit 保持全串拼音风格;纯移动光标同样重算候选。
IInputScheme::set_raw_input 提升为接口方法以支持不动现场组合的
独立前缀查询。runtime_isolation 两处按 R3/R4 新语义改写(caret 在
音节中间 floor 到空前缀、候选为空是预期状态);新增 caret 前缀
会话单测(engine/tests + server CI 目标,隔离词库夹具,覆盖
全拼 floor、双拼贪心配对、五笔无单元模型、boundary=0 空候选)。
2026-09-22 14:56:22 +08:00
Jia Sui c6632e8d85 feat(engine): 五笔前缀逐键提示与调频接线
- 码表查询从精确匹配改为前缀匹配:精确等长行优先(保住一级/二级简码),
  前缀行按权重降序跟随,LIMIT 50 截断(引擎层不读 server 配置的分层边界)
- WubiCandidateProvider 注入 RuntimePaths:接通 update_weight / delete_by_key
  到 user_dictionary journal;新增 bump_wubi_weight(同码组 MAX+1、主库更新
  与 journal upsert 同事务,行缺失不造行)
- create_word / cache_dynamic_candidate 补「为何 no-op」注释(后续任务范畴)
- 两个 toml 的 wubi_schema 注释与实现对齐:当前仅支持 wubi86
- server 测试:前缀语义、LIMIT 截断、上界构造、调频+journal 回放、删词墓碑
2026-09-22 09:23:25 +08:00
Jia Sui cdfad7c04c fix(server): 顶字推送后的 HideCandidateWnd 不再清除余码组合
顶字提交让 DLL 结束旧组合时,TSF 必然发来 HideCandidateWnd;HideCandidate
处理器照常 ClearState 会把服务端刚重建的余码组合(如「数据」顶字后剩下的
x)一并抹掉,用户后续按键从空组合开始组词,服务端与 TSF 缓冲从此错位。

- HideCandidate 处理器核对顶字余码标记(client+epoch+raw 非空),
  匹配则跳过 ClearState 并重新请求显示候选窗
- 顶字推送前设置标记;HandleImeKey 开头复位,防推送失败后标记滞留
- 新增三点丢键定位探针(reader 收包 / worker 派发 / 与 DLL keydown-sent
  按 request_id 配对)与 stale-dropped 诊断日志
2026-09-22 09:23:25 +08:00
Jia Sui 536d0a95ef feat(windows): 五笔四码后继续输入不丢字(顶字)
四码(不论是否唯一)之后继续敲字母时,先上屏该码的首选候选,再把这个字母
保留为下一次组合的开头。根因是两侧对「组合里有几个字母」认知不一致:TSF 的
本地按键缓冲跟着敲键涨(raw 预编辑样式下 preedit 就是它),服务端的五笔 raw
被引擎裁在四码;空格提交时组合文本被整段替换成候选并清掉本地缓冲,多敲的
字母随之丢失。

- 新增 worker 消息 CommitCandidateAndContinue(27),payload "<consumed>\t<text>":
  TSF 按服务端给出的消费数裁**自己**的组合缓冲,而不是套用服务端按旧视图算好
  的剩余串——快打时用户已多敲的字母因此不会被覆盖
- TSF 处理序列全部复用既有原语:_InsertTextToComposition + _HandleCompleteCommitFirst
  真提交,_StartComposition + AddVirtualKey + _HandleCompositionInputWorker(NO_REQUEST_ID)
  重建余下字母的组合;无组合时退化为直写
- 服务端顶字不看自动上屏开关:开关只决定唯一码要不要多敲一键才上屏,
  不决定丢不丢输入;自动上屏那条路也改用同一消息,顺带收掉快打竞态
- 引擎新增 wubi_four_code_is_complete(完整四码、码表作答、不要求唯一;
  空码不算——没有候选可提交时不触发顶字)

已知限制:光标在组合中间时的改码行为维持现状;空码继续敲字母仍会被忽略。
2026-09-22 09:23:25 +08:00
Jia Sui 5d1a9a6cc2 feat(server): 五笔四码唯一自动上屏
五笔敲满四码且码表只答一个候选时不再需要空格:第四个字母落下即上屏,下一个
字母直接开新组合。行为无条件生效,不设配置开关:业界默认开启(微软五笔、
搜狗五笔、极点、Rime 主流五笔方案),唯一候选没有第二选择,确认空格是纯冗余;
而一条开关要牵动配置五处同步,换不来可形成预期的行为差异,不值。

- engine 只报告事实(wubi_unique_four_code:完整四码、码表作答、候选恰好一个),
  是否上屏由 Server 决定;判定收在 input_key_policy.h 的纯函数里,随 portable
  目标进 ASan/UBSan;组词(creating_word)激活时组合保持打开,唯一四码不会把
  正在拼的词截断上屏
- 提交复用 ProcessSelectionKey,与空格上屏同一套记账;但只能走 worker 管道推送
  CommitCandidate:字母键在默认 raw 预编辑样式下不读请求-回复管道,回一帧 Normal
  既不会上屏,还会在 TSF 侧留下永不释放的回复帧,而 Server 已按 Normal 清掉组合
- 上屏前按当前组合同步重建候选页:候选页异步发布,提交路径读的 ui.items /
  page_words 否则可能还停在第三码那一拍
- 非唯一四码不做顶字上屏:一个按键只能回一条管道消息,真顶字需要扩 IPC 协议
2026-09-22 09:23:24 +08:00
fanlusky 99a8a355c0 fix(engine): 云/AI 联想候选上屏后按手动分词落库
选中云或 AI 联想候选上屏后,再次输入同样的拼音却不出现该词。根因有二:

- create_word_from_canonical_pinyin 已按 canonical 键里的撇号校验过分段,却又调用
  do_validate 二次校验;do_validate 会先去掉撇号、再用贪心 correction 重新断句,把
  qi'e'huan 切成 qie'huan(音节数 3→2),与字数不符而静默拒绝入库。移除这次多余且
  有害的二次校验,分段以调用方给定的 canonical 键为准(do_validate 本身保持不变,
  造词/调频/删除三条旧路径不受影响)。
- 上屏时 AI/云候选自身不带 canonical_pinyin,落库用的是去掉音节边界的
  committed_pinyin,同样会被重新贪心断句。改用会话里带撇号的 normalized_segmentation
  作 canonical 键,经 create_word_from_canonical_pinyin 落库。
2026-09-22 01:13:11 +08:00
Jia Sui 566ff8b832 feat(engine): 新增加加辅助码(拼音加加),第六套辅助码方案
按笔顺拆前两个部件、各取读音首字母(妈 = 女 n + 马 m = nm),部件用加加俗称读音(亻=d、
扌=t、纟=j、忄=s、攵=f、夂=z、刂=l、彳=s);138 个独体/部首字取「本字声母 + 起笔」。码表
7968 条,覆盖《通用规范汉字表》7846 字,比小鹤少 422 个扩展区生僻字——缺码会让含这些字的
词组拿不到辅助码,是已知取舍。

编码对齐拼音加加 5.x 安装包内的 fzm.bin(双拼键位已换算为声母):6487 字取到两码,6349 字
逐字一致,138 字保留本表规则,覆盖外的 800 字按同一规则重建。来源与非官方性质写在 NOTICE.md
与随包的 THIRD_PARTY_NOTICES.txt。

- engine/contracts/assets 注册 helpcode_jiajia,assets.h 重新生成(5 → 6)
- 设置页两个下拉显示名「加加」,两份配置模板注释同步
- 两侧测试补支持列表;README / README.en.md / server/AGENTS.md 改「六套」

默认值不变(双拼 lantian、全拼 ziranma)。
2026-09-20 22:00:34 +08:00
fanlusky 94df9a7917 fix(stats): 修复统计功能审查发现的 8 个问题
- Server 退出时唤醒统计管道监听线程,避免从未有客户端连接时 join 永久阻塞
- DLL 发送批次时对 ERROR_FILE_NOT_FOUND 重试,覆盖服务端重建管道的窗口
- 新增 StatisticsEnabledChanged(26) 推送开关,DLL 侧在采集入口直接短路:
  关闭时不读 range、不分类、不开管道
- stats_meta 的 schema_version 写入正确的键名(此前键与值都是 "1")
- dropped_count 仅在实际发送的批次中消费,空批次不再吞掉丢弃计数
- _CaptureCompositionStats 改为遍历 composition range 的克隆,避免移动宿主的活动区间
- FanyImeStatsEvent 显式声明 reserved 尾部,不再把未初始化的填充字节写入管道
- 透传统计去重标记在抑制一次后即消费,避免同一 tick 内的连击被漏计
2026-09-20 20:34:50 +08:00
Jia Sui ffcd1aee28 feat: 输入统计(DLL 采集、Server 聚合存储与设置页展示)
- windows: 三上屏出口 + OnTestKeyDown 直通字符采集,修复半角数字恒零
- engine: 统计 Web 消息契约(statsRequest/statsResponse)
- server: 统计管道接收、聚合、SQLite 存储、保留策略与每日自动清理
- ui-html: 设置页「统计」模块(卡片、热图、明细、开关、清理)
- docs: PRIVACY / README 改为内置可选统计
2026-09-20 19:08:16 +08:00
fanlusky 1ea01d5e0a fix(quanpin): 备选切分保护位只拉首页外的候选,不覆盖调频名次
保护位原来对任何排在 index 1 之后的备选切分都生效,于是调频刚把 吉安 排到
自然第 5 位,它就因为成了 ji'an 组里权重最高的词、过了 1/100 门槛而被重新
钉回第 2 位。用户选一次就跳到第二,之后再怎么调也只能停在第二——固定槽位把
调频写进权重的结果整个覆盖掉了。

改成只对自然排序已经掉出首页(默认 page_size 6)的备选切分生效。西安 在出货
词库里的自然位置是 16,仍旧提升到第 2 位;调频过的 吉安 留在它自己挣来的位置
上,之后按 promote 一步步往上走。

夹具同步补候选:engine 与 server 的 xian 夹具原来只有三条主候选,备选切分自然
就在首页内,测不到提升这条路径。
2026-09-19 22:42:29 +08:00
fanlusky e32eeadec9 fix(engine): 调频比较集跨键合并,按权重而非显示顺序取基准
打 jian 时列表里混着另一种切分 ji'an 的词。吉安 在出货词库里权重是 1,压在同组的
积案(9420)/几案(5999) 底下,而 adjust_candidate_ranking 按音节数圈比较集,于是
吉安 只跟 ji'an 这一组比,可学权重封顶一万出头,永远追不上 见 的 3460998——用户
选多少次,这个候选都纹丝不动。

音节数这个尺度是 #400 加的,为的是挡住另一个 bug:显示列表里备选切分词会被保护位
顶到最前面,中点数学却假定列表按权重降序,于是拿 西鄂(w=6) 当基准把 写 写成 506。
根子在「显示顺序 != 权重顺序」,按音节数滤掉跨表行只是绕开了症状,代价是把每个候选
关进自己的组里。

改成在比较集上直接按权重降序排一次,不变式在源头恢复:任何行都不可能坐在高于自身
权重的位置上充当基准,西鄂 那条路被堵死;同时比较集不再按音节数切开,吉安 的基准
取到整个列表最重的 见,置顶一次即越过,promote 模式连选五次一路走到首位。写入仍只
落在 entry_key 的行上,#36 的护栏不变。

test_user_dictionary_journal 新增两条回归(跨键置顶、连续 promote 收敛到首位),
#400 的两条断言原样保留并仍然通过。
2026-09-19 22:14:57 +08:00
fanlusky 663f72301d fix(server): 独立上屏的整句候选也落库
上一个提交改的是 InputSession::commit(),而 Windows 前端根本不走那条路:选词由
ProcessSelectionKey 自己驱动。那里 CandidateSource::Generated 走提前返回,落库的
闸门 ShouldStoreEarlyReturnPhrase 要求造词前缀非空,也就是只认"先选前缀、再用整句
收尾"这一种形状;用户直接选一条完整的整句时 creating_word 没激活,条件为假,什么
也不做就返回了。Google 解码器那条整句(Fallback)不在提前返回的名单里,走普通
路径,但同样没有行可以调频。

因此拆出 ShouldStoreStandaloneSentence:整句候选独立上屏时就该落库,带 7 音节
上限(对标 quanpin::WordLatticeOptions::max_phrase_syllables)。接在造词前缀
后面的那一支由用户一段段选出来,维持原样不设限。

顺带修掉同一处的读音拼接:前缀为空时不能带上那个分隔符,'na'yi'tiao 这种前导
撇号会让整条读音作废。

原先有一条测试把这个缺陷钉住了(断言没有前缀就不落库),按新的意图翻过来。
2026-09-19 13:48:26 +08:00
fanlusky ccbaa3a631 fix(engine): 送进 Google 解码器前把 ü 换成 nue/lue/ju 写法
小鹤双拼 ntdddswu 出的是「女蛾黛动物」而不是「虐待动物」:nt 转成全拼后
是 nve(shuangpin_pinyin_set 把 nue 抹掉了,词库与词格那一侧统一用 v 形),
而本地 googlepinyinime-rev 的拼写表和 inputtools 云输入都不认 nve/lve/jv,
只有 nue/lue/ju/jue,也不做 v→ü 的转写。带分隔符的串照样送过去了,解码器
却把 nve 重新切成 nv + e,于是有了「女蛾」。

加一个 quanpin::to_google_spelling,按音节整体改写 nve→nue、lve→lue、
jv→ju 这一类拼写,只在两个 Google 解码器的边界上用:云输入的 query_text、
双拼与全拼的整句回退。词库查询和词格用的仍然是 canonical_syllable 那一侧的
v 形写法,没有动。nv'er、lv'se 这些 Google 也认的写法原样通过。
2026-09-19 12:29:01 +08:00
Lu Fan(SonnyCalcr) e1d53dd8f0 Merge pull request #426 from ouchihao/codex/fix-299-double-helpcode-cache
fix(engine): isolate ordered double-helpcode query caches
2026-09-18 21:34:50 +08:00
Ouchihao 0925937bfe fix(engine): isolate ordered double-helpcode query caches 2026-09-18 20:43:17 +08:00
Ouchihao 5e641fb5a0 fix(engine): accept yo as a complete shuangpin syllable 2026-09-18 20:35:14 +08:00
Lu Fan(SonnyCalcr) 4dea1cd12f Merge pull request #420 from metasequoiaime/feat/ctrl-arrows-segment-caret
feat(input): 组词内按分词的光标与删除(#35 / #187 / #347)
2026-09-18 12:35:52 +08:00
Jia Sui 809f700998 test(server): 覆盖微软双拼 ';' 尾键的分段边界
- nihkb;(你好病 = ni|hk|b;)断言 {0,2,4,6}:';' 归属它开始的音节
- nihcb; 断言 {0,2,3,5,6}:贪心最长配对是契约,'cb' 是有效微软编码
  (cou),按段删除跟随 preedit 显示的同一条切分

Refs #187
2026-09-18 08:59:19 +08:00
Jia Sui 2576986881 feat(windows): Ctrl+左右键按分词移动光标
组词中 Ctrl+Left/Right 按一个字的拼音(音节段)移动光标,
与 Ctrl+Backspace 的删除粒度共用同一套引擎分段边界。

- 闸门归纳为 SegmentEditFunction(code, modifiers):Ctrl-only + 组词存在,
  VK_BACK/LEFT/RIGHT 分别映射到 segment 函数;三处判据与延迟投影共用
- 新增 FUNCTION_MOVE_LEFT_SEGMENT / FUNCTION_MOVE_RIGHT_SEGMENT,
  编辑会话只按 CompositionRestored 的 caret 移动本地光标,不重建 buffer
- Server 用 segment_raw_boundaries() 取前/后边界,能力位与非 UiLess 时回包;
  非拼音/本地模式/UiLess/旧端仍按单字符 ±1 回退

Refs #187
2026-09-17 23:36:34 +08:00
Jia Sui b1ec3ab1e0 feat(server): Ctrl+Backspace 按分词删除前置拼音(#187)
组词中 Ctrl+Backspace 删除光标前的一个分词单元:剩余拼音非空时删
最后一个字的拼音(音节段),剩余拼音为空时删除最后一段已选分词。

engine:
- InputSession::segment_raw_boundaries() 作为按段删除/移动的唯一分段来源:
  全拼把纠错显示串的分隔符映回 raw 下标,双拼与 pinyin_segmentation 共用贪心谓词,
  2 字辅助码自成一段;非拼音方案与本地模式返回空表示无单位模型
server:
- IsSegmentBackspaceKey(Ctrl-only)、整段丢弃 / 边界截断 / 单字符回退三分支,
  截断点残留分隔符清理
- 复用 CompositionRestored 回包(含 caret),仅协商能力位且非 UiLess 时发送;
  新出现「只剩已选汉字、无拼音」的中间态
windows:
- 两处 Ctrl 闸门对组词中的 Ctrl+VK_BACK 放行,新增 FUNCTION_BACKSPACE_SEGMENT
  与 _HandleCompositionBackspaceSegment(读回包重建,失败回退单字符)
- 空 raw 状态渲染、终态清理 word_for_creating_word,延迟投影失效处理

Closes #187
2026-09-17 23:01:31 +08:00
fanlusky bc37ac9a9e feat(installer): 安装时可自定义数据目录位置
词库、用户配置和皮肤此前固定在 %LOCALAPPDATA%\metasequoiaime,整包几百 MB
全压在 C 盘。安装向导新增一页,允许把这部分数据放到任意盘。

程序本体仍固定装在 Program Files:MetasequoiaImeServer.exe 带 uiAccess=true,
只有在受信任目录里这个标志才生效。

运行期的权威来源是 HKLM\Software\Metasequoia\MetasequoiaIME 的 DataDir。
Server、TSF DLL 和引擎三个组件各自按同一顺序解析:METASEQUOIA_IME_DATA_DIR
环境变量 -> 该注册表值 -> %LOCALAPPDATA%\metasequoiaime。三处各持一份实现,
因为组件边界规则禁止 windows/ 与 server/ 互相 include。32 位 TSF DLL 用
KEY_WOW64_64KEY 读,避开 WOW64 重定向。

安全相关的两个约束:

- 安装器在数据目录里写 .metasequoiaime-data 标记。覆盖安装的清理和卸载的整目录
  删除只在看到这个标记(或目录就是历史默认位置)时才执行——用户可能把数据目录
  指到一个本来就有自己文件的文件夹。
- 选择页与 /DATADIR= 走同一套校验:拒绝相对路径、盘符根目录、不存在的盘、程序
  目录内部,以及 Windows/Program Files/各 AppData/用户主目录本身。

升级时改了位置,PrepareToInstall 在停掉输入法进程后用 robocopy /MOVE 搬运用户
拥有的数据(msime_user.db 及其 WAL/SHM/journal、config.toml、config.base.toml、
skins\),词库和前端资源由本次安装重新写入。跨卷移动目录 MoveFile 会失败,所以用
robocopy。用户词库搬运失败会中止安装:否则新版本对着空的 msime_user.db 启动,
ssPostInstall 的回放只记一句 skipped,用户的自造词就静悄悄没了。

测试侧新增 METASEQUOIA_IME_CONFIG_DIR,只改 config.toml 的位置而不搬词库。原先
靠重定向 LOCALAPPDATA 做隔离的办法在装了正式版的机器上已不再成立——注册表值
的优先级高于它。
2026-09-17 22:49:19 +08:00
fanlusky a898c3b11c feat(candidate): Ctrl+Enter 上屏候选右侧的译文
候选框开着且高亮候选带译文时,Ctrl+Enter 上屏那条译文而不是候选本身。
译文只有一条就直接上屏;有多条时把候选框整个换成这几条译义,空格/数字键
照常选一条上屏。

TSF 端原先在分类器最前面把所有 Ctrl 组合都还给应用,Ctrl+Enter 到不了
Server。现在在那条拦截之前先判一次:候选框开着才认,否则组合仍归应用。
认下来的键走既有的 FUNCTION_SERVER_CANDIDATE_KEY 通道,由 Server 决定
是上屏(CommitExactText)还是只开一个新候选页(NavigationIgnored)。

译文页复用同一份 candidate_ui.items,因此云/AI/英文/emoji/颜文字这些异步
回调和候选扩展都要在这个子模式下让路,否则会把译文冲掉。输入串全程没动,
退出时把存下来的 items 和页码原样放回去即可。译文页上的空格/数字键只回
Normal / OutofRange——SELECT_BY_NUMBER 的 TSF 路径只认这两种。
2026-09-17 20:21:56 +08:00
Jia Sui e64195731e feat(server): Backspace 删空剩余拼音后退选已选分词(#35)
已选分词是造词中间态:删空剩余拼音时不再整条 composition 丢弃,
而是退选最后一段并恢复其原始拼音与候选。

engine:
- SelectionTransition 暴露本次选择消费掉的原始输入(保留大小写与双拼按键)
engine/contracts:
- 新增回复类型 CompositionRestored=14 与能力位 CompositionRestore;
  MaxKnown 同步抬高,Server 按协商能力位门控发送,旧 DLL 不会收到未知帧
server:
- 造词中间态压 (consumed_raw, previous_creating_word) 快照,边界 Backspace 弹栈退选
- 任何清空 raw 的路径同时清 creating_word 与快照历史,避免跨 composition 泄漏
windows:
- _HandleCompositionBackspace 提示退选时读回包重建 keystroke buffer / word / 预编辑 / 光标
- NeedToCreateWord 与 CompositionRestored 共用载荷解析;旧 Server 与 UiLess 回退旧行为

Closes #35
2026-09-17 18:45:01 +08:00
Jia Sui d4a0796400 feat(windows): 重构智能标点
原「智能标点」是小狼毫 digit_separators 的语义:字母或数字后的 , . : 直出英文,
2 秒内再按同一标点转回中文——后半段靠 SendInput 往应用里补一个 Backspace 再打
字符,落点全靠光标当时在哪。这次换掉主路径:输入中文标点后按空格,用 TSF 编辑
会话就地把它改写成对应英文(。,!?;:、 → . , ! ? ; : /),2 秒内再按同一标点
键撤回中文;原来的直出降为子开关,整个家族默认全关、改为显式开启。

- 按键与会话:转换/撤回是本地文档改写,不进 IPC、不进延迟队列;判据落在
  _IsKeyEaten,OnTestKeyDown 的预判与 _DispatchKeyDown 的第二遍预判共用同一份,
  声称吃掉的键不交还宿主。改写走编辑会话,请求不上时另起一个最小会话把键本身
  的字符写回去,避免已吃掉的键静默丢失。
- 状态:原来散落的九个成员收成一个 SmartPunctuationAction(None / ChineseCommitted
  / AsciiConverted),带焦点令牌、前台窗口和 beforeChar 指纹;清理以分类结果为准,
  丢焦点与上下文栈变化一并清掉。
- 转换范围:。,!?;:、 与单独出现的 “”‘’【】《》() 都是映射表里的单字符
  条目,路径完全一致;成对补全产生的一对(autoClosedPair)不 arm,按空格只落空格,
  不会留下 <》 这类孤儿。
- 开关:总开关 + 中文标点后按空格转换 + 同键转回中文 + 数字后直出 + 字母后直出,
  五键全部默认关;直出拆成数字/字母两个独立子开关(IPC 24 收窄为数字、25 追加为
  字母),TSF 按前字符类型分别查键。
- 命名:子开关「独立符号」改名「中文标点后按空格转换」,配置键
  smart_punctuation_standalone → smart_punctuation_space_convert,IPC 22 只改常量名
  (SmartPunctuationSpaceConvertChanged),编号与语义不变、新旧测试包互通;设置页
  文案同步重写,五处配置链路与死宿主一并跟改。
- 兼容:旧键未出厂、不迁移(升级合并按模板缺键丢弃、取默认 false);编号 23 随
  「按空格整体转换一对」的设计退役并留空不复用。
- 验证:TSF x86/x64 与 Server Release 编译、Server 335 单测、windows_ipc_contract
  x64/x86、设置页 build + 27 测试、clang-format 18.1.8 全绿。真机未跑。

Signed-off-by: Jia Sui <jsfaint@gmail.com>
2026-09-17 09:29:57 +08:00
fanlusky ef7840a4c6 fix(voice): 语音条水平按整屏居中,任意任务栏边缘都居中偏底部
上一版改用工作区(rcWork)定位后,任务栏在左/右边时工作区被挤窄,
水平中心会相对屏幕中心偏移,语音条不再位于屏幕正中。

明确目标行为:任何情况下都让语音条“屏幕水平正中、偏底部”。
 - 水平改回按整屏矩形(rcMonitor)居中,不受左/右任务栏影响;
 - 垂直仍贴工作区(rcWork)底部,自动避开底部任务栏。

把 GetMonitorWorkArea() 换成一次解析同一显示器、同时返回整屏与工作区的
GetForegroundMonitorMetrics(),供上述两个维度共用,避免两次解析落到不同
显示器。
2026-09-16 16:12:24 +08:00
fanlusky 7e493b6a6d fix(voice): 语音条按工作区定位,兼容任务栏在任意边缘
update_window_bounds() 此前用"整屏矩形 - 底部任务栏高度"来定位语音条,
隐含假设任务栏在底部。任务栏放到左/右/上边时,语音条会算偏(位置错误或
与任务栏重叠)。

改为使用显示器工作区(MONITORINFO.rcWork)——它已扣除任意边缘的任务栏/
应用栏,水平居中于工作区、贴其底部上方 10px 放置。任务栏在底部时行为不变
(rcWork.bottom == monitor.bottom - taskbar_height)。

顺带把只服务于旧定位方式的 GetTaskbarHeight() 与返回整屏矩形的
GetMonitorCoordinates() 替换为 GetMonitorWorkArea()(二者原本仅此一处使用),
并移除不再需要的 <shellapi.h>。
2026-09-16 16:07:37 +08:00
fanlusky 510f70c1a5 fix(voice): 语音条适配分辨率/DPI 切换——与候选框一致
语音提示条(WaveOverlay)在切换屏幕分辨率/DPI 后尺寸与缩放错乱,
候选框已有的适配逻辑此前未覆盖到语音这一块。根因有二:

1. update_dpi_scale() 用 GetDpiForWindow(hwnd_) 取缩放,而定位用的是前台
   窗口所在显示器(GetMonitorCoordinates),两者可能不同源,且切换分辨率
   时 GetDpiForWindow 可能返回陈旧值。改用 GetDpiForMonitor(MDT_EFFECTIVE_DPI)
   按目标显示器计算,与候选框 ScaleFromMonitor 同一套技术,并让定位矩形与
   缩放同源。
2. ID2D1HwndRenderTarget 的 DPI 只在创建时定过一次,切换后不更新,导致
   draw() 里逻辑坐标映射到错误像素尺寸。现在在 update_dpi_scale() 中同步
   刷新渲染目标 DPI(等价于候选框 SetDpiOverride/SetDpi)。

同时把 WM_DISPLAYCHANGE 从 WM_PAINT 分支拆出单独处理,并让
update_window_bounds() 每次重新定位都刷新缩放,对齐候选框"每次显示都
重算"的行为。
2026-09-16 15:49:19 +08:00
Jia Sui b149a500b0 fix(server): 按数字选词偶发选错——以屏幕上的候选列表为准
按数字 2 想选第 2 个词,有时上屏的却是第 1 个:后台按手里的最新候选列表
数序号结算,而用户看到的是窗口还没重画完的旧画面;调频(一次置顶)每次
选完都改顺序,两者的时间差就会撞出「看 2 选 1」。关掉调频或固定候选位置
后顺序不再变化,问题消失——与报障观察一致。

修法:候选列表带版本号,窗口画完后回报画到第几版;按数字或空格时如果
窗口还没画到最新,后台最多等 30 毫秒再按用户看到的那一版结算。等待上限
是硬约束:必须留在 TSF 50ms 回复窗口内,超时会触发管道重连与按键重放。
窗口不可见、宿主自绘(UILess)、鼠标点击不等待。

- 发布候选页时递增版本号,快照携带该值;
- D2D 绘制完成、WebView2 DOM 回调(含 dedup 分支)回写实际渲染快照的版本号,
  异步回调捕获快照值而非回读实时值;
- 单测覆盖等待判定与 30ms 常量;阴性验证:等待失效时「落后 → 等待」用例
  失败,恢复后 333 项测试全绿;
- 真机验证(调频 pin + 快速同拼音 + 连按数字)待跟进。
2026-09-15 11:02:39 +08:00
fanlusky 5a5dea6b7e feat(japanese): 日语模式禁用 -/= 翻页并用 - 打长音符
日语模式下把 - 让给长音符(ー)输入,同时禁掉 -/= 的候选翻页:

- 罗马字方案接受 VK_OEM_MINUS,set_raw_input 不再把 - 过滤掉;
  罗马字表里本来就有 - 到 ー 的条目,只缺按键准入。
- n 后面接 - 时按辅音处理,让前面的 n 收成 ん,避免 n- 卡在 pending。
- 空编码时单独按 - 也起头组合,候选框第一项是 ー、第二项是普通 -。
- 组词流程同样适用:判据只看日语模式加组合状态,不再走翻页和词转字。
- -/= 在日语模式下不翻页,_ = + 这些没有假名写法的字符退回标点上屏,
  并把 = _ 补进服务端的标点集合,和 TSF 侧保持一致,否则会等不到回包。

TSF 侧的影子分类、IsVirtualKeyNeed(ForFreshComposition) 和服务端按键策略
三处同时改,统一以配置项 input_mode == japanese 为判据(R 模式不在范围内,
TSF 侧看不到临时会话,用会话类型判断会让两侧分类打架)。
2026-09-15 01:57:50 +08:00
fanlusky 9e2aedc915 chore(settings): 调整设置启动页提示文案与字号
将启动动画页的副标题提示由「首次启动可能需要稍等片刻」改为
「冷启动可能需要稍等片刻」,并将该行字号由 13.0 DIP 加大到 14.0 DIP。
2026-09-15 00:27:58 +08:00
Jia Sui 9bbd86b633 fix(engine): 调频权重基准限定同音节尺度
置顶/提升把一个候选的权重算成「显示列表首位权重 + 500」。显示列表会混入
备选切分词和不同切分的候选,它们的权重是另一套尺度(单字表是语料计数、
多字表是词组词权),于是基准取错就把候选写成垃圾值:用户机器上 xi'e 的西鄂
(w=6) 坐在 xie 列表首位时,置顶写会写出 506(= 6+500),低于些/血/谢,反而
被埋到三十名开外;将也以同样方式被写成 24240(= 激昂 23740+500)。

- 候选的 rank 与权重基准改在同音节尺度内计算:同音节数的表互相可比(九宫格
  数字上下混多个单字表、简拼上下文混多个 key 都保留原语义),跨音节数不再
  互相充当中点基准。选中词已是同尺度首位时不再写权重,避免被跨尺度反降。
- 写入仍按 entry_key 走,rebalance 不会落到别的 key 的行上(#36 的护栏不变)。

test_user_dictionary_journal 增加回归:混表列表里置顶写必须以些为基准,
且不再把些本身写成 506。
2026-09-14 20:14:28 +08:00
Jia Sui 809c62995b fix(engine): 全拼模糊音下备选切分抢位
模糊音开启时 QuanpinDictionary::query 对整个候选列表按 pinyin 字符串长度降序
排序(本意是压住模糊/前缀候选)。备选切分的 canonical 键带撇号且音节更多
(xi'e / ji'ang / you'di'an),字符串比主切分键(xie / jiang / you'dian)更长,
于是权重只有 6 的西鄂、23740 的激昂从保护位第 2 跳到第 1,顶掉 xie/jiang 的
首候选;xie 连模糊变体都没有,仅因排序无条件执行就被波及。

- 排序键改为候选覆盖的输入字母数(去掉撇号),与用户实际敲的长度对齐:备选
  切分键与主键同层交给词频,模糊/前缀候选仍排在整串匹配之后。
- 备选切分保护位加词频门槛:备选最佳词权重达到主切分首位 1/100 以上才提升。
  真歧义保留(xian -> 西安 55K 对 先 1.66M),罕见重码被拒(xie -> 西鄂 6、
  jiang -> 激昂 23.7K、youdian -> 尤迪安 7)。

test_fuzzy_pinyin 补齐四个场景回归断言(夹具权重取自出货词库);
test_quanpin_scheme 的保护位用例移到新契约:夹具备选词权重 1 -> 100,
让它留在门槛内继续覆盖「真歧义保留保护位」。
2026-09-14 20:14:28 +08:00
Evan 30a22e6f3d feat(ipc): reserve dedicated voice control pipe 2026-09-14 19:04:20 +08:00
Evan b10b8184d3 fix(ipc): reject malformed voice control fields (#398) 2026-09-14 10:40:29 +00:00
Evan 6b33e678fb feat(ipc): dispatch validated voice lifecycle commands 2026-09-14 10:32:24 +00:00
Evan 497b46e067 feat(ipc): validate voice control leases
* feat(ipc): validate voice control leases

* style(ipc): format voice control dispatcher

* fix(ipc): use local engine contract path
2026-09-14 18:21:49 +08:00
Evan e31964ce4f feat(voice): expose serialized lifecycle controls 2026-09-14 10:01:18 +00:00
Jia Sui 3dea733f64 fix(ui,server): 候选窗 D2D 阴影按皮肤对齐 webview——Card 支持 pass 列表
D2D 候选窗的 Card 阴影是内置 Win11 漫射三层(σ11/6/2.8,α0.42/0.30/0.48):
四边对称、无偏移,紧贴边缘合成 α≈0.49、暗晕铺到 15~20 DIP。webview 端候选窗
皮肤 CSS 用的是带右下偏移的双层软阴影(light: 8px 10px 24px rgba(0,0,0,.18)
+ 2px 3px 8px rgba(0,0,0,.10))。用户实机截图剖面实测:webview 底边峰值 α≈0.16、
D2D 0.41,差 2.5 倍,即「D2D 阴影偏重」的根因。对称三层无论怎么调参都拟合不了
带偏移的衰减曲线;浮动工具栏已有的 SetShadowScale 只缩模糊半径压不了深度
(高斯在边缘恒贡献 α/2)。

给 Card 拆出数据驱动的阴影 API,阴影参数跟皮肤走(与颜色/圆角同模式):

- ui/include/msimeui/Layout.h、ui/src/Layout.cpp:ShadowPass{σ,α,offset} +
  SetShadowPasses(空列表保持内置 Mist 三层,逐位不变)、SetShadowEnabled
  (整体开关,语义对齐 Popup 同名 API)、SetShadowOpacity(uniform 缩 α,与
  SetShadowScale 正交);渲染统一走 DrawGaussianShadowPass,无 device context
  时兜底 DrawLayeredMistShadow
- server/src/window/candidate_presenter.cpp:ApplySkin 按皮肤设阴影——内置四皮肤
  与自定义包用皮肤 CSS 标准对(σ=blur÷2)light {12,0.18,(8,10)}+{4,0.10,(2,3)}、
  dark α .34/.22;默认皮肤 webview 端 body HTML 无 box-shadow,D2D 同步关掉
- 浮动工具栏(scale=0.45)与托盘菜单(全默认)不触新 API,观感不变
- 透明垫余量重验:新 pass 可见范围(α>0.02 阈值)左16/上14/右32/下34 DIP,
  均在左32/上20/右32/下40 垫内,无裁切切痕
2026-09-14 12:34:10 +08:00
fanlusky f05507ade8 fix(config): 升级时保住用户已填的 API token 等凭证
用户反映更新后 API token 被清空、需要重填。根因不在安装器(它早已用
onlyifdoesntexist 且清理时保留 config.toml / config.base.toml,从不覆盖用户
配置),而在 Server 启动时的合并路径 SyncConfigWithInstalledTemplate:当
config.toml 存在、非空但解析不过(上次写入被打断留下半截文件,或 token 里混入
破坏 TOML 的字符)时,旧逻辑直接拿出厂模板覆盖,等于把用户全部配置——尤其是
凭证——清零。

配置升级本质上已经是「逐条把用户值重放到新模板」(MergeTomlIntoTemplate),因此
不另建配置操作日志,而是把「逐条重放」精准用在最难重填的凭证上,并堵掉硬重置:

- ReapplyRealCredentials:合并结束时把用户填过真值的凭证逐条重放回去,有键改值、
  无键则在其分节插入。覆盖 token / api_key / apikey / secret_id / secret_key /
  app_id / asr_app_key 及所有 per-provider 槽位(token_* / asr_token[_*] /
  polish_token[_*]),真值判定复用 VoiceInput::IsPlaceholderToken。这样模板漂移
  漏掉某个 token 键也不会静默丢失;占位符不粘住,新默认值照常生效。
- SyncConfigWithInstalledTemplate 的 unparseable 分支:先把坏文件备份成
  config.toml.corrupt-<时间戳> 留证,再逐行抢救可识别赋值重放到新模板;即便整体
  抢救结果仍解析不过,也只回退到「模板 + 重放凭证」,而非纯模板。

新增回归测试:模板漏键时真 token 被写回、未填的占位符跟随新默认、损坏配置能捞回
真 token,以及读安装器真正分发的 config.default.toml 逐分节确认所有凭证键挺过升级。
2026-09-14 03:15:16 +08:00
fanlusky 529060a359 feat(settings): API Key 配置项增加连通性测试按钮
设置页中所有需要用户自填 API Key 的配置项(AI 助手、候选词翻译、
语音识别、语音润色)旁增加「测试配置」按钮,点击后用当前填写的凭据
向服务发一个最小请求,即时反馈配置是否有效。

- server: 新增 ApiCredentialTest::Run 统一分发(chat 发 1-token 请求、
  ASR 传静音 WAV、翻译发单词校验、豆包走 DoubaoAsrClient::TestCredentials),
  settings 可执行文件补上 curl/zlib/bcrypt/winhttp 及 cloud/voice 源。
- engine/contracts: 新增 apiCredentialTest / apiCredentialTestResult 消息,
  已 generate.py 重新生成并 sync-contracts 同步至 ui-html/shared。
- ui-html: credential-test 模块负责发消息与渲染结果;测试按钮改小两号;
  外观页补充字体下拉框加宽至 1.5 倍、加号按钮宽度减半。
- docs(agents): 补充格式化会卡 CI 的注意事项——clang-format 须为 18.1.8,
  且 format.sh 在 Windows 上因 venv 布局失效,需直接调 clang-format 自查。
2026-09-13 22:11:09 +08:00
fanlusky e350a94509 feat(translation): 候选词翻译支持小牛翻译(NiuTrans)
在线翻译服务由「腾讯云 / 自定义 DeepLX」扩展为三选一,新增小牛翻译。

- 新增 NiuTrans provider(/v2/text/translate):按官方规则对参数加 apikey
  排序拼接后取 MD5 作为 authStr(BCrypt 实现,无新增依赖),表单编码逐条
  翻译,解析 tgtText,errorCode/errorMsg 视为失败
- cloud_translation 调度改为 Provider 枚举,优先级 niutrans > custom > tencent,
  缓存 key 带 provider 区分
- 配置层新增 [niutrans](enabled/app_id/apikey)及读取与读写接口
- 设置界面新增「小牛翻译」下拉项与 APP ID / API Key 输入框,切换时保持
  provider 互斥
- 新增 test_niutrans_translation(凭据校验、鉴权 MD5、响应解析)
2026-09-13 20:54:45 +08:00
fanlusky 8097aa7df2 feat(voice): 豆包语音识别支持新版控制台单 API Key 鉴权
新版豆包控制台只签发一个 API Key,以 X-Api-Key 发送,不再需要 App ID。
原先这条路径是靠「asr_app_key 是否为空」隐式推断的,而随包配置里
asr_app_key 带有占位符(<YOUR_OWN_DOUBAO_APP_ID>)且非空,全新安装会被
判定为旧版控制台,带着占位符发送 X-Api-App-Key 导致鉴权失败。

改为显式配置项 voice_input.doubao_auth_mode(api_key | legacy):

- voice_providers 新增 NormalizeDoubaoAuthMode()/UsesDoubaoLegacyAuth(),
  集中处理模式解析。该项缺失时按 asr_app_key 是否为「可用值」推断,占位符
  不计入,因此既有配置升级后行为不变。
- DoubaoAsrClient 构造函数改收显式 legacy_auth,新版模式下忽略配置中残留
  的 App ID;连接失败提示按模式区分文案。旧版 App ID + Access Token 路径
  完整保留。
- 设置界面新增「豆包鉴权方式」下拉框,新版模式下隐藏 App ID 输入框并把
  Token 标签改为「API Key」。

同时把官方推荐用于输入法场景的 bigmodel_nostream 整句流式接口做成设置里的
可选项,与原有「接口地址」文本框双向同步。默认端点仍为 bigmodel_async,避免
改变既有用户的实时上屏行为。

新版文档将 payload_msg.result 描述为列表,而原 ParseResponse 只按对象读取,
接上该端点会安静地返回空转写。新增 ExtractTranscript() 同时接受对象与数组两
种结构,一套解析覆盖两个端点。
2026-09-13 18:58:39 +08:00
Jia Sui 94abc08e7c feat(engine): ü 系拼写别名归一,非标准拼法候选带轻标记
nue/lue 是词库标准键 nve/lve 的混拼别名,此前按原样查库必然落空。
把别名归一收敛到 resolve_segments 唯一咽喉点(jqxy 系 jv→ju 既有行为
一并收敛),查询、调频、造词键全部落到标准拼法上;别名改写同步进
pinyin_segmentation_,mark_autocorrect_candidates 据此给非标准拼法的
候选写 corrected_from,前端以词尾 * 轻标记提示(对标搜狗纠错星标),
提交、调频、置顶、造词不受影响。

契约翻转:fold_autocorrect_letters 不再把 v 折叠成 u——旧契约
「别名永不标记」按产品决策废弃,别名即标记;nu/nv、lu/lv 是真实
不同音节,归一按段精确匹配 [nl]ue,绝不互串,nu'e 手动分隔符不受
影响。composition 层显示折叠保留(职责是分隔符重建的等价分类),
注释改写说明与字典侧标记契约的分工。
2026-09-13 15:56:29 +08:00
Jia Sui 3c2f3ae374 feat(engine): 全拼纠错补齐漏字/多字,k-best 切分按词频消解歧义
此前纠错只有交换(sahng -> shang)与 QWERTY 邻键(shabg -> shang)两类,
生成器在离线阶段就把「一个错串对应多个音节」的变体整个丢掉,运行时也只
取代价最低的一条切分。三类输入因此没有出路:漏字的(shng)、多敲一键的
(shangg、sjhang)、漏声母连基础切分都切不出来的(uanli 想打 quan'li)。

本次按专利 CN 101133411 B 的思路重排整条链路:错串可能对应的读音全部
保留到运行时,先由 k-best 切分带出平行读音,再让词典词频决定谁排前面。

- 纠错类型从两类扩到四类:新增漏字(shng -> shang)与多字(shangg 双打、
  sjhang 邻键 -> shang)。两个新位跟随既有 autocorrect_transposition /
  autocorrect_neighbor 开关联动(任一打开即生效),定义保持独立,留待
  将来的独立配置键。查询与 preedit 重建共用同一映射,两侧纠正空间一致。
- 生成器从两表扩到四表并保留歧义变体(ahan -> {shan, zhan});同一条
  (错串, 正确) 对被两条规则产出时在两张表各留一份,保证每个类型开关
  单独有效。正确音节收敛进 kCorrectSyllables 单表、行内改存 16-bit
  下标,一万七千多行不再各自重复音节字符串,--check 拦截过期表头。
- 切分层新增 autocorrect_cut_kbest:按位置自左向右做 top-k 假设传播,
  排序键是(纠错边数,边权之和,生成序)——最少纠错优先的契约不变,
  权重只在同边数内打破平局,引擎绝不用一条额外纠错换更低权重。边权取
  专利 [0052] 允许的估常量(交换 10 / 漏字 11 / 多字 12 / 邻键 13),
  标定路径是挖掘用户日志。漏字/多字边不等长,切分段改按 raw 长度推进
  位置,preedit 分隔跟着原始字母的区间走。
- 词典层固定 k=9,为离线评测定值:漏声母键 uan 有 10-15 个等权漏字
  目标,k=3 按表序截断够不到 quan'li 的正确读音;k=9 提升 R@1/R@3
  且 p95 不变。主切分仍是查询键,其余读音交给
  merge_alternative_segmentations 与主读法在词频下竞争;备选读音在
  缓存命中路径同样发布,corrected_from 标记对全部纠错读音生效;纠错
  读音排在音形别名读音之前。结尾单插入(shangg = shang + g)天然是
  简拼尾形状,仍被简拼形守卫拦下——多字纠错的可达面在中间/开头插入
  与多音节输入。
- 纠错门放宽:!segments.empty() 从门里去掉。BFS 工作在原始字母上,
  基础切分为空的丢声母输入恰是最需要纠错的一批,jian -> ian 现在能
  走到纠错候选并按词频合并到首位。简拼形守卫保留:合法音节加至多一个
  尾字母(zheg)是用户意图,任何纠错表都不得改写。
- preedit 补两道防线:zheg 在显式三位掩码下仍走简拼守卫,不被漏字表
  重排为 zheng;只有 BFS 切分的读音与 scheme 别名层完全一致时才按
  BFS 位置重画分隔符,否则 sahng 会被切成 sa'hng 而候选实际来自
  别名读音 shang。

新增独立评测工具 eval_quanpin_autocorrect:从真实 msime.db 采样高频
词条,按交换/邻键/漏字/多字模型注错,测 R@1/R@3、读音还原率与温态
p50/p95,固定种子可复现。它是评测工具不是测试,永远不进 CI,作为
纠错阈值与门逻辑改动的零回归标尺;注错规则与生成器逐键一致,改任一
侧须同一提交同步另一侧。

表格测试改为按 (错串, 正确) 对断言唯一并加覆盖门(合计 >= 4700 条),
新增歧义保留、漏字/多字变体、k-best 排序与权重定序、preedit 守卫等
用例;engine/tests 的 README 与 AGENTS.md 同步写清 imetest 与评测
工具两个产物的边界。
2026-09-13 11:04:34 +08:00