{"id":49133543,"url":"https://github.com/gaboolic/moqi-ime","last_synced_at":"2026-05-07T15:02:34.236Z","repository":{"id":351076122,"uuid":"1197938122","full_name":"gaboolic/moqi-ime","owner":"gaboolic","description":"go语言实现的墨奇输入法后端引擎","archived":false,"fork":false,"pushed_at":"2026-05-07T06:38:56.000Z","size":16920,"stargazers_count":5,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-07T08:23:16.631Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/gaboolic.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-01T02:06:55.000Z","updated_at":"2026-05-07T07:43:12.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/gaboolic/moqi-ime","commit_stats":null,"previous_names":["gaboolic/moqi-ime"],"tags_count":4,"template":false,"template_full_name":null,"purl":"pkg:github/gaboolic/moqi-ime","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gaboolic%2Fmoqi-ime","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gaboolic%2Fmoqi-ime/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gaboolic%2Fmoqi-ime/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gaboolic%2Fmoqi-ime/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/gaboolic","download_url":"https://codeload.github.com/gaboolic/moqi-ime/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gaboolic%2Fmoqi-ime/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32743069,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-07T02:14:30.463Z","status":"ssl_error","status_checked_at":"2026-05-07T02:14:29.405Z","response_time":62,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-04-21T20:08:58.256Z","updated_at":"2026-05-07T15:02:34.206Z","avatar_url":"https://github.com/gaboolic.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# MOQI IME\n\n基于 Go 的输入法后端进程：通过标准输入/标准输出与 Windows 端宿主（`moqi-im-windows` / Moqi IME）以行协议通信，为多种输入法实现提供统一运行时。\n\n## 当前输入法状态\n\n- 已实现并可接入 **Rime / 中州韵** 输入法\n- **fcitx5** 输入法正在接入中\n- 仓库中还包含 `simple_pinyin`、`meow` 等输入法实现或实验目录\n\n## 仓库根目录结构\n\n```\nmoqi-ime/\n├── server.go              # 程序入口：日志、加载输入法、stdin 主循环\n├── go.mod                 # 模块名：github.com/gaboolic/moqi-ime\n├── imecore/               # 协议与 TextService 抽象\n│   ├── protocol.go        # 请求/响应结构、MOQI_MSG 常量\n│   ├── client.go          # 客户端会话信息\n│   └── service.go         # TextServiceBase 默认按键/生命周期实现\n└── input_methods/         # 与可执行文件同目录部署的子目录（见下文）\n    ├── fcitx5/\n    ├── meow/\n    ├── rime/\n    └── simple_pinyin/\n```\n\n每个输入法目录下通常包含 `ime.json`（至少含 `guid`、`name` 等），以及各实现所需的资源（如 Rime 的 `data/`、`icons/`、`ai_config.json` 等）。\n\n## 启动流程（`server.go` → `main`）\n\n1. **日志**：调用 `openLogFile()`，按顺序尝试创建/追加日志文件；成功则 `log` 写入文件，失败则退回到标准错误输出。\n2. **创建服务**：`NewServer()` 初始化客户端表、工厂表与 stdin 的 `bufio.Reader`。\n3. **加载输入法**：`loadInputMethods(server)`（见下一节）。\n4. **主循环**：`Run()` 逐行读取 stdin，空行跳过；每行交给 `handleMessage`，出错时仍尽量对解析出的 `client_id` 回复 `success: false`，避免宿主阻塞。\n\n运行时数据目录使用 **`Moqi`** 作为应用文件夹名（例如 `%LOCALAPPDATA%\\Moqi\\Logs`、`%APPDATA%\\Moqi\\Rime`），与仓库目录名 `moqi-ime` 相互独立。\n\n## 输入法如何被加载（`loadInputMethods`）\n\n运行时以 **`os.Executable()` 所在目录** 为根，读取 `input_methods/` 下的**子目录**：\n\n1. 对每个子目录，尝试读取 `\u003c子目录\u003e/ime.json`；读不到或 JSON 无效则跳过。\n2. 从 `ime.json` 取出 `guid`（转小写）、`name` 等；**没有 `guid` 则跳过该目录**。\n3. 调用 `RegisterService(guid, factory)`，把「语言配置 GUID」映射到具体的 `TextService` 工厂。\n\n**重要**：**选用哪段 Go 实现不是由 `ime.json` 里的 `moduleName` 等字段动态决定的**，而是由 **`loadInputMethods` 里对子目录名的 `switch entry.Name()`** 决定：\n\n| 子目录名 `entry.Name()` | 注册的实现 |\n|-------------------------|------------|\n| `meow` | `input_methods/meow` |\n| `rime` | `input_methods/rime` |\n| `simple_pinyin` | `input_methods/simple_pinyin` |\n| `fcitx5` | `input_methods/fcitx5` |\n| **其他任意目录名** | **默认使用 `simple_pinyin` 的实现**（仍使用该机读到的 `guid`） |\n\n因此：新增输入法时，若需要独立实现，必须在 `server.go` 中 **import 新包** 并在上述 `switch` 中 **增加对应目录名分支**；仅放 `ime.json` 而不会匹配到已有 `case` 时，会落到默认拼音实现。\n\n## 通信协议格式\n\n- **传输**：一行一条消息，文本编码与宿主一致；内容为 **JSON** 负载，外层带前缀字段。\n- **请求行（宿主 → 本进程）**：\n\n  ```text\n  \u003cclient_id\u003e|\u003cJSON\u003e\n  ```\n\n  其中 `\u003cclient_id\u003e` 为宿主分配的连接标识；`\u003cJSON\u003e` 反序列化为 `imecore.Request`（见 `protocol.go`）。\n\n- **响应行（本进程 → 宿主）**：前缀固定为代码中的常量 **`MOQI_MSG`**（`imecore.MsgMOQI`），格式为：\n\n  ```text\n  MOQI_MSG|\u003cclient_id\u003e|\u003cJSON\u003e\n  ```\n\n  末尾换行由 `fmt.Printf` 输出；**所有正常响应均带 `MOQI_MSG` 前缀**，与旧式仅 `\u003cclient_id\u003e|...` 的写法不同，请以当前代码为准。\n\n## 请求与响应语义概要\n\n### `method` 路由（`handleRequest`）\n\n- **`init`**：根据请求中的 **输入法 GUID** 创建会话。GUID 优先取顶层 **`id`** 字符串（与宿主约定一致）；若为空则从 **`data.guid`** 读取。在 `factories` 中查找工厂，调用 `TextService.Init`；失败则从 `clients` 中移除。\n- **`close`**：调用 `Service.Close()` 并删除客户端。\n- **`onActivate`、`onDeactivate`、`filterKeyDown`、`onKeyDown`、`filterKeyUp`、`onKeyUp`、`onCommand`、`onMenu`、`onCompositionTerminated`、`onPreservedKey`、`onLangProfileActivated`**：转发到已初始化客户端的 `HandleRequest`，再把 `*imecore.Response` 转为 map 发送。\n- **其他 `method`**：返回 `success: false` 与错误说明。\n\n### 按键相关返回值\n\n各输入法在 `HandleRequest` 中设置 `Response` 的 `ReturnValue`（或 `ReturnData`）。`convertResponse` 会写入 JSON 的 **`return`** 字段（整数或宿主约定的类型）：**`0` 表示未吞掉按键，`非 0`（通常为 `1`）表示已处理**。组合串、候选、上屏等通过 `compositionString`、`candidateList`、`commitString`、`showCandidates`、`setSelKeys` 等字段返回，与 `imecore.Response` 一致。\n\n### 请求中的常用字段\n\n除 `method`、`seqNum` 外，按键类请求常带 `keyCode`、`charCode`、`scanCode`、`repeatCount`、`keyStates` 等；`keyStates` 支持布尔数组或整数数组两种 JSON 形式（见 `protocol.go` 的 `UnmarshalJSON`）。`compositionString`、`candidateList` 等由宿主在需要时传入。\n\n## 如何新增一种输入法（检查清单）\n\n1. 在 `input_methods/\u003c目录名\u003e/` 下添加 **`ime.json`**，至少包含 **`guid`**（与 Windows 侧语言配置文件一致，会转小写匹配）。\n2. 实现 `imecore.TextService`：`Init`、`HandleRequest`、`Close`（可嵌入 `TextServiceBase` 再覆盖分支）。\n3. 在 **`server.go` 顶部 import** 新包。\n4. 在 **`loadInputMethods` 的 `switch entry.Name()`** 中为 **`\u003c目录名\u003e`** 增加 `case`，调用 `server.RegisterService(guid, factory)`。\n5. 将 **`input_methods/\u003c目录名\u003e/`** 与编译出的 **`server.exe`** 放在同一部署根目录下（见下节）。\n\n若只加目录和 `ime.json` 而不改 `switch`，GUID 会注册为 **默认拼音** 实现。\n\n## 编译与运行假设\n\n- **Go 版本**：见 `go.mod`（当前为 Go 1.21+）。\n- **编译示例**（在仓库根目录 `moqi-ime/`）：\n\n  ```bash\n  go build -o server.exe .\n  ```\n\n- **目录布局**：`server.exe` 与 **`input_methods/`** 须位于**同一目录**（因为 `loadInputMethods` 与 Rime 等均用 `filepath.Dir(os.Executable())` 拼接路径）。\n- **宿主配置**：Windows 侧 `backends.json` 为**顶层 JSON 数组**（不是 `{ \"backends\": [...] }`）。与 `moqi-im-windows` 配套时示例为：\n\n  ```json\n  [\n    {\n      \"name\": \"moqi-ime\",\n      \"command\": \"moqi-ime\\\\server.exe\",\n      \"workingDir\": \"moqi-ime\",\n      \"params\": \"\"\n    }\n  ]\n  ```\n\n  其中 `command` / `workingDir` 相对于宿主安装根目录，需与实际文件夹名一致。\n\n## 日志路径（`openLogFile`）\n\n按顺序尝试，**第一个成功打开的文件** 作为日志输出：\n\n1. `%LOCALAPPDATA%\\Moqi\\Logs\\moqi-ime.log`\n2. `%TEMP%\\Moqi\\moqi-ime.log`\n3. 当前工作目录下的 `moqi-ime.log`\n\n目录不存在时会尝试 `MkdirAll`。若全部失败，日志打到 **stderr**。\n\n## 配置与 Rime 数据路径\n\n- **Rime 共享数据**：`\u003cexeDir\u003e\\input_methods\\rime\\data`\n- **Rime 用户目录**：`%APPDATA%\\Moqi\\Rime`（Rime 实现中 `APP == \"Moqi\"`）\n- **打开日志目录菜单项**：`%LOCALAPPDATA%\\Moqi\\Logs`\n\n## Rime：AI 候选（写好评等）\n\n`input_methods/rime` 在 **`New(client)`** 时加载 AI 配置；若配置合法且能构造 HTTP 客户端，则根据热键在 **`filterKeyDown` / `filterKeyUp` / `onKeyDown` / `onKeyUp`** 中与 Rime 按键逻辑协同处理（见 `rime.go` 中 `handleAI*`、`triggerAIReview` 等）。\n\n### 配置文件位置与复制行为\n\n1. 启动时会尝试把**内置**配置复制到用户目录（仅当用户文件尚不存在）：  \n   源：`\u003cexeDir\u003e\\input_methods\\rime\\ai_config.json`  \n   目标：`%APPDATA%\\Moqi\\Rime\\ai_config.json`\n2. 加载顺序：**先用户路径，再内置路径**；若都读不到有效配置，再尝试**纯环境变量**拼出一套 API 配置（见下）。\n\n### `ai_config.json` 示例\n\n```json\n{\n  \"api\": {\n    \"base_url\": \"https://api.openai.com/v1\",\n    \"api_key\": \"replace-with-your-token\",\n    \"model\": \"gpt-4o-mini\"\n  },\n  \"actions\": [\n    {\n      \"name\": \"写好评\",\n      \"hotkey\": \"Ctrl+Shift+G\",\n      \"prompt\": \"请围绕“{{composition}}”生成最多 3 条适合直接发布的中文好评，每条 20 字左右。\"\n    }\n  ]\n}\n```\n\n- **`api`**：`base_url`、`api_key`、`model` 必填（可从环境变量补全，见 `fillAIAPIConfigFromEnv`）。\n- **`actions`**：可配置多个动作；热键解析支持 `Ctrl` / `Alt` / `Shift` 与主键 **A–Z、0–9**（如 `Ctrl+Shift+G`、`Ctrl+Alt+1`）。若缺省 `actions`，会使用内置默认动作（默认热键 **Ctrl+Shift+G**，名称「写好评」）。\n- **`prompt` 占位符**（`applyAIPromptPlaceholders`）：`{{previous_commit}}`、`{{composition}}`、`{{raw_input}}`、`{{candidate_1}}`～`{{candidate_3}}`、`{{first_candidate}}`～`{{third_candidate}}`、`{{candidates_top3}}` 等；若模板中**未出现任何占位符**，会在提示词后附加一段固定上下文（上一句 / 原始输入 / 前三个候选）。\n\n### 环境变量（无配置文件或字段留空时）\n\n- `MOQI_AI_BASE_URL`：OpenAI 兼容 API 根 URL（尾部 `/` 会被规整）\n- `MOQI_AI_API_KEY`\n- `MOQI_AI_MODEL`  \n\n三者齐全时，即使无 `ai_config.json` 也可启用 AI；此时动作列表为默认的一条。\n\n### HTTP 与结果处理（`ai_client.go`）\n\n- 使用 **OpenAI 兼容的 Chat Completions** JSON API；单次请求超时 **`aiRequestTimeout` = 20 秒**（同步调用，请求在触发键处理路径内完成）。\n- 模型回复会解析为**最多 3 条**候选（去重、去编号前缀、单行过长截断等，见 `normalizeAICandidates`）。\n- **输入来源**：当前 Rime **组合串**与**当前候选列表前若干项**（及可选的「上一句」提交记忆），**不读取宿主应用内选中文本**。\n- **失败或空结果**：记录日志并 `resetAIState`，**不强行上屏**，保留原有 Rime 状态。\n- **交互**：AI 激活后可用数字键、`↑`/`↓`、回车、空格选择与提交；`Esc` 退出 AI 候选模式（具体按键处理见 `isAIHandledKey` 与 `handleAIKeyDown` 等）。\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgaboolic%2Fmoqi-ime","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgaboolic%2Fmoqi-ime","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgaboolic%2Fmoqi-ime/lists"}