君ノ声ガ 聞コエルヨ。
你的声音,我能听到呀。
声明:本仓库主要部分均为 AIGC,可能有缺陷,欢迎审查和 PR。
这是一个 BetterZUIKey 的扩展组件,建议与本体搭配使用以达到最佳效果。
缺少本体不影响除了“只响应系统框架语言切换消息(严格模式)”之外的其他功能。
应用图标基于搜狗输入法自带图标二次创作;流萤像素画来源未知,如有侵权请联系删除
Gboard 确实是一款优秀的输入法,但在使用过程中,仍有这样的问题:
Gboard 把「切语言」这件事攥在自己手里——地球键、Shift 单击中/英、以及它自己的语言列表。这些动作要么在它进程内部闭环,要么由系统代它执行,框架看不见也管不了。
于是 BetterZUIKey 里「切换到下一个输入法语言」的逻辑与它根本对不上:BZK 发送的是 subtype 切换,而 Gboard 自身还有一套并行的切换逻辑,两边互相冲突。
- 物理键盘的中文态下把
{}、[]、+-、*#等一律按全角输出,需要半角的场合只能切英文模式- 从逆向结果看:Gboard 原生库里有一张覆盖整个 ASCII 可打印区的 1:1 全角映射表,所以「逐个补进白名单」这条路永远补不完
- 软键盘与物理键盘的引号 / 括号都没有自动补全
- 中文态拼音栏还有字时按
Enter,浏览器地址栏 / 聊天消息栏会被一并提交,极易造成误触Shift+Enter可以缓解,但不能解决根本问题
本模块注入 Gboard 进程,拦截它自己的语言切换、接管中文态的标点与提交管线、补齐自动补全——以此让它与框架(以及 BZK)协同工作。
- 严格模式:拦截 Gboard 自己的语言 / 布局切换,语言只由框架 / BetterZUIKey 决定
- 与 BetterZUIKey 联动:未检测到 BZK 时严格模式开关禁用;需要在 BZK 的「输入法增强 → 输入法适配管理」里把 Gboard 加进「使用系统框架」(BZK v1.7.0 以上),由 BZK 完全接管
- 标点管线:把「中英字符」与「全半角」拆开,分别受不同开关控制
- 引号 / 括号自动补全:软键盘与物理键盘两个独立开关,共用同一份可编辑配对表(默认 18 对)
- 有选区时包裹选区(
abc→(abc))而不是替换它 - 光标后侧已有闭字符时只把光标移过去,不会重复插入;手动移动光标后也可以恢复正常闭合
- 有选区时包裹选区(
- 中文态 Enter 不提交:拼音照常上屏,但不再把输入框误提交
- 配置热生效:广播推送 + 有界补播链,修改配置无需重启输入法
- 状态位镜像:三个状态位(全角 / 中英标点 / 物理补全)的当前档位回传到设置页显示,可以长按条目应急切换
- 替换语音输入:用自定义引擎接管 Gboard 的语音键,长按卡片进入配置页
- 云端脚本引擎:内置讯飞 / 腾讯云 / 豆包(新版 / 旧版),也可以导入自定义脚本
- 离线模型:SenseVoice / Paraformer / 两档流式 Zipformer,完全本机识别(与云端配置互斥)
| 功能 | 解释 | 默认值 |
|---|---|---|
| 只响应系统框架语言切换消息 | 严格模式:拦截 Gboard 的原生切换,语言只认框架信号 | 关 ¹ |
完整的 …… 和 —— |
输入 — / … 时输出两个而不是一个 |
开 |
| 智能中文标点 | 更合理的中文标点映射(+ - # [ ] 等按半角处理) |
开 |
| 全角模式 | 允许在全角 / 半角之间切换(提交的字母数字也一起转,如 ABC123;拼音组合态只转符号) 切换快捷键:Shift+Space |
开(状态默认半角) |
| 中英文标点 | 允许中文态在中英标点之间切换 切换快捷键:Ctrl+. |
开(状态默认中文标点) |
| 智能编号 | 数字后面的 。 ) 自动用半角,方便输入 1. 2) |
开 |
| 引号 / 括号自动补全(软键盘) | 软键盘打引号、括号时自动补另一半并把光标移进中间 | 开 |
| 物理键盘自动补全 | 同上,但只认硬件按键 切换快捷键:Ctrl+Shift+9 |
关(状态默认开) |
| 中文态 Enter 不提交 | 中文态按 Enter 时不再把整个输入框提交 | 关 |
| 替换语音输入 | 用自定义引擎(云端脚本或离线模型)接管 Gboard 的语音键,长按卡片进入配置页 | 关 |
¹ 未检测到 BetterZUIKey 时此项禁用(严格模式要靠 BZK 接管语言切换),强行开启会导致没有可用的切换快捷键
只有三个状态位(全角 / 中英标点 / 物理补全)带快捷键;其余都是纯开关,修改后即时生效。
「语音输入」卡片里可以选离线模型:完全在本机识别,不联网也能正常使用;与上述云端配置文件 (讯飞等)互斥,同一时刻只有一个生效。模型在设置页下载,四档体积差异较大,可按需选择:
| 档位 | 体积 | 语言 | 标点 | 说明 |
|---|---|---|---|---|
| SenseVoice-Small | 约 228 MiB | 中/粤/英/日/韩 | 自带 | 准确率较高,自带标点与数字规整 |
| Paraformer-Small | 约 78 MiB | 中英 | 需补全 | 体积与速度的折中 |
| Zipformer-中文 | 约 24 MiB | 仅中文 | 需补全 | 流式:边说边出字,体积最小 |
| Zipformer-中英 | 约 189 MiB | 中英 | 需补全 | 流式:边说边出字,中英混说 |
设置页卡片上的准确率(97% / 96%)来自官方在公开朗读测试集(AISHELL-1)上的去标点字错率换算, 实际效果视口音与噪声而定;两档流式模型官方未公布数字。
- 流式档位可以边说边出字;若要提高非流式模型的实时性,可以打开「启用切分」,使用 silero_vad 按句切分。 流式模型对口语的非流利现象较为敏感,转录效果可能下降。
- 「补全标点」给不带标点的模型(Paraformer / Zipformer)插入中英标点,需要额外一个约 72 MiB 的 标点模型。
- 权重与推理组件的许可以及 SenseVoice / Paraformer 的署名见 THIRD_PARTY_NOTICES.md 以及设置页「关于 → 开源许可」。
- 权重文件的副本也发布在 个人网站,为保证国内用户的下载体验,优先从此下载,失败后回退 GitHub / Hugging Face
除了离线模型,也可以让云端引擎接管 Gboard 的语音键。需要先到各家控制台开通服务,再长按 「替换语音输入」卡片进入子页,单击对应配置填写参数,保存后该配置自动启用。
| 配置 | 填写内容 | 开通位置 |
|---|---|---|
| 讯飞语音听写 | APPID / APISecret / APIKey | 语音听写(流式版) |
| 腾讯云实时语音识别 | AppID / SecretId / SecretKey / 引擎(默认 16k_zh) |
实时语音识别(需开通) 账号 APPID(13 位数字) API 密钥 |
| 豆包流式语音识别(新版) | API Key / 模型版本 | 服务管理(开通「流式语音识别 2.0」) API Key 管理 |
| 豆包流式语音识别(旧版) | App ID / Access Token / 模型版本 | 应用管理(旧版控制台) Access Token 在该应用详情页 |
「模型版本」请选择控制台里已开通的那一个; 若选错,会在上屏时提示「这个模型版本没有开通」,届时请更换正确版本。
豆包的两条配置还有三个开关:二次识别修正 / 规范为书面格式(ITN) / 删除不流畅内容,默认均关。
每个引擎只允许连接其自身的服务域名,音频不会发往其他地址。
本模块在 Gboard 进程内完成六件事:拦截语言切换 / 路由物理按键 / 改写提交内容 / 配对与补全 / 回传状态位 / 替换语音输入。
逆向细节、实测数据与已知陷阱见 PRINCIPLE.md。
模块 App(MainActivity)
↕ 显式广播(ConfigSender · 变更即刻发 + 有界补播链 · 接收端 SenderCheck 验来源)
↕ 状态位反向广播(模块 → App,用于设置页显示「当前状态」)
↕ 离线权重交付(ModelProvider · App 下载后以 FD 交给输入法进程)
Gboard 进程(BridgeHook)
├── SwitchGuard 严格模式①:IMM / InputMethodService / IMMS 代理 / 特权代理 四层挂点
├── KeyGuard 严格模式②:拦注入的 KEYCODE_LANGUAGE_SWITCH(204)
├── KeyRouter 物理按键总路由:Shift+Space / Ctrl+. / Ctrl+Shift+9 + Shift 单击守卫
│ └ EnterFix 中文态 Enter:改写成 Shift+Enter,再吞掉注入事件
├── SymbolNormHook RemoteInputConnection.commitText / setComposingText(框架类)
│ └ SymbolNorm 语义层 → 宽度层
│ └ AutoPair 补闭字符 / 包选区 / 跳过已存在的闭字符
├── Banner 热键反馈(不用 Toast)
└── 替换语音输入(接管 Gboard 的识别器:云端脚本或离线模型)
├── RecognizerProxy 动态代理识别器接口(按方法「形状」分派)
├── VoiceEngineHost 会话生命周期 + 结果上屏(partial 走结果通道 / final 走 IC)
│ ├── AudioSource 麦克风采集(16k / 单声道 / PCM16,40ms 一帧)
│ ├── ScriptEngine Rhino 脚本宿主 → WsClient(云端)
│ ├── LocalAsr sherpa-onnx:整段 / VAD 切分 / 流式(离线)
│ └── GboardSink 把文字翻译成 Gboard 的结果对象
└── NativeLibs / OfflineModels / OfflineSelfTest(抽 .so / 缓存 / 加载自检)
配置通道与权重传递上,本模块做了几个不太直观的决定,完整推导见 PRINCIPLE.md §9:
- 用显式广播,而不是 ContentProvider —— Gboard 的
targetSdk=36,受 Android 11+ 包可见性限制,无法看到本模块的包;而可见性只约束发起方,反过来由本模块(能看见 Gboard)向它发送显式广播即可送达。接收端在 Gboard 进程内用输入法服务自身的 Context 运行时注册,不要求宿主 APK 声明任何内容。 - 配置要落盘到目标进程 —— 广播是「一次性」的:Gboard 一重启就回到编译期默认值,开关会静默恢复默认值。所以模块收到广播时一并将配置落盘,启动时先读回;另加一条有界补播链,覆盖「修改设置时 Gboard 通常未在运行」的场景。
- 状态位走反向广播 —— 三个状态位存在于 Gboard 进程的 prefs 里,App 无法读取,设置页也就无法显示「当前是哪一档」。所以热键切换的那一刻由模块发一条显式指定包名的广播回来。
- 离线权重不进 APK、也不放共享目录 —— 权重(24 MiB ~ 228 MiB)打进 APK 会导致每次更新都要重新下载,所以由设置页下载到 App 自己的
files/;而 Gboard 无法读取其他 App 的文件,跨进程只能交付 FD。链路是:App 下载(前台服务 + 通知栏进度)→ 交给输入法进程 → 校验体积与 sha256 后落进自己的缓存,只保留当前选中的那一档。
已知陷阱都记在对应类的注释里;下面几条反直觉且会静默失效,改动前建议先读一遍 (推导与实测数据见 PRINCIPLE.md):
- 挂
onKeyDown必须挂「实例类链」 —— Gboard 覆盖了它,只挂InputMethodService只能看到onKeyUp(实测:没有任何一条onKeyDown日志)。同理,挂在框架类上的钩子有一部分从来不响(覆盖版不调super⇒ 框架实现永不执行),这是跨版本必修项,见 §7。 - 符号归一要挂
RemoteInputConnection的全部重载 —— 框架类、不被混淆,但 API 33+ 的 3 参TextAttribute版本才是 Gboard 实际走的那条,只挂两参会漏掉大部分符号(§3)。 - 半角化必须用区间规则(
FF01–FF5E → ASCII),逐个补表补不完 —— Gboard 原生.so里是一张覆盖整个 ASCII 可打印区的 1:1 全角表,+=就是这样被漏掉的(§4)。 - 同一个方法最多挂 64 次(libxposed 上限)⇒ 所有 hook 都要带「装过就跳过」的旗标,否则每次会话重装会把日志刷满(§10)。
在改动严格模式或语言门控之前,强烈建议先通读 PRINCIPLE.md §2 / §3 —— 那两节的结论经过三轮互相推翻才收敛,重走一遍成本很高。
- 前置条件:已安装 LSPosed + Gboard (严格模式另需 BetterZUIKey v1.7.0 以上,未安装时该开关禁用)
| 项 | 值 |
|---|---|
| 包名 | com.google.android.inputmethod.latin |
| 兼容版本 | Gboard 17.2.2 ~ 18.3.1 |
| ABI | arm64 |
模块以
17.2.2为开发基线;18.1.3完成静态核验;18.3.1实装 A/B 测试。 除了 DexKit 的结构化查找之外,功能挂的都是框架类 / 框架方法名,不硬编码混淆名,所以换版本时更可能是「功能降级」而不是崩溃。
- 在 Releases 下载 APK 并安装
- LSPosed Manager 里启用模块 —— 作用域由模块静态声明(只有 Gboard),无需也无法手动勾选
- 杀掉 Gboard 进程,让 hook 生效
- 打开模块 App,主页可见各开关;顶部「自启动权限 → 去获取」可跳到系统设置,放行后配置同步更稳定
- 若要严格模式(需 BZK v1.7.0 以上):在 BZK 的「输入法增强 → 输入法适配管理」的「使用系统框架」一段里勾上 Gboard(1.7.0 起为内置项,默认关),再打开本模块的严格模式开关
git clone git@github.com:CommandPrompt-Wang/BetterZUIKey-GboardExt.git
cd BetterZUIKey-GboardExt
./gradlew :app:assembleDebug
# APK: app/build/outputs/apk/debug/BetterZUIKey-GboardExt-v<versionName>.apk需要 JDK 17 + Android SDK 37(compileSdk 37 / minSdk 27 / targetSdk 36),以及 libxposed API 101。
- 请自备
app-sign.keystore和keystore.properties(与 BZK 同一套签名,都不入库) - 运行期按结构(DexKit)而非写死的混淆名去找 Gboard 内部类
adb shell logcat -s GboardExtnorm hooked 2 method(s) on RemoteInputConnection
keys: installed on <impl class> (2)
strict switch guard installed: 74 hook(s), STRICT=false
key guard: service side installed
config restored: strict=false, longMarks=true, num=true, enter=false, ...
config broadcast: strict=false, longMarks=true, num=true, enter=false, ...
hotkey Shift+Space -> fullwidth=true
pair: ( -> () [hw]
enter: down gate enabled=false chinese=true composing=true meta=0x0 pkg=...
enter: rewrite down -> Shift+Enter pkg=...
strict: blocked injected LANGUAGE_SWITCH (total 3)
各诊断旗标(
DEV_TRACE/DEV_*)默认关,排查时按需在对应类里打开。改动EnterFix/AutoPair之前建议先看enter: down gate ...那一行——「第一次不生效」这类问题通常能直接从该行定位。
这是一个 LSPosed 模块,直接 hook 输入法的按键、提交与语言切换链路。使用前请:
- 理解每个开关的含义再操作
- 不当配置可能导致切不到某个语言、标点 / 配对行为异常,或严格模式下无法切换语言(此时先关掉严格模式)
- 只针对
com.google.android.inputmethod.latin的 Gboard17.2.2~18.3.1/arm64实测;其它版本可以尝试,但不保证效果
开发者不承担因使用本模块造成的输入异常、数据丢失或设备故障的任何责任。
app/src/main/java/moe/lovefirefly/bzk/gboardext/
├── BridgeHook.java # Xposed 入口:按包名过滤 + 后台线程装各 hook
├── SwitchGuard.java # 严格模式①:四层挂点拦「当前输入法内部」的 subtype 切换
├── KeyGuard.java # 严格模式②:拦注入的 KEYCODE_LANGUAGE_SWITCH(204)
├── KeyRouter.java # 物理按键总路由:三个热键 + Shift 单击守卫(挂实例类链)
├── ServiceProbe.java # 定位 IME 实现类 / 中文态判据 / DexKit 结构化查找 / 横幅视图挂载
├── SymbolNormHook.java # 提交管线挂点:RemoteInputConnection(全部重载)+ 智能编号
├── SymbolNorm.java # 符号归一:语义层(该输出什么字符)+ 宽度层(宽窄,区间规则)
├── EnterFix.java # 中文态 Enter:down/up 成对改写为 Shift+Enter + 吞掉注入事件
├── AutoPair.java # 配对:补闭字符 / 包裹选区 / 跳过已存在闭字符(光标兜底)
├── GboardPair.java # 配对表:开→闭 Map + 转义 + 自检(奇偶 / 重复)
├── GboardState.java # 三个状态位,存在目标进程 prefs 里 + 镜像回 App
├── GboardConfig.java # 配置结构 / 默认值 / dump + parseDump
├── BroadcastConfig.java # 配置通道:运行时注册的显式广播接收器 + 落盘到目标进程
├── SenderCheck.java # 广播来源校验:签名级权限 + 发送方核对(阻止第三方伪造配置)
├── ConfigSender.java # App 侧唯一发送入口(设置页 / 补播 / 开机共用)
├── ConfigRetry.java # 有界补播链(AlarmManager.set,不需要精确闹钟权限)
├── ConfigRetryReceiver.java # 补播链的一环 + 开机 / 升级入口
├── ConfigWatch.java # provider 轮询(在 Gboard 上不可行,默认关,留作兜底)
├── ConfigProvider.java # provider 通道(同上,仅兜底)
├── TraceProbe.java # 全量诊断探针:一次按键即可看出真实路径(默认关)
├── Banner.java # 输入法窗口上的一行提示(Toast 会被通知设置拦截)
├── MainActivity.java # 首页:所有开关 + 语音入口 + 「当前状态」显示
├── LicensesActivity.java # 开源许可页(随 APK 分发的组件 + 用户下载模型的署名)
│
│ # 替换语音输入(云端脚本 / 离线模型)——宿主侧与 App 侧
├── VoiceEngineHost.java # 宿主:会话生命周期、klu 代理、结果上屏(partial 走结果通道 / final 走 IC)
├── RecognizerProxy.java # 动态代理实现 Gboard 的识别器接口(按方法的「形状」分派,不依赖混淆名)
├── AudioSource.java # 麦克风采集:16k / 单声道 / PCM16,40ms 一帧 + RMS 音量回调
├── ScriptEngine.java # Rhino 脚本宿主 + ctx.* 原语(ws / b64 / hmac / localAsr*)
├── WsClient.java # 极简 WebSocket 客户端(RFC 6455 子集),只服务于引擎脚本
├── GboardSink.java # 把识别文字翻译成 Gboard 的结果对象(反射 + 形状查找,对不上就降级透传)
├── VoiceProbe.java # 会话准入探测(Gboard 调用序列与参数)
├── VoiceProfiles.java # 引擎配置:内置 index.json 播种 + 用户导入
├── VoiceEngineActivity.java # 设置页:配置文件列表 + 离线模型卡片 + 「启用切分」「补全标点」
├── VoiceEngineAddActivity.java # 「添加配置文件」页(粘贴 / 导入脚本)
├── VoiceModels.java # 离线模型清单:体积 / sha256 / 下载源 / 状态(与配置文件互斥)
├── ModelDownloadService.java# 下载:前台服务 + 通知栏进度 + 断点续传 + 哈希校验
├── ModelProvider.java # 跨进程交权重:以 FD 交给输入法进程
├── OfflineModels.java # 输入法侧缓存 bzk-models/<档位>/(只留当前选中的一档)
├── StorageProbe.java # 存储探针:证明 Gboard 无法读取其他 App 的文件 ⇒ 只能通过 provider 交付 FD
├── OfflineSelfTest.java # 离线引擎自检:抽出 .so → System.load → 询问版本号(失败仅记日志)
├── LocalAsr.java # sherpa-onnx:整段解码 / VAD 切分 / 流式 transducer / 标点
└── NativeLibs.java # 从模块 APK 抽 .so 并按依赖顺序 System.load
app/src/main/java/com/k2fsa/sherpa/onnx/ # sherpa-onnx 官方 Java API(随源码一起编译,宿主只用到其中一小部分)
app/src/main/assets/engines/ # 内置引擎脚本 + index.json:mock · iflytek · tencent · volc(新版)· volc-legacy(旧版)· sensevoice · paraformer · zipformer-{zh,bi}
app/src/main/assets/models/silero_vad.onnx # 「启用切分」用的 VAD 模型(随 APK 分发)
app/src/main/jniLibs/arm64-v8a/ # libonnxruntime.so + libsherpa-onnx-jni.so(离线语音用,随 APK 分发)
third_party/ # 第三方组件与模型许可(Gradle 直接作为 assets 源目录打进 APK)
GPL-3.0 © 2025–2026 CommandPrompt-Wang
本模块分发或使用了若干第三方组件与模型(sherpa-onnx · ONNX Runtime · silero-vad · Rhino 等,
以及 SenseVoice / Paraformer 等权重),各自的许可与署名见
THIRD_PARTY_NOTICES.md(许可全文在 third_party/licenses/,
随 APK 一起分发,设置页「关于 → 开源许可」里也能看)。
