基于 Go 的输入法后端进程:通过标准输入/标准输出与 Windows 端宿主(moqi-im-windows / Moqi IME)以行协议通信,为多种输入法实现提供统一运行时。
- 已实现并可接入 Rime / 中州韵 输入法
- fcitx5 输入法正在接入中
- 仓库中还包含
simple_pinyin、meow等输入法实现或实验目录
moqi-ime/
├── server.go # 程序入口:日志、加载输入法、stdin 主循环
├── go.mod # 模块名:github.com/gaboolic/moqi-ime
├── imecore/ # 协议与 TextService 抽象
│ ├── protocol.go # 请求/响应结构、MOQI_MSG 常量
│ ├── client.go # 客户端会话信息
│ └── service.go # TextServiceBase 默认按键/生命周期实现
└── input_methods/ # 与可执行文件同目录部署的子目录(见下文)
├── fcitx5/
├── meow/
├── rime/
└── simple_pinyin/
每个输入法目录下通常包含 ime.json(至少含 guid、name 等),以及各实现所需的资源(如 Rime 的 data/、icons/、ai_config.json 等)。
- 日志:调用
openLogFile(),按顺序尝试创建/追加日志文件;成功则log写入文件,失败则退回到标准错误输出。 - 创建服务:
NewServer()初始化客户端表、工厂表与 stdin 的bufio.Reader。 - 加载输入法:
loadInputMethods(server)(见下一节)。 - 主循环:
Run()逐行读取 stdin,空行跳过;每行交给handleMessage,出错时仍尽量对解析出的client_id回复success: false,避免宿主阻塞。
运行时数据目录使用 Moqi 作为应用文件夹名(例如 %LOCALAPPDATA%\Moqi\Logs、%APPDATA%\Moqi\Rime),与仓库目录名 moqi-ime 相互独立。
运行时以 os.Executable() 所在目录 为根,读取 input_methods/ 下的子目录:
- 对每个子目录,尝试读取
<子目录>/ime.json;读不到或 JSON 无效则跳过。 - 从
ime.json取出guid(转小写)、name等;没有guid则跳过该目录。 - 调用
RegisterService(guid, factory),把「语言配置 GUID」映射到具体的TextService工厂。
重要:选用哪段 Go 实现不是由 ime.json 里的 moduleName 等字段动态决定的,而是由 loadInputMethods 里对子目录名的 switch entry.Name() 决定:
子目录名 entry.Name() |
注册的实现 |
|---|---|
meow |
input_methods/meow |
rime |
input_methods/rime |
simple_pinyin |
input_methods/simple_pinyin |
fcitx5 |
input_methods/fcitx5 |
| 其他任意目录名 | 默认使用 simple_pinyin 的实现(仍使用该机读到的 guid) |
因此:新增输入法时,若需要独立实现,必须在 server.go 中 import 新包 并在上述 switch 中 增加对应目录名分支;仅放 ime.json 而不会匹配到已有 case 时,会落到默认拼音实现。
-
传输:一行一条消息,文本编码与宿主一致;内容为 JSON 负载,外层带前缀字段。
-
请求行(宿主 → 本进程):
<client_id>|<JSON>其中
<client_id>为宿主分配的连接标识;<JSON>反序列化为imecore.Request(见protocol.go)。 -
响应行(本进程 → 宿主):前缀固定为代码中的常量
MOQI_MSG(imecore.MsgMOQI),格式为:MOQI_MSG|<client_id>|<JSON>末尾换行由
fmt.Printf输出;所有正常响应均带MOQI_MSG前缀,与旧式仅<client_id>|...的写法不同,请以当前代码为准。
init:根据请求中的 输入法 GUID 创建会话。GUID 优先取顶层id字符串(与宿主约定一致);若为空则从data.guid读取。在factories中查找工厂,调用TextService.Init;失败则从clients中移除。close:调用Service.Close()并删除客户端。onActivate、onDeactivate、filterKeyDown、onKeyDown、filterKeyUp、onKeyUp、onCommand、onMenu、onCompositionTerminated、onPreservedKey、onLangProfileActivated:转发到已初始化客户端的HandleRequest,再把*imecore.Response转为 map 发送。- 其他
method:返回success: false与错误说明。
各输入法在 HandleRequest 中设置 Response 的 ReturnValue(或 ReturnData)。convertResponse 会写入 JSON 的 return 字段(整数或宿主约定的类型):0 表示未吞掉按键,非 0(通常为 1)表示已处理。组合串、候选、上屏等通过 compositionString、candidateList、commitString、showCandidates、setSelKeys 等字段返回,与 imecore.Response 一致。
除 method、seqNum 外,按键类请求常带 keyCode、charCode、scanCode、repeatCount、keyStates 等;keyStates 支持布尔数组或整数数组两种 JSON 形式(见 protocol.go 的 UnmarshalJSON)。compositionString、candidateList 等由宿主在需要时传入。
- 在
input_methods/<目录名>/下添加ime.json,至少包含guid(与 Windows 侧语言配置文件一致,会转小写匹配)。 - 实现
imecore.TextService:Init、HandleRequest、Close(可嵌入TextServiceBase再覆盖分支)。 - 在
server.go顶部 import 新包。 - 在
loadInputMethods的switch entry.Name()中为<目录名>增加case,调用server.RegisterService(guid, factory)。 - 将
input_methods/<目录名>/与编译出的server.exe放在同一部署根目录下(见下节)。
若只加目录和 ime.json 而不改 switch,GUID 会注册为 默认拼音 实现。
-
Go 版本:见
go.mod(当前为 Go 1.21+)。 -
编译示例(在仓库根目录
moqi-ime/):go build -o server.exe . -
目录布局:
server.exe与input_methods/须位于同一目录(因为loadInputMethods与 Rime 等均用filepath.Dir(os.Executable())拼接路径)。 -
宿主配置:Windows 侧
backends.json为顶层 JSON 数组(不是{ "backends": [...] })。与moqi-im-windows配套时示例为:[ { "name": "moqi-ime", "command": "moqi-ime\\server.exe", "workingDir": "moqi-ime", "params": "" } ]其中
command/workingDir相对于宿主安装根目录,需与实际文件夹名一致。
按顺序尝试,第一个成功打开的文件 作为日志输出:
%LOCALAPPDATA%\Moqi\Logs\moqi-ime.log%TEMP%\Moqi\moqi-ime.log- 当前工作目录下的
moqi-ime.log
目录不存在时会尝试 MkdirAll。若全部失败,日志打到 stderr。
- Rime 共享数据:
<exeDir>\input_methods\rime\data - Rime 用户目录:
%APPDATA%\Moqi\Rime(Rime 实现中APP == "Moqi") - 打开日志目录菜单项:
%LOCALAPPDATA%\Moqi\Logs
input_methods/rime 在 New(client) 时加载 AI 配置;若配置合法且能构造 HTTP 客户端,则根据热键在 filterKeyDown / filterKeyUp / onKeyDown / onKeyUp 中与 Rime 按键逻辑协同处理(见 rime.go 中 handleAI*、triggerAIReview 等)。
- 启动时会尝试把内置配置复制到用户目录(仅当用户文件尚不存在):
源:<exeDir>\input_methods\rime\ai_config.json
目标:%APPDATA%\Moqi\Rime\ai_config.json - 加载顺序:先用户路径,再内置路径;若都读不到有效配置,再尝试纯环境变量拼出一套 API 配置(见下)。
{
"api": {
"base_url": "https://api.openai.com/v1",
"api_key": "replace-with-your-token",
"model": "gpt-4o-mini"
},
"actions": [
{
"name": "写好评",
"hotkey": "Ctrl+Shift+G",
"prompt": "请围绕“{{composition}}”生成最多 3 条适合直接发布的中文好评,每条 20 字左右。"
}
]
}api:base_url、api_key、model必填(可从环境变量补全,见fillAIAPIConfigFromEnv)。actions:可配置多个动作;热键解析支持Ctrl/Alt/Shift与主键 A–Z、0–9(如Ctrl+Shift+G、Ctrl+Alt+1)。若缺省actions,会使用内置默认动作(默认热键 Ctrl+Shift+G,名称「写好评」)。prompt占位符(applyAIPromptPlaceholders):{{previous_commit}}、{{composition}}、{{raw_input}}、{{candidate_1}}~{{candidate_3}}、{{first_candidate}}~{{third_candidate}}、{{candidates_top3}}等;若模板中未出现任何占位符,会在提示词后附加一段固定上下文(上一句 / 原始输入 / 前三个候选)。
MOQI_AI_BASE_URL:OpenAI 兼容 API 根 URL(尾部/会被规整)MOQI_AI_API_KEYMOQI_AI_MODEL
三者齐全时,即使无 ai_config.json 也可启用 AI;此时动作列表为默认的一条。
- 使用 OpenAI 兼容的 Chat Completions JSON API;单次请求超时
aiRequestTimeout= 20 秒(同步调用,请求在触发键处理路径内完成)。 - 模型回复会解析为最多 3 条候选(去重、去编号前缀、单行过长截断等,见
normalizeAICandidates)。 - 输入来源:当前 Rime 组合串与当前候选列表前若干项(及可选的「上一句」提交记忆),不读取宿主应用内选中文本。
- 失败或空结果:记录日志并
resetAIState,不强行上屏,保留原有 Rime 状态。 - 交互:AI 激活后可用数字键、
↑/↓、回车、空格选择与提交;Esc退出 AI 候选模式(具体按键处理见isAIHandledKey与handleAIKeyDown等)。