Files
qingjian/docs/contributing.md
T
yenharvey 03c4074f59 docs: Android 用户文档、实现要点与移动端统一到 Rust 的计划
- 用户文档:Android 入门页,按键、快捷输入、统计与译词页补手机上的差异
- 设计与笔记:九宫格、语音与滑行、渲染与主题的手机部分,Android 实现要点与 crate 笔记
- 路线图与 contributing:记下 Android 进度,移动端键盘与面板将来统一到 Rust,共享规则不在壳里另写
2026-10-20 20:35:18 +08:00

104 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 开发约定
改代码前先看这一页:架构上不能越的线、代码怎么组织、版本号与提交信息怎么写、改了行为要同步哪些文档。
设计的来龙去脉在 [design/architecture.md](design/architecture.md),各 crate 的实现要点在 [notes/crate-notes.md](notes/crate-notes.md)。
## 架构约束(不要违反)
长版与理由见 [design/architecture.md](design/architecture.md)。
- **Core 与平台层严格解耦。** `qingjian-core` 及其兄弟 crate 必须平台无关:词库、拼音解析、候选生成、排序、学习、翻译、文本变换全部属于 Core。
平台层(IMK / TSF / IBus-Fcitx)只做两件事:把系统输入事件翻译成 Core 的输入,把 Core 返回的帧画到候选窗口。
**平台层里不允许出现排序逻辑、词库访问、翻译调用或文本变换。** 判断标准:把 IMK 换成 TSF,不应该需要改 Core 的任何一行。
- **一个候选词只显示一种辅助语言。** 用户配置 Primary Language + 单个 Learning Language。不要设计成 `translations: Vec<Translation>` 或
`HashMap<Lang, String>` 这类多语言并列的数据结构,那会在 API 层面把「一次只学一种语言」这条产品原则给破坏掉。翻译是候选词的 annotation(可选、单条)。
- **输入优先于学习。** 任何为学习功能增加的延迟、弹窗、UI 干扰都是设计错误。翻译查询不能阻塞候选生成,Core 必须能在翻译尚未就绪时先返回候选。
- **输入方案是配置项,不是模式。** 双拼、注音这类键盘方案放 `[general]` 里当设置,中 / 英切换始终是布尔;新方案不能改变别的方案的既定按键行为([user/getting-started/keys.md](user/getting-started/keys.md))。
- **显示面与设置程序自绘,菜单与安装器原生。** 候选窗、拼音行、状态条这类显示面由渲染器出位图各平台贴图(主题靠它);设置程序 `apps/settings` 三平台共用一份,画在同一个引擎上;
输入法菜单、系统托盘、安装器用各平台原生控件。见 [design/rendering.md](design/rendering.md) 与 [design/settings.md](design/settings.md)。
- **移动端的屏幕键盘与面板将来统一到 Rust。** Android 现在由 Kotlin 自绘键盘与面板,这是过渡状态;做第二个移动平台(iOS / iPadOS / HarmonyOS)时统一到 Rust,见 [plan/roadmap.md](plan/roadmap.md) Phase 5。
在那之前,能跨平台共享的规则(主题推导出的配色、键位与尺寸计算)放 Rust,不在平台壳里新写一份;不为单个平台另起一套键盘或 UI 框架。
Kotlin 里现存、统一时要一起搬的规则:键位表与尺寸(`KeyboardLayout` / `KeyboardMetrics`)、滑行起判阈值(`SwipeTrail.START_KEYS`)、
面板与 App 里写死的颜色(剪贴板代码高亮的数字色、生词标签色、失败与危险操作的红)。清单在 roadmap Phase 5,搬走一项划掉一项。
## 代码组织
- **一个类型一个文件。** 一个 struct / enum / trait 及其 impl 单独一个文件;模块文件只做 `mod` 声明、re-export 与自由函数,不把一个 crate 平铺在 `lib.rs` 里。
- **子模块用目录。** 有子模块的模块用 `foo/mod.rs`,**不用** `foo.rs` + `foo/` 并列。
- **同词干的兄弟文件收进目录,绝不用文件名前缀分组。** `key_event.rs` + `key_outcome.rs` → `key/mod.rs` + `key/{event,outcome}.rs`,哪怕没有 `key.rs` 这个共同父文件;
`query/` + `querying.rs` 这种也不行。判断:两个及以上文件名共享一段前缀且同属一个概念,就收进以那段前缀命名的目录。
- **一个职责连带它专用的类型收进一个目录。** `engine/commit/mod.rs` 放上屏部分,`chain.rs` / `last.rs` 放只有它用的类型。
- **大类型的 `impl` 按职责拆成子模块。** 每个文件一个 `impl Foo { … }`(`host/settings.rs` 这样),结构体与构造留在 `mod.rs`,跨文件用到的私有方法标 `pub(super)`。
- **文件长度。** 单文件不超过 800 行,目标 500 行以内;测试超过 200 行搬到 `tests.rs`(多时 `tests/` 按主题分文件)。
- **导入写精确路径。** 不用 `use super::*` / `use foo::*`(`#[cfg(test)] mod tests` 里的 `use super::*` 除外)。
子文件从父模块拿的类型写 `use super::{Engine, Candidate};`,父模块没定义的直接 `use crate::…` 或 `use <crate>::…`,不绕 `super::` 转手。
存量的 glob 改到那个文件时顺手换掉,不专门清扫。
## 命名与注释
- 代码标识符一律英文,注释与文档用中文;`thiserror` 的 `#[error]` 文案用英文,日志与 UI 文案用中文。
- 新文件都要有 `//!` 文件头;结构体 / 枚举字段之间空一行,字段名说不清的加 `///`(`r` / `g` / `b`、`width` 这类不用,`a`「255 为不透明」这类要)。
- 注释只写维护时用得上的:文件头一两句说它是什么、怎么用;代码里只解释读代码看不出来的约束与原因(平台怪癖、协议约定、踩过的坑),一两句说完。
不写复述代码的教学性注释,不写改动经过、调试过程这类日志型注释;较长的设计理由写进 `docs/`,注释里留一句指过去。
- 不写装饰性分隔注释(`// ====`),提交钩子会拦。
## 依赖与配置
- 依赖用 `cargo add` 加,共用包提到根 `[workspace.dependencies]`;错误用 `thiserror` 不用 `anyhow`;日志用 `tracing` 门面。
- 快捷键一律进 `[shortcut]` 可配置,不写死键码。
- 设置项只有部分平台的输入法读时,在设置程序的 `apps/settings/src/support/` 表里登记三个平台各是「支持 / 不适用 / 待实现」,页面按表显示,不直接写 `cfg!`;「待实现」的同步列进 `docs/plan/todo.md` 的「设置项的平台缺口」。
## 版本号
- `crates/*` 用 `version.workspace = true`;**`apps/*` 各壳是独立发布的产品,写死自己的 `version`**(Windows 读 `server/Cargo.toml`)。
- 发版之间带 `-dev`(两端都是 `0.1.3-dev`),打包脚本再接 git 短哈希成 `0.1.3-dev-1a2b3c4`(脏加 `+`,Cargo.toml 里只写 `-dev`)。
- 发版提交去掉 `-dev` 打标签 `v<版本>`(三个平台共用一个 Release;单平台补丁用 `macos-v` / `windows-v` / `linux-v<版本>`),标签后再改成下一个 `-dev`;带 `-dev` 的标签 CI 拒绝;pkg / Inno 只认数字点号。
## 提交信息
- [Conventional Commits](https://www.conventionalcommits.org/zh-hans/):第一行 `<类型>(<范围>): <说明>`,类型与范围英文小写,说明用中文,例如
`fix(core): 修自绘输入框吞数字`、`feat(windows): 三进程日志统一到 %LOCALAPPDATA%\Qingjian\logs`、`docs(changelog): 补 0.1.3 条目`。
- 类型:`feat` 新功能 / `fix` 修 bug / `docs` 只改文档 / `refactor` 不改行为的整理 / `perf` 性能 / `test` 只改测试 /
`build` 打包与构建脚本 / `ci` 工作流 / `chore` 版本号、依赖、仓库杂务 / `style` 只改格式 / `revert` 还原。
- 范围:crate 或壳的名字——`core` `platform` `render` `dictionary` `translate` `learning` `predict` `lm` `neural` `format` `cli`
`macos` `windows`(Server / DLL / 设置程序细分时用 `server` `tsf` `settings`)`installer` `linux` `android` `tools` `docs` `ci` `deps` `release`;
跨好几处的可以省略。不兼容的改动在范围后加 `!`。
- 正文写「为什么」与取舍,一行一条。`.githooks/commit-msg` 会拦第一行不合格式的提交,合并提交也一样(`git merge --no-ff -m "feat: …"`)。
- 2026-09-16 之前的历史是「`macOS:……` / `Core:……`」的中文冒号格式,不重写。
## 文档同步
- 实现与规划分歧时以代码为准并改文档。技术方向写 `docs/design/`,计划写 `docs/plan/`,工程记录写 `docs/notes/`;不往 README 里加技术内容。
- **用户能感知的行为改了(按键、菜单、设置程序、配置文件、数据文件),同一个提交里改 `docs/user/` 对应的页**,按键改动同时改 `keys.md`。
- 改了某个 crate 的实现要点(数据文件、常数、生成命令),同步 `docs/notes/crate-notes.md`。
## 提交前检查
- 钩子:`.githooks/pre-commit`(禁装饰性分隔注释 + fmt + clippy)、`.githooks/commit-msg`(提交信息格式)、`.githooks/pre-push`(全 workspace 测试);`git config core.hooksPath .githooks` 启用一次。
两个跑编译的钩子按 `uname` 划平台范围:**非 Apple 平台排除 `qingjian-macos`**(IMK 壳依赖 objc2,在别的平台上是硬 `compile_error!`,
排除不掉就整条命令失败),与 [ci.yml](../.github/workflows/ci.yml) 三个 job 的划分一致;Server 的 uiAccess 缺省不带
(没签名的 exe 带 uiAccess=true 起不来,os error 740),只有 `build.ps1 -Sign` 设 `QINGJIAN_UIACCESS=1` 打开。
- **在 Linux / Windows 上改 macOS 壳**:排除之后等于盲改,编译错误要到 CI 才看得到。
`rustup target add aarch64-apple-darwin` 一次,之后 pre-commit 在「这次提交碰了 `apps/macos/`、`crates/` 或依赖」时
会对 darwin 目标跑一遍 clippy(只做类型检查、不链接,与 CI 的 macOS job 同一道门;第一次约半分钟,之后一两秒);
没装这个 target 的人流程不变。
原理与手动跑法见 [notes/crate-notes.md](notes/crate-notes.md) 的 apps/macos 一节。
- 排序 / 整句 / 纠错的改动先跑 `apps/cli` 再合。
## CI 与发版
- CI 三个 job(Linux 全量 / macOS 壳 / Windows 三 crate)都 `--locked`;Dependabot 升 actions;每周 `cargo audit`。
- 发版:推 `<平台>-v<版本>` 标签触发 `release.yml`,门禁是版本号 = 标签且不带 -dev、标签在 main 上、产品数据按 SHA256SUMS 校验。
- CHANGELOG 手写、发版时由维护者统一改(PR 不动它)。流程与 Secrets 见 [notes/release.md](notes/release.md)。
## 外部 PR
- 从 main 开分支,一个 PR 只做一件事、只碰一个平台(Core 改动单独一个)。
- 维护者对着 main 审,squash 合并保留作者署名。PR 模板里的合并前清单就是审核标准。
- **修 bug、改壳里行为(按键、上屏、候选窗位置)的 PR 必须真机验过**:先在自己机器上复现问题,改完在同一个应用里确认修好,
PR 的「怎么验证的」写明系统版本、应用与操作步骤。编译与 CI 通过不算验证。没验过的修复发出去,报 issue 的人升级后还得再报一次。
复现不了的(没有那个应用或系统)不提修复:把分析写在 issue 里,或者提只加日志、不改行为的 PR。
- 新功能涉及几个平台时,验你手上有的平台;没有机器的平台在「怎么验证的」里写明没验,由维护者在真机上测。
不改行为的改动(日志、注释、文档)与有测试 / 回放兜底的 Core 逻辑不受此限。
- `crates/qingjian-render` 与 `crates/bamboo-slip` 是双许可(GPL-3.0-or-later 或商业许可,见各自目录的 `LICENSE.md`)。**向这两个目录提交改动,即表示你同意以 [MIT-0](https://opensource.org/license/mit-0) 许可你的这部分贡献**:版权仍归你,你也可以照常以任何许可在别处使用它;这样维护者才能把它一起放进 GPL 与商业许可。贡献里有别人的代码或素材时,在 PR 里写清来源与许可。仓库其他部分的贡献只按 GPL-3.0-or-later。