Files
openless/docs/architecture.md
TRIPandHKLHaoBin 6f9af89583 release: OpenLess 2.0.0-Beta.4+build.20260930 (#1126)
Integrate ASR/Omni fixes, API Route, Android IME and confirmation-based vocabulary learning. Complete settings transitions and macOS single-item credential storage. Synchronize all application versions to 2.0.0-Beta.4+build.20260930.

Integrated PRs: #1116, #1120, #1121, #1123, #1124, #1067 and #1128. Linux #1117 remains independent and is excluded from automatic release.

Validated: full cross-platform CI, 18 UI motion cases, 112 frontend test files, 1245 Core tests (1 ignored) and 572 macOS native tests.

Co-authored-by: HKLHaoBin <we3q@qq.com>
2026-09-30 20:32:53 +08:00

132 lines
18 KiB
Markdown
Raw Permalink 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.
# OpenLess 2.0 架构
状态:canonical,当前实现说明;更新:2026-09-30。平台范围见 [2.0 需求](2.0-requirements.md),文件定位见 [目录结构](structure.md)。
## 1. 分层与工作区
应用开发与构建源在 `openless-all/app/`。下文源码路径以该目录为基准。根 [Cargo workspace](../openless-all/app/Cargo.toml) 成员为 `crates/openless-core` + `linux-egui`;`src-tauri`(及其 `backend-tests` 测试 crate)被 exclude,独立构建。Core 是与 Host 同进程的业务库。
| 层 | 位置 | 职责 |
| --- | --- | --- |
| 界面(Win/mac/Android) | `src/`(React/TypeScript/i18next,八种界面语言) | 页面、设置、窗口分支;调用 typed IPC,展示快照与事件 |
| Host(Win/mac/Android) | `src-tauri/`(crate `openless`) | `src/lib.rs` 注册命令;适配窗口、热键、音频、凭据、插入、IME 和生命周期 |
| 共享 Core | `crates/openless-core/` | 业务规则、会话、服务调用和数据仓储;通过 trait 接入 Host 能力 |
| Linux Host + UI | `linux-egui/`(crate `openless-linux-egui`) | `backend.rs` 组装 `OpenLessBackend`,`main.rs` 实现 egui/eframe UI,不依赖 Tauri/WebKitGTK |
Android 侧:`src-tauri/src/android/`(JNI/桥接)+ `android/`(aidl、kotlin、manifests、frontend);`android/frontend` 经 Vite 别名 `@android` 被 `src/` 引用;manifest 由 `scripts/merge-android-*.mjs` 合成。Linux 已有可复用 Host/UI 起点,剩余能力与产品验收见 [交接目录](linux-egui-handoff/README.md)。
## 2. 数据流
```mermaid
flowchart TB
React["React 页面 / 窗口"] --> IPC["src/lib/ipc · typed wrapper"]
IPC --> Tauri["Tauri commands / coordinator / core_adapters"]
Egui["egui UI"] --> Linux["LinuxHost / LinuxBackendBuilder"]
Tauri --> Core["OpenLessBackend · Core"]
Linux --> Core
Core --> Stores["历史 / 设置 / 词库 / 风格包仓储"]
Core --> Network["云端 provider / 风格包 API"]
Core --> Ports["录音 / 插入 / 凭据 / 本地模型等接口"]
Ports --> Native["Host 原生实现"]
Core --> Events["BackendEvent · 语义事件"]
Events --> Tauri
Events --> Linux
```
- 桌面:React → 类型化 IPC 门面(`src/lib/ipc/`)→ Tauri command → Core;Core 事件由 Host 转发回界面。
- Linux:egui UI → `LinuxHost`(`lib.rs`:`snapshot` / `subscribe` / `save_settings` / `drain_events` 等)→ `OpenLessBackend` → Core,类型化 Rust 接口,不经 IPC。
- 浏览器预览:provider 公开目录由 Core 生成到 `src/lib/ipc/provider-descriptors.generated.json`(`cargo run --locked -p openless-core --example export_provider_descriptors` 重新生成;只含公开元数据,无凭据);原生端走同一受启动合同保护的 IPC。
- 语言目录:`contract/language-catalog.json` 是工作语言原生名、识别代码与 Apple locale 的单一来源;React `languageCatalog.ts` 和 Core `language_catalog.rs` 读取同一份数据,识别服务仍在运行时判断具体语种是否可用。界面语言独立保存在 `ol.locale`,启动和切换经既有 `set_remote_locale` 将解析后的语言同步给原生托盘与手机远程输入页。
- 旧 React command/event 名称只保留在 Tauri 兼容 Adapter;跨平台合同以 `contract/backend-2.0.json` 为准。
启动时,`src/App.tsx` 经 `src/lib/ipc/shared.ts` 请求 `get_startup_snapshot`,校验合同版本和 backend 运行状态后进入业务界面。Core 事件定义在 `events.rs`,Tauri 的转译入口为 `src-tauri/src/tauri_events.rs`,Linux 直接订阅类型化事件。
听写主链由 `dictation_engine.rs` 管理:触发会话 → 录音/ASR → 清理与润色 → Host 插入 → 历史与事件。Tauri 在 `coordinator/dictation_core.rs` 接入该链路;本地 ASR 的模型管理归 Core,原生执行实现分别位于 Host。取消、失败和旧会话事件处理也属于该业务链,而不是页面各自实现。
Android 悬浮窗在录音中转入追问时,Host 先捕获原选区,Core 的 `stop_dictation_for_qa` 按原会话 ID 完成转写并释放听写资源,跳过听写润色与文字插入,再由 `QaApi::submit_captured_text` 接收已录问题和选区。重复手势由 Host 的异步锁合并;QA 不重新抓取已经变化的前台选区。
Android IME 通过 `InputConnection` 插入文字;笔画、英文候选与轻量拼音由原生 Kotlin 控制器处理。`OpenLessApplication`、`OpenLessRuntimeService` 和 Warmup Activity 管理后台运行与恢复。用户操作与验收入口见 [Android 输入法](android-ime.md)。
设置保存使用 `get_settings_snapshot` / `update_setting_fields`:前端只提交字段差异,Core 按偏好修订号提交事务并在版本冲突时重试;`prefs:changed` 使前端重新读取快照。`preferencesWriteGate.ts` 将尚未完成的本地编辑叠加到已确认快照,失败时只撤回对应请求。
手改学词由 Core 的 `host_document/observation.rs` 和 `api.rs` 管理插入范围、观察代数、建议有效期与词典写入。独立本机授权默认关闭,配置位于「实验与扩展 → 手改学词」;观察时长 10–60 秒、建议有效期 5–60 秒、最大自动词长 2–32 个字符。设置改变会结束旧观察并清除旧建议,授权与参数不进入云同步。macOS AX、Windows UIA 与 Android 无障碍仅在当前编辑器内进行短时观察,密码框和已知敏感应用被排除;观察文本仅用于本地差分,确认前不写入词典。`PendingCorrection.expiresAtMs` 是 Web 和 Android 确认卡共用的截止时间;历史详情与 Android IME 保留显式确认加词入口,Linux 原生观察仍在独立验收范围。
## 3. Core 模块地图(按域,见 `src/lib.rs` pub mod 清单)
- 听写链路:`dictation_engine` / `dictation_context` / `audio` / `external_audio` / `silence_auto_stop` / `streaming_insert` / `hotkey_interpreter` / `voice_session`
- 服务与凭据:`provider_rules` / `provider_registry` / `provider_resolution` / `provider_service` / `provider_transport` / `cloud_providers` / `providers` / `omni` / `llm_gemini` / `credentials`(+`credentials_legacy`) / `endpoint_security` / `net`
- 本地模型:`model_store` / `local_asr_service` / `local_asr_catalog` / `asr/`(云与本地 provider 实现)
- 文本加工:`polish` / `prompt_compose`(+`prompts/`,`include_str!` 编译进二进制) / `output_cleaning` / `correction` / `vocabulary`
- 知识与历史:`history` / `activity` / `style_packs` / `style_pack_store`(+`style_pack_archive`) / `marketplace`
- 交互域:`qa_service` / `selection_service` / `selection_voice_service`(+`selection_voice_intent`) / `edit_plan` / `less_computer` / `coding_agent`(+`coding_agent_guard`) / `remote_input_service` / `auxiliary` / `cli`
- 基座:`api` / `events` / `ports` / `settings` / `preferences` / `persistence` / `config` / `errors` / `types` / `shared_types` / `shortcut_types` / `domains` / `android_types` / `host_document/` / `testing` / `vendor/`
## 4. Host 注入点
操作系统集成由 Host 提供。Core 的 `ports.rs`、`config.rs`、`domains.rs` 和 `credentials.rs` 定义录音、插入、任务执行、系统动作、领域运行时及凭据接口;Core 自身仍包含 HTTP 调用和框架无关的文件仓储。
Tauri 在 `src-tauri/src/coordinator.rs` 构造 Core,`core_adapters.rs` 组装原生依赖并对接已有 persistence。Linux 在 `linux-egui/src/backend.rs` 使用 `LinuxBackendBuilder`,注入音频、凭据、服务、设置、本地 ASR 和平台动作。`BackendConfig` 由 Host 提供数据、缓存、资源路径与平台能力。
业务规则缺失时应修复 Core;平台能力缺失时修复对应 Host。设置值、测试 fixture 或 `Unsupported` 实现不能代表原生能力已就绪。
## 5. 窗口体系
`src-tauri/tauri.conf.json` 声明 `main`、`capsule` 两个窗口。`src/main.tsx` 读取 `?window=`,`src/App.tsx` 按类型加载胶囊、`qa`(含复用它的「润色结果」模式)、`selection-voice-intent`、`less-computer` 和 `less-computer-glow`;未指定类型时进入主界面。各 WebView 共用前端入口,重页面按需加载;移动端再依据平台能力选择布局。Linux 单实例由 `linux-egui/src/single_instance.rs` 守护并转发启动意图。
主窗口默认逻辑尺寸为 1300×835,允许用户调整;macOS 原生窗口按钮左侧和顶部均留出 16px,前端保留 44px 拖动区。桌面侧栏宽 226px,主内容从版本行下方开始,设置面板单独限制高度并在内部滚动。
Siri、Classic、Typeless 三种胶囊共用 Core 的 `CapsuleStyle`,窗口尺寸与点击范围在保存偏好时同步。胶囊按显示器工作区底部定位,避开未自动隐藏的 Dock/任务栏;可见期间重新检查工作区。带正文的浮窗使用不透明底色,聊天面板另叠加细噪点纹理,圆角外部仍保留透明区域。
思考动画覆盖转写、润色和原生文字写入,输入完成后才收尾。macOS 流式键盘输入由会话内串行 worker 维护原控件和累计 UTF-16 终点;每批发送后不再等待 AX 长确认,finish/cancel 在已接收写入之后等待最终屏障,再恢复输入源。AX 仅读选区范围,不读正文;采用 250 ms 无进展预算和 10 秒总预算。不可读、提交型 Return 或预算耗尽时明确降级到按键已发送语义,不重新粘贴已发送文字。目标应用的实际输入表现仍需设备验收。
Less Computer 面板将听写与直接语音提交分开:麦克风把转写填入草稿供编辑,语音模式和快捷键可直接提交给 Agent。Core 的 `voice_state` 事件携带会话 ID、模式、实时转写及收尾结果;波形使用实际音量采样。停止和取消均绑定指定会话,延迟请求不能结束下一段录音;开麦与文字发送互斥。工具过程默认折叠,运行状态只在真实活动步骤显示动效,右侧工作台汇总当前轮次;历史和多会话仍标为暂不可用。登录弹窗打开时,听写结果只更新草稿,不抢走弹窗或授权浏览器的焦点。
选区直接润色在捕获文字和原输入目标后显示处理中提示,重复快捷键的 Busy 返回不覆盖该提示。已有语音选区入口在松开快捷键后继续显示思考动画,直到处理/替换完成,或交给确认和预览面板。录音提示音的 Web Audio context 在恢复超时或音频时钟冻结时丢弃并最多重试一次,重试沿用原请求的取消和迟到边界。
Selection Voice 在有效输入目标中允许空选区:Core 将无草稿编辑转换为 Compose,生成正文后沿用预览/直接插入路径;已有选区仍按编辑或问答处理。Host 的胶囊所有权与会话 ID 绑定,结束录音后停止接收音量帧,终态通过 Done/Idle 收尾。Selection Voice 和 Less Computer 的辅助录音遵循当前多模态偏好;Omni 将音频转换为指令文字,后续意图、编辑与 Agent 调用仍由各自模型完成。
AI 服务设置在顶部选择传统或多模态模式,各配置入口等宽排布并完整显示,不依赖横向滑动。传统模式禁用多模态模型入口;多模态模式禁用语言模型、语音识别和本地模型入口。禁用入口名称置灰,名称下方附醒目的黄/琥珀色“当前模式未使用”提示,鼠标和键盘均不可进入;连接与扩展保持可用。多模态表单将供应商/模型及自定义高级字段并排,拉取模型与验证集中在页尾;桌面常用窗口内单页显示完整表单,窄屏或放大字体时保留内容的可访问性。切换模式时若当前页面已禁用,则转到当前模式可用的配置页;本地模型的内部跳转入口受相同限制。切换模式保留两组渠道和凭据。Omni 连接测试只验证文本请求,实际音频可用性需录音验证;DashScope 音频使用 `data:;base64,` 和独立 `format` 字段。API Route 作为可手动添加的 OpenAI 兼容润色渠道,不改变默认提供方。
界面动效由 `src/lib/motion.ts` 统一管理:设置面板以 360ms 淡入、上移并缩放到位,背景以 220ms 淡入;先绘制入场起点,再启动动画时钟。其他弹窗入场和配置内容切换 240ms、弹窗退出 180ms,主页面进入 160ms、退出 90ms;缓动读取 `tokens.css` 中的入场/退出曲线,设置入场使用 soft 曲线。动画使用透明度、位移及缩放,不动画布局尺寸。快速关闭、重开或切回原页从当前绘制状态续播,旧切页任务不能覆盖新选择。`useExitMount` 共用退出时长;系统开启减少动态效果时跳过动画等待。服务设置的模式高亮和分类指示条在选中项间移动,供应商配置切换淡入;加载图标与保存中状态保持真实反馈,临时合成层在完成或取消后释放。
界面启动等待所选语言资源就绪;语言选择持久化到 `ol.locale`,其他 WebView 通过存储事件同步,日期、数字和默认风格展示随语言变化。用户修改的风格名称、说明和内容保持原文。旧版两种强制排版字段只保留数据兼容,界面清除其布局效果,窄屏改由响应式布局处理。
## 6. 存储与外部服务
| 数据或连接 | 所有者与源码入口 |
| --- | --- |
| 历史、活动、偏好、词库、纠错、风格包 | Core 对应仓储模块;Tauri `src-tauri/src/persistence/` 提供平台路径及兼容存储适配 |
| 模型与录音文件 | Core `model_store.rs`、Host 本地运行时和 `persistence/paths.rs`;录音归档受设置控制 |
| 服务凭据 | Core `CredentialStore` 合同,Tauri keyring/Android Keystore 或 Linux `credentials.rs` 适配 |
| 云端 ASR / LLM | Core provider 目录、选择与传输模块;平台本地引擎位于 `src-tauri/src/asr/local/` 或 Linux Host |
| 风格包市场 | Core `marketplace.rs` 管理 HTTP、GitHub device flow 与本地安装;地址由 `MarketplaceConfig` 注入,内置默认值在该模块 |
| 加密云同步 | Core `cloud_sync_e2ee` 复用 GitHub 登录并交换独立同步会话,通过 `/v1/...` 保存客户端加密快照;protocol/documents/store 分别负责加密协议、登记与合并、仓库及系统凭据的受控恢复。默认关闭,凭据、本地基线和回滚日志不交给 UI。详情与验证边界见 [加密云同步客户端](encrypted-cloud-sync.md) |
| 旧手动同步 | `cloud_sync.rs` 和 `/me/sync` 保留有限明文快照合同;旧入口不上传新加密文档中的服务密钥。已注册加密恢复 gate 的仓库拒绝旧多文件恢复,防止绕过受控恢复;未注册的旧 Host 保留原合同。见 [旧同步合同](cloud-sync.md) |
| 风格图标 | React `src/lib/stylePackIcon.ts` 清理上传的 SVG 并转成 PNG;`set_style_pack_icon` / `read_style_pack_icon` 经 Core `style_pack_store.rs` 保存资源、校验读取范围并返回图片 data URL。图标沿用 ZIP 的 64 KiB 限制,与风格包一起导出 |
| 局域网手机输入 | Core `remote_input_service.rs` 定义共享业务,Tauri `remote_server/` 提供本机网络入口和网页资源 |
应用不会把普通听写交给风格包市场后端。市场安装完成后使用本地风格包;官网也不参与应用的业务调用。
模型远程元数据只在下载详情选择模型时读取,界面按模型与镜像合并请求并缓存五分钟。Sherpa 离线解码通过 `src-tauri/src/asr/local/blocking_decode.rs` 串行运行;超时或取消不会提前释放原生任务持有的模型和许可,任务结束后才允许下一次解码。长期参考数据、训练准备和历史快照不是 Core 的在线训练服务。
macOS 凭据通过 `persistence/credentials/macos_vault.rs` 共用 `credentials.v2` 钥匙串项目与进程内缓存:服务凭据、云同步本地包装密钥和记住的同步密钥在同一次受保护读取中加载。同步密钥位于 `_macosLocalSyncKeys` 本机扩展,Core 的提供方快照、UI IPC 与云端导出均不包含该字段。提供方保存/恢复保留扩展;新增密钥经原生存储读回验证,遗忘以持久化空值阻止旧项复活并删除旧独立项。
第一次迁移仍须分别获得旧独立钥匙串项目的系统授权,不能取消或扩大它们的 ACL;成功读取后把本机密钥合入同一项目,后续启动只读取该项目。旧分块和迁移来源保留供旧版本使用,读失败不缓存为空、不扫描其他旧项目;可重试授权或读取。Keychain 外部修改在下一次进程启动生效。ASR 配置状态从同一次成功读取的快照生成。其他平台保留各自存储限制与适配。系统对独立项目的授权边界见 [Apple ACL 文档](https://developer.apple.com/documentation/security/access-control-lists)。
## 7. 验证入口
以下命令均在 `openless-all/app/` 执行,按变更范围选择:
| 范围 | 命令与依据 |
| --- | --- |
| 前端与合同 | `npm test`;`pretest` 先构建,`scripts/frontend-test-runner.mjs` 发现前端测试和脚本合同检查,含 Core 快捷键回归 |
| Core/Linux 格式 | `cargo fmt --all --check`,仅根 workspace |
| Core | `cargo test -p openless-core --locked` |
| Linux Host | `cargo test -p openless-linux-egui --locked`;原生能力在 Linux 目标环境验证 |
| Tauri 格式与编译 | `cargo fmt --manifest-path src-tauri/Cargo.toml --check`、`cargo check --locked --manifest-path src-tauri/Cargo.toml` |
| Tauri 库与独立回归 crate | 依平台选 `cargo test --locked --manifest-path src-tauri/Cargo.toml --lib`、`cargo test --locked --manifest-path src-tauri/backend-tests/Cargo.toml`;适用矩阵见 [CI](../.github/workflows/ci.yml) |
源码构建 Tauri 前执行 `git submodule update --init --recursive`。其 manifest 含受 target 条件控制的本地 path 依赖,Cargo 解析仍需对应子模块;部分 CI 作业通过专用脚本去除非目标依赖。Core/Linux workspace 排除 Tauri,不需要为这些独立检查初始化 Tauri 子模块。
仅 Markdown 变动检查相对链接、源码路径与描述一致性。平台交付按 [桌面验收](2.0-desktop-acceptance.md)、[Linux 验收](linux-egui-handoff/07-acceptance.md)和 [发布规范](../RELEASING.md)完成。