docs: 以源码为准重写 1-app 文档系统(19 篇)

保留目录骨架与文件名,内容全部以当前源码为依据重写:范围与版本锚点
(2.0.0-Beta.1 / crate 0.1.0 / com.openless.app)、架构分层与 Core 模块地图、
桌面验收清单(功能域→源码入口→证据)、Linux 交接 8 篇(沿用 L01-L12 登记体系,
锚点落到 settings.rs/audio.rs/ports.rs 等)、后端接口契约、Android 计划、
运营指南 5 篇(以 provider descriptor 与实际 CSP/.gitmodules 为锚)。
原过程中的计划叙述、截图与历史收口记录不再保留。
This commit is contained in:
sim committed 2026-09-07 13:48:29 +08:00
1 parent 639c2fbf9b
commit 34cab16fce
19 files changed
+381 -1857

No files matched your search

+23 -73
View File
@@ -1,81 +1,31 @@
# Windows / macOS 2.0 功能保留验收
# 桌面验收清单(Windows / macOS / Android)
范围依据:[当前2.0需求](./2.0-requirements.md)。本清单定义必须验收什么,不预填“已通过”。
基线:各平台实际交付的 Tauri 1.x 能力,以及 PR #1019 已明确承诺的 2.0 能力。
状态:要求清单,不是通过声明;更新:2026-09-07。每个功能域给出源码入口与必须提供的真实证据;自动化测试通过不等于设备验收通过。证据分级见[范围](2.0-requirements.md)第 4 节。
## 1. 逐项保留的用户能力
## 1. 功能域 → 源码入口 → 验收要求
每一行均检查入口、成功路径、失败/取消、重启后状态和旧数据;不能只检查接口存在。
| ID | 功能组 | 必须保留并验收 |
| 域 | 源码入口(`openless-all/app/`) | 必须的真实证据 |
| --- | --- | --- |
| D01 | 启动与生命周期 | 首次配置、权限引导、正常/最小化启动、单实例、第二次启动动作、原有CLI入口、退出释放资源 |
| D02 | 主听写 | Hold/Toggle/Auto、自定义组合键、停止/Esc、组合键仲裁、去抖/收尾冷却、无声停止、设备故障与重试 |
| D03 | 音频与反馈 | 默认/指定麦克风、设备刷新/切换、音量预览互斥、录音静音及恢复、提示音、胶囊状态和终态可见性 |
| D04 | 云ASR/LLM/Omni | 各平台1.x已支持provider的认证、模型、批量/实时、润色/翻译、代理、超时/取消和错误反馈;实验总开关有效 |
| D05 | 渠道与凭据 | 创建/改名/删除/启停/排序/活动渠道、端点/模型/自定义头等设置、验证/列模型、无鉴权自建端点、OAuth既有流程 |
| D06 | 本地模型 | 原平台目录/镜像/下载/续传/取消/删除、准备/加载/保活/释放、原子激活、模型切换、真实推理与失败补偿 |
| D07 | 风格/语言 | 原文/轻度/结构化/正式等原有模式、工作语言、目标语言、简繁、上下文窗口和前台应用归因 |
| D08 | 全局快捷键 | 听写、翻译、QA、选区、切换风格、打开主窗、指定风格包及各平台实际提供的Agent入口;冲突、重绑与重启恢复 |
| D09 | 插入 | 普通/流式、Unicode/换行、原目标恢复、final reconciliation、剪贴板恢复/降级;Inserted/PasteSent/CopiedFallback/OutcomeUnknown不混淆 |
| D10 | QA | 文本/语音、多轮、选区/上下文、实时输出、停止/取消/关闭/重开、历史偏好;慢启动/转写期间仍可取消 |
| D11 | 选区操作 | 原有润色、预览、确认/取消/撤回、stale保护;原平台及本PR承诺的Selection Voice意图与编辑流程 |
| D12 | Less Computer | 已承诺的文本/语音入口、多轮、增量/工具/审批、拒绝/取消、CLI检测/配置/工作目录、进程树清理 |
| D13 | 历史/录音/统计 | 列表与原有操作、原始/最终文本、真实provider/model/耗时、重新润色/转写、录音播放/导出、独立保留上限、统计热图 |
| D14 | 词典与纠错 | 增删/启停、预设、规则顺序、词条命中、建议接受/拒绝;平台已有编辑观察及隐私开关 |
| D15 | 风格包 | 创建/编辑/启用/激活/重置/删除、提示词诊断、ZIP导入导出、快捷键和活动状态联动 |
| D16 | 风格市场 | 原有列表/详情/安装/下载/上传、点赞/收藏/作品管理、GitHub登录/取消/退出及错误恢复 |
| D17 | 手机输入 | 服务/端口/PIN、地址和二维码、TLS/H5、默认模式、手机PCM、电脑落字开关、停止/取消/断连、连接隔离 |
| D18 | 设置和维护 | 原有主题/字号/语言、胶囊、托盘、启动选项、网络诊断/日志、更新渠道及检查/下载/安装/重启 |
| D19 | 1.x数据 | preferences、渠道/凭据、风格包、词典/规则、历史/录音、模型及自定义目录;幂等、失败保留来源、不覆盖新配置 |
| 启动/生命周期 | `src/App.tsx`(`?window=` 分支)、`src/components/CoreStartupScreen.tsx`、`src-tauri/src/lib.rs` 启动合同 | 启动快照与版本校验、错误可读与重查、崩溃后恢复 |
| 听写 | `src-tauri/src/coordinator/dictation.rs` + Core `dictation_engine.rs` | 首帧、热键触发、取消、静默自动停止、落字 reconciliation、错误恢复 |
| 快捷键与插入 | `src-tauri/src/lib.rs`(`global_hotkey_runtime`)、`src/components/ShortcutRecorder.tsx`、`windows-ime/` | 全局注册/冲突提示/重绑/重启还原;Windows IME 落字与回退 |
| AI 服务与渠道 | `src/pages/settings/{ChannelList,ProvidersSection}.tsx`、`src-tauri/src/commands/{providers,channels,credentials}.rs` | 渠道增删改/启停/验证结果展示/凭据安全存储(明文不落盘) |
| 本地模型 | `src/pages/LocalAsr/`、`src-tauri/src/commands/local_asr.rs`、Core `model_store.rs` | 下载/取消/激活原子性;Windows Foundry/Sherpa;macOS Apple Speech/MLX |
| 历史 | `src/pages/History.tsx`、`src-tauri/src/commands/history.rs` | 列表/详情切换(窄窗)、重润色结果保留、录音归档与导出 |
| 风格包与市场 | `src/pages/{Style,Marketplace}.tsx`、`src-tauri/src/commands/{style_packs,marketplace}.rs` | 导入/应用/导入导出失败反馈;市场浏览/安装/OAuth |
| 划词与 QA | `src/pages/{SelectionAsk,SelectionPolishPreview,SelectionVoiceIntentPicker,QaPanel}.tsx` | 触发→意图→预览→应用/取消/撤回;epoch 防旧事件覆盖 |
| Less Computer | `src/pages/LessComputer{Panel,Glow}.tsx`、`src-tauri/src/coding_agent/` | 工具审批/输出/取消;Windows Job 对象与进程清理 |
| 手机输入 | `src-tauri/src/remote_server/`、远程输入页面 | 配对/TLS/会话桥接/弱网与陈旧地址处理 |
| 胶囊与多屏 | `src/components/Capsule.tsx`、`src-tauri/src/coordinator/capsule_focus.rs` | 跟随光标所在屏、多显示器/多 Space 正确显示 |
| 音频提示 | `src/components/AudioCue.tsx`、`src-tauri/src/audio_mute.rs` | 提示音、系统静音/恢复所有终态恢复音量 |
| 设置与更新 | `src/pages/settings/`(7 分类,`navigation.ts`)、`src/components/{AutoUpdate,AutoUpdateGate}.tsx` | 自动保存提示、焦点返回、更新安装失败可见与手动兜底 |
功能是否属于1.x必须核对实际入口和生产路径;仅有预留配置字段或旧路线图不算已交付。
## 2. 平台特有门槛
## 2. Windows 原生验收
- **Windows**:TSF/IME 管线(`windows-ime/` 工程;`scripts/windows-ime-*.ps1`、`scripts/windows-package-msvc.*`);Foundry/Sherpa 本地 ASR;`scripts/windows-*-smoke.ps1` 套件在真机执行。
- **macOS**:AX/TIS 授权流、Apple Speech、MLX(`scripts/macos-mlx-*.mjs`、`scripts/macos-capsule-spaces-contract.test.mjs`)、签名与公证(`scripts/build-mac.sh`、release workflow)。
- **Android**:APK 工作流(`.github/workflows/android-apk.yml`)+ 悬浮窗/无障碍/Shizuku 契约(`scripts/android-*.test.mjs`)。
- 左右修饰键、组合键、全局监听及原有应用内回退;停止/重绑时注销旧监听。
- TSF DLL安装、Profile/category注册、键盘列表选项、管道/会话生命周期、原窗口/控件指纹和输入法恢复。
- TSF/SendInput/Paste、非TSF降级开关、CRLF/Unicode;原生timeout/cancel和派发后断连不得触发重复落字。
- 普通编辑器、浏览器、聊天框、终端,以及无权限/目标失效场景;不能把假成功写入历史。
- Foundry/Sherpa按当前实际支持模型与运行时验证,包含取消、异常、保活及旧释放任务不得卸载新模型。
- 托盘/任务栏/窗口、自启与系统麦克风;真实CLI满stdin、取消、孤孙和自然完成行为。
- 新装、1.x升级、更新/卸载/回滚;只把实际生成并验证的安装包类型列为通过。跳过MSI不能算MSI成功。
## 3. 完成口径
## 3. macOS 原生验收
- 麦克风/Accessibility拒绝与重新授权,Secure Input、密码等敏感场景边界。
- 关闭光标上下文时零文档读取;前台应用元数据仍用于既有归因/换行规则。
- AX选区/上下文、TIS输入源准备/恢复、普通与流式落字,Auto/ShiftReturn/LineFeed/Return。
- AppKit主线程、NSPanel、胶囊、菜单栏/Dock、多显示器、窗口重开和焦点行为。
- Apple Speech、Qwen/MLX/C、Whisper按原有OS/架构支持矩阵交付;不把Apple Silicon能力当作Intel能力。
- Finder启动时的PATH、外部CLI检测与取消、Keychain及自启。
- 声明支持的arm64/x86_64构建、安装/升级/更新,正式分发的签名、公证与Gatekeeper。
当前2.0 macOS普通听写复用了选区路径的app/PID级目标恢复;1.x普通听写未调用该恢复路径,不能将它描述为既有基线能力。精确恢复同应用内原控件/光标仍未得到证明。
## 4. 证据与阻塞规则
每个D编号按Windows/macOS分别记录:
| 字段 | 记录内容 |
| --- | --- |
| 基线与范围 | 1.x版本/提交、实际入口、OS/架构;原平台不支持的明确写“不适用” |
| 2.0生产路径 | UI/热键/CLI → Tauri Adapter → Core → 原生效果/事件,指出删除的旧业务副本 |
| 自动证据 | 行为回归、类型/编译/链接、目标runner与精确head;不把mock结果称作设备成功 |
| 设备/安装证据 | 安装包、OS/目标应用、步骤、期望/实际、日志或截图;涉及数据须用测试资料 |
| 结论 | 已实现且验证 / 已实现待设备验收 / 未实现 / 原平台不适用 |
- 原平台已有功能出现未实现、错误结果、数据丢失或取消失效:本批桌面2.0阻塞项。
- 平台声明支持但只有构建/fixture证据:保留待验收,不直接标“完整支持”。
- Linux应用/UI/原生验收未完成:移交egui团队,不单独阻塞本批桌面2.0;共享Core缺陷仍阻塞。
- 正式GitHub评审、签名和发布权限按项目流程处理;内部子代理通过不替代这些门禁。
## 5. 2026-09-06 发布收口决定
按维护者决定,优先完成2.0交付;本次提交收口后不再启动新的全面审查轮次。影响输入目标、结果正确性、数据和主要流程的已确认问题保留修复;低影响显示边界暂缓,不将其扩大为新的发布阻塞。
- QA语音启动极快失败、首条Recording尚未被事件桥消费时,胶囊可能没有错误提示;QA面板仍接收Core错误事件。
- 极短跨会话切换中,尚未呈现首帧的QA与旧Selection Voice自动隐藏定时器,仍可能出现胶囊提示被忽略或提前隐藏。已呈现QA的旧事件/定时器覆盖后继原生提示已补epoch检查。
- 上述为源码时序分析的显示风险,不宣称已在真实设备稳定复现,也不等于数据、回答或录音结果错误。完整显示origin接线及旧Selection Voice定时器统一留待后续体验修复。
现有设备、安装包、签名、公证和最终head CI证据仍分别记录;本次收口不把未执行项改写为通过。
安装包、签名、公证与最终 head CI 证据分别记录;未执行项不得改写为通过。设备/安装/升级/回滚验收是发布门,不属于本清单可由代码自证的部分。
+21 -58
View File
@@ -1,71 +1,34 @@
# OpenLess 2.0:当前需求与交付范围
# OpenLess 2.0:范围与完成条件
更新:2026-09-06。本文是本轮范围与验收的权威入口,优先于此前迁移计划、复核记录中的旧范围。
初始盘点基线为 `2b3155722758a2b027f94e7032accf468d03106c`;后续实现/合同修复以本文件所在提交为准。
状态:canonical(2026-09-07 以源码为准重写);更新:2026-09-07。
## 1. 目标
## 1. 平台与版本锚点
1. **Windows、macOS 是本批 2.0 完整支持的平台。** 各自在 Tauri 1.x 中实际已有的用户功能,必须在 2.0 保留并完整实现。
2. **Linux 本批交付可接入的 Core。** egui 团队接管 Linux Host Adapter 的剩余实现、生产接线、egui UI 与 Linux 产品验收。
3. Linux 交接采用[独立目录](./linux-egui-handoff/README.md)中的小文档;逐项说明现成能力、真实缺口、接口、实施责任与验收条件。
## 2. Windows/macOS 的“完整”
- 比较的是用户可执行的完整操作:入口 → Core 业务 → 原生效果 → 结果/历史/错误反馈。只有类型、接口、按钮或 fixture 不算实现。
- 以迁移前各平台实际交付的 1.x 功能为基线。现有源码锚点是 `v1.3.18-tauri` 和 PR 的 1.x base `fc9824ee`;还须覆盖其中较新的 Beta 能力及本 PR 已承诺的功能。
- 不得因修改架构而丢失功能、设置效果、旧数据、快捷键、取消、异常恢复或平台行为。
- Windows 的 TSF/Foundry/Sherpa、macOS 的 AX/TIS/Apple Speech/MLX 等按各自原有 OS/架构支持范围验收,不要求移植另一平台的专有机制。
- Windows Less Computer 等本 PR 已明确增加的交付仍保留,不借重新定义 1.x 基线撤销。
- 1.x 的预留字段或未实现设计不自动成为“已交付功能”;也不能把真实存在的实现误标为从未支持。每项须有源码/产品入口证据。
- 原生能力必须以真实结果表达,不能用 `Unsupported`、no-op、伪成功或仅有单元测试替代该平台原先可用的功能。
详细功能与平台验收见[桌面验收清单](./2.0-desktop-acceptance.md)。该清单是需求,不是“全部已通过”的声明。
## 3. 责任划分
| 交付 | 本轮 Core/Tauri 工作 | egui 团队接手 |
| 目标 | 载体 | 版本锚点 |
| --- | --- | --- |
| 共享业务 | Core 领域实现、状态机、配置/凭据/模型迁移、取消/错误/结果规则 | 调用 Core;发现业务接口缺项时回报 Core 负责人 |
| Windows/macOS | Tauri/React 接线、原生 Adapter、完整 1.x 功能保留、自动与设备验收 | 不属于 Linux 接入任务 |
| Linux 接入面 | 可用的公开接口、DTO/事件、Adapter 合同、fixture、示例和缺口文档 | Linux 原生 Adapter 剩余实现、全局热键、窗口/托盘/权限/更新等宿主效果 |
| Linux UI | 提供已有代码和行为合同作起点 | egui/eframe 页面、交互、事件消费、错误展示、完整产品流程 |
| Linux 发布 | 保留现有验证脚本与已有证据供复用 | Linux 打包、真实桌面/设备、安装升级回滚与正式发布验收 |
| Windows / macOS / Android | Tauri 2 Host + React 界面(`src-tauri/` + `src/`) | `src-tauri/tauri.conf.json` 与 `package.json` 均为 `2.0.0-Beta.1`;identifier `com.openless.app` |
| Linux | egui/eframe Host + 原生 UI(`linux-egui/`,crate `openless-linux-egui`) | crate 版本 0.1.0;发布版本跟随上方 2.0.0-Beta.1 |
| 共享业务 | `crates/openless-core`(平台无关,crate 版本 0.1.0) | 公开合同 `openless-all/app/contract/backend-2.0.json` |
这里的“egui 团队”包含本次明确移交的 **Linux Host 工作**。egui 框架本身不会自动完成系统全局热键、音频、Secret Service 或安装器。
显示版本与内部 crate 版本是两个概念;发布版本号、标签与渠道以仓库发布流程(`RELEASING.md`、GitHub workflows)为准。
## 4. Core 对 Linux 的交付门
## 2. “完整”的定义(Windows / macOS)
“预留 Core”要求能接入、能验证,不是未来再实现的空接口:
- 比较对象是用户可执行的操作链:入口 → Core 业务 → 原生效果 → 结果/历史/错误反馈。仅类型、按钮、fixture 或接口存在不算实现。
- 功能基线为各平台 1.x 实际交付(标签 `v1.3.18-tauri` 及其 beta 线);架构迁移不得丢失功能、旧数据、快捷键、取消与异常恢复路径。
- 平台特有能力按各自 OS 范围验收,不要求跨平台移植:Windows 的 TSF/IME(`windows-ime/` 工程)、Foundry/Sherpa 本地 ASR;macOS 的 AX/TIS、Apple Speech、MLX。
- 原生能力以真实结果表达;`Unsupported`、no-op、伪成功或仅单元测试通过不构成交付。
1. Linux 可依赖 `openless-core`,Core 不依赖 Tauri/egui 或要求调用 Tauri 私有模块。
2. 业务规则有真实实现;平台动作由明确的 Host Interface 承接。缺 Linux 原生效果可以标注未接入,不能把缺少 Core 业务实现推给 UI。
3. 提供 `2.0.0` 启动合同、DTO、事件顺序/重放、session/取消、错误和能力语义。
4. 提供可运行的无设备 fixture/headless 示例及对应 contract tests。
5. Linux 现有 Adapter、UI 和包构建作为可复用起点保留;每项分别标明“已有实现”“接线缺口”“原生验证待补”。
6. 确定缺口在交接目录有责任、实现位置、Core 调用点、依赖与验收要求。
## 5. 本批 2.0 完成条件
## 3. 完成条件
| 范围 | 完成条件 |
| --- | --- |
| Windows | 各项原有能力完成真实生产接线;无未关闭的确定功能/迁移缺陷;通过必要自动测试、安装与目标应用/设备验收 |
| macOS | 同上,并覆盖所声明支持的 Intel/Apple Silicon 与权限、输入源、原生模型、签名分发要求 |
| 共享 Core | 跨平台依赖/合同/迁移/会话与错误语义通过验证,可供 Linux 团队直接接入 |
| Linux 本轮移交 | 本文第4节及拆分交接资料完整;无需等待 Linux 应用所有功能、UI、真实设备和安装升级完成 |
| Windows | [桌面验收清单](2.0-desktop-acceptance.md)各域生产接线 + 安装、目标应用与设备验收 |
| macOS | 同上,另加 Intel/Apple Silicon、权限、输入源、本地模型、签名与公证 |
| Android | APK 工作流与悬浮窗管线按 [Android 计划](android-mobile-apk-overlay-plan.md) |
| Linux(本批移交) | 可接入的 Core + 交接资料完整;应用侧缺口按[登记表](linux-egui-handoff/02-gap-register.md) L01–L12 推进,不阻塞 Windows/macOS 交付 |
| 共享 Core | 合同、迁移、会话与取消语义可被 Linux 直接接入,见[契约](linux-egui-backend-contract.md) |
**Linux 应用侧的未完成项不再阻塞本批 Windows/macOS 2.0 交付。**
但缺陷若位于共享 Core、会破坏 Windows/macOS,或使承诺的 Linux Core 接口不可调用,仍是本轮问题。
## 4. 证据分级
验收报告必须区分:源码已实现、自动测试通过、目标平台编译通过、安装运行通过、设备交互通过。CI 和内部审查不替代正式发布所需的真实行为证据。
## 6. 现有成果和其它平台
- 已完成的 PR 修复、Linux 代码、构建和测试不回退、不删除;本次调整的是责任与完成条件。
- Android 不纳入本次新增的“首批完整支持”目标;若保留现有构建,其已有合同不能因共享 Core 修改而无故破坏。
- 本次是需求与交接范围更新;当前代码版本快照仍为 `2.0.0-Beta.1`。版本号、标签与正式发布按项目发布流程执行。
## 7. 后续报告口径
- 分别报告“Windows 2.0 完成度”“macOS 2.0 完成度”“Core 移交完成度”“Linux 团队待办”。
- Linux 宿主未接线应写作“Core 已有,Linux Host/UI 待实现”,不要写作“Core 缺失”,也不要写作“Linux 功能已完成”。
- 若接口本身尚不足以承载原有业务,列为 Core 缺口并由本轮团队修复。
源码已实现 / 自动测试通过 / 目标平台编译通过 / 安装运行通过 / 设备交互通过,五级分开表述。CI、单元测试与内部审查不替代真实发布所需的行为证据;macOS 上的构建或 cfg 排除不能冒充 Linux 原生结果。
+23 -339
View File
@@ -1,348 +1,32 @@
# OpenLess Android APK 与悬浮窗实施计划
# Android APK 与悬浮窗实施计划
> 状态:实施中 / 后端分层已落地
> 日期:2026-06-07
> 范围:Android APK v1(应用内录音)→ IME v2(跨 App 输入)→ 悬浮窗 v3;不改桌面语义
状态:实施中(后端分层已落地);更新:2026-09-07(以源码为准重写)。
目标是把现有 OpenLess 桌面应用扩展为 Android APK,并在手机端提供可用的语音输入体验。当前项目是 React/Vite + Tauri v2 + Rust 后端,Tauri 官方支持 Android 构建;现仓库已增加 Android 分层与脚手架,桌面核心能力(全局热键、托盘、桌面浮窗、TSF IME 等)通过 `#[cfg(not(mobile))]` 收口。
## 1. 现有管线(源码锚点)
核心策略:先产出可安装 APK,再做 Android 输入法服务,最后做跨 App 悬浮窗。桌面功能保持不受影响。
| 组成 | 位置 |
| --- | --- |
| Kotlin 宿主 | `android/kotlin/`(含 `androidTest/`、`test/`) |
| AIDL 桥接 | `android/aidl/com/` |
| Manifest 合成 | `android/manifests/`(基础)+ `scripts/merge-android-updater-manifest.mjs`、`merge-android-overlay-manifest.mjs`、`merge-android-v1-manifest.mjs` |
| Shizuku | `scripts/merge-android-shizuku-manifest.mjs`、`patch-android-shizuku-deps.mjs`(含各自 `.test.mjs`) |
| 前端片段 | `android/frontend/`(经 vite 别名 `@android` 被 `src/` 引用:`AndroidPermissionsPanel`、`androidMicrophonePermission`、`androidIpc`、`androidTypes`) |
| Rust 桥接 | `src-tauri/src/android/`(jni/native_bridge/insert/overlay/shizuku/updater 等) |
| CI | `.github/workflows/android-apk.yml`;打包脚本 `scripts/copy-android-scaffolding.mjs`、`configure-android-release-signing.mjs` |
| 更新器 | 公钥检查 `scripts/check-android-updater-pubkey.mjs`;manifest updater 合成 |
参考依据:
- Tauri Android 依赖:Android Studio、SDK/NDK、`ANDROID_HOME`、`NDK_HOME`、Android Rust targets。
- Tauri Android 构建命令:`tauri android init`,`tauri android build --apk`。
- Android 悬浮窗:Android 8.0+ 使用 `SYSTEM_ALERT_WINDOW` + `TYPE_APPLICATION_OVERLAY`。
## 2. 契约与边界测试(npm test 自动执行)
---
`scripts/android-ipc-import-boundary.test.mjs`(pretest 强制)+ `android-accessibility-{enabled-detection,paste-cache,selection-ipc}-contract`、`android-credential-keystore-contract`、`android-insert-tier-fallback-contract`。IPC 面与 `contract/backend-2.0.json` 的 `androidJni` 对齐。
## 1. 目标与非目标
## 3. 剩余工作
### 目标
- 悬浮窗(overlay)与无障碍链路的真机验收(契约测试之外的真实设备证据)。
- Shizuku 插件路径在真机的安装/授权/失败反馈闭环。
- 发布签名配置的密钥管理与出包验证(`configure-android-release-signing.mjs` 流程化)。
- **APK v1**:应用内主窗口、设置、历史、云端 ASR/LLM、基础录音、复制结果
- **IME v2**:`OpenLessImeService` 作为跨 App 输入主路径
- **悬浮窗 v3**:前台服务 + `TYPE_APPLICATION_OVERLAY`,仅作录音控制入口
- **平台能力查询**:前端通过 `get_platform_capabilities` 隐藏桌面专属设置项
- **桌面零破坏**:所有适配经 `#[cfg(not(mobile))]` / `#[cfg(mobile)]` 分层
## 4. 参考
### 非目标
- APK 首版不纳入本地 ASR(Foundry、Sherpa、Qwen 桌面假设)
- 首版不承诺直接写入其他 App(无 IME 时走复制兜底)
- Accessibility 跨 App 输入不作为默认路径
- 悬浮窗不承担最终文本插入职责
- 不改桌面现有功能语义
### 明确边界
- **不动** macOS / Windows / Linux 热键、托盘、胶囊、QA 浮窗逻辑
- **Android 命令名与桌面一致**;不支持的命令返回明确 unavailable 状态
- **Coordinator 听写主链路复用**;`end_session` 在 Android 增加 IME commit 分支
---
## 2. 架构定位
```
openless-all/app/
package.json # tauri:android:* scripts
vite.config.ts # TAURI_ENV_PLATFORM → 0.0.0.0 + HMR
src-tauri/
tauri.conf.json # bundle.android.minSdkVersion ≥ 26
tauri.android.conf.json # 单 main 窗口(无 capsule/qa/tray)
capabilities/
default.json # 桌面
mobile.json # Android 主窗口
android-scaffolding/ # Kotlin 模板(init 后复制到 gen/android)
src/
lib.rs # desktop/mobile run() 分层
android_ime.rs # IME 状态 + commit(JNI 桩)
android_overlay.rs # 悬浮窗权限/状态(JNI 桩)
permissions.rs # Android 麦克风 runtime permission 分支
persistence.rs # Android 凭据加密文件桩
types.rs # PlatformCapabilities
commands.rs # get_platform_capabilities + 桌面命令 stub
coordinator/dictation.rs # end_session Android IME 分支
```
平台分层示意:
```
┌─────────────────────────────────────────────────────────┐
│ React UI(能力查询 → 隐藏桌面专属设置) │
├─────────────────────────────────────────────────────────┤
│ Tauri commands(同名 IPC;mobile 返回 unavailable stub) │
├──────────────┬──────────────────────────────────────────┤
│ desktop │ mobile (Android) │
│ hotkey/tray │ in-app dictation + cloud ASR │
│ capsule/qa │ android_ime (v2) / android_overlay(v3) │
│ TSF/AX/粘贴 │ IME commit (v1); clipboard TBD │
└──────────────┴──────────────────────────────────────────┘
```
> **v1 剪贴板**:APK v1 不使用 Android 剪贴板兜底(未接 arboard);跨 App 文本输入依赖后续 IME/JNI 接线。
---
## 3. 模块设计
### 3.1 `PlatformCapabilities`(`types.rs`)
```rust
pub struct PlatformCapabilities {
pub platform: String,
pub supports_ime_input: bool,
pub supports_overlay: bool,
pub supports_desktop_hotkey: bool,
pub supports_tray: bool,
pub supports_local_asr: bool,
pub supports_capsule_overlay: bool,
}
```
- Android:`supportsImeInput=true`(v2 起)、`supportsOverlay=true`(v3 起)、`supportsDesktopHotkey=false`、`supportsTray=false`
- 桌面:按 OS 填真实能力
### 3.2 `android_ime.rs`
- `get_android_ime_status()` → 是否已启用 OpenLess 输入法
- `commit_text(text)` → 通过 JNI 提交到 `InputConnection`(桩 → 日志 + 返回未连接)
- Kotlin:`OpenLessImeService` 继承 `InputMethodService`
### 3.3 `android_overlay.rs`
- `get_android_overlay_status()` → `SYSTEM_ALERT_WINDOW` 授权状态
- `request_android_overlay_permission()` → 跳转 `OverlayPermissionActivity`
- Kotlin:`OpenLessOverlayService`(前台服务 + overlay window)
### 3.4 `permissions.rs` / `persistence.rs`
- 麦克风:Android runtime permission 分支(JNI 桩;未接线时 `NotDetermined`)
- 凭据:Android 加密 JSON 文件桩(`credentials.enc.json`),不沿用桌面 keyring
### 3.5 `coordinator/dictation.rs`
`end_session` 插入阶段:
```
#[cfg(target_os = "android")]
android_ime::commit_text → Inserted
失败 → copy_fallback → CopiedFallback
#[cfg(not(mobile))]
现有 Windows TSF / AX / paste 路径
```
---
## 4. 原生层接口
| 组件 | 职责 |
|---|---|
| `MainActivity` | Tauri WebView + 权限状态桥接 |
| `OpenLessImeService` | 接收识别结果,`commitText` 到当前输入框 |
| `OpenLessOverlayService` | 悬浮窗开始/停止录音、显示状态 |
| `OverlayPermissionActivity` | 引导用户授权 `SYSTEM_ALERT_WINDOW` |
Rust ↔ Kotlin 通信:Tauri mobile plugin / `jni`(脚手架阶段为桩,init 后接线)。
---
## 5. 实施里程碑
### M0 环境与文档(本期)
- 扩展本计划文档(元数据、架构、文件表、风险)
- `package.json`:`tauri:android:init|dev|build`
- `vite.config.ts`:mobile dev host `0.0.0.0` + HMR
- `tauri.conf.json`:`bundle.android.minSdkVersion: 26`
- `tauri.android.conf.json` + `capabilities/mobile.json`
### M1 Rust 分层 + APK 骨架(本期)
- `lib.rs` desktop/mobile `run()` 分层
- `Cargo.toml` gate 桌面专属依赖
- `PlatformCapabilities` + 命令 stub
- `permissions.rs` / `persistence.rs` Android 分支
- `cargo check` 桌面通过;Android target 尽力验证
### M2 IME v2
- 接线 `OpenLessImeService` JNI
- 设置页显示输入法启用状态
- 跨 App 提交验收
### M3 悬浮窗 v3
- 接线 `OpenLessOverlayService`
- 授权引导 + 前台服务稳定性
---
## 6. 文件触达表
| 文件 | 变更 |
|---|---|
| `docs/android-mobile-apk-overlay-plan.md` | 扩展为完整实施规划(本文档) |
| `openless-all/app/package.json` | `tauri:android:*` scripts |
| `.github/workflows/android-apk.yml` | Android debug APK CI |
| `openless-all/app/scripts/merge-android-v1-manifest.mjs` | v1 manifest merge (RECORD_AUDIO) |
| `openless-all/app/vite.config.ts` | mobile dev server / HMR |
| `openless-all/app/src-tauri/tauri.conf.json` | `bundle.android` |
| `openless-all/app/src-tauri/tauri.android.conf.json` | 单 main 窗口 |
| `openless-all/app/src-tauri/capabilities/mobile.json` | Android 权限集 |
| `openless-all/app/src-tauri/Cargo.toml` | gate desktop deps |
| `openless-all/app/src-tauri/src/lib.rs` | mobile/desktop 分层 |
| `openless-all/app/src-tauri/src/types.rs` | `PlatformCapabilities` 等 |
| `openless-all/app/src-tauri/src/commands.rs` | 能力查询 + mobile stub |
| `openless-all/app/src-tauri/src/permissions.rs` | Android 麦克风 |
| `openless-all/app/src-tauri/src/persistence.rs` | Android 凭据 |
| `openless-all/app/src-tauri/src/android_ime.rs` | 新增 |
| `openless-all/app/src-tauri/src/android_overlay.rs` | 新增 |
| `openless-all/app/src-tauri/src/coordinator/dictation.rs` | `end_session` Android 分支 |
| `openless-all/app/src-tauri/android-scaffolding/*.kt` | Kotlin 模板 |
---
## 7. 风险与对策
| 风险 | 对策 |
|---|---|
| 本机无 Android SDK / NDK | 文档记录手动脚手架;`android-scaffolding/` 供 init 后复制 |
| `global-hotkey` / `enigo` / `arboard` 无法编 Android | `Cargo.toml` `cfg(not(mobile))` gate |
| `keyring` 在 Android 不可用 | 加密文件桩 + 后续 Keystore 接线 |
| 桌面构建被 mobile 分层破坏 | 桌面 `cargo check` 为 CI 门禁;mobile 代码 `#[cfg(mobile)]` 隔离 |
| JNI 未接线时 IME/overlay 假成功 | 状态查询返回 `enabled=false`;commit 走 copy fallback |
| 多窗口配置污染移动端 | `tauri.android.conf.json` 仅声明 `main` |
| 本地 ASR 拖慢 Android 首版 | 明确排除;`supportsLocalAsr=false` |
| `SYSTEM_ALERT_WINDOW` 用户拒绝 | 设置页显示状态;悬浮窗功能降级为应用内入口 |
---
## Android APK CI Workflow
GitHub Actions workflow: [`.github/workflows/android-apk.yml`](../.github/workflows/android-apk.yml)
### Capability platform isolation
Desktop permissions live in `capabilities/default.json` with `"platforms": ["macOS", "windows", "linux"]`, so updater, autostart, and multi-window permissions do not apply on Android. Android uses `capabilities/mobile.json` with `"platforms": ["android"]` for the main-window permission set. In-app updates use a custom Rust updater (`src-tauri/src/android/updater.rs`) because `tauri-plugin-updater` does not support Android.
### Triggers & channels
| Trigger | Build mode | Behavior |
|---|---|---|
| `workflow_dispatch` | **release** if `ANDROID_KEYSTORE_*` secrets configured; else **debug (unsigned)** | Upload Actions artifacts; non-blocking fallback with job summary notice when unsigned |
| Push tag `v*-tauri` / `v*-beta-tauri` | **release** (required secrets) | Signed release APKs + minisign `.sig` + `latest-android-{arch}[-beta].json` → attach to GitHub Release |
`OPENLESS_RELEASE_CHANNEL` matches desktop `release-tauri.yml`: `-beta-tauri` → beta (prerelease manifests); otherwise stable.
Tag releases require secrets: `TAURI_SIGNING_PRIVATE_KEY` (minisign), `ANDROID_KEYSTORE_BASE64`, `ANDROID_KEYSTORE_PASSWORD`, `ANDROID_KEY_ALIAS`, `ANDROID_KEY_PASSWORD` (APK signing).
### APK naming
| ABI | Release (tag) | Debug (dispatch) |
|---|---|---|
| arm64-v8a | `OpenLess_<version>_arm64-v8a.apk` | `OpenLess-android-debug-arm64-v8a-run-<n>.apk` |
| armeabi-v7a | `OpenLess_<version>_armeabi-v7a.apk` | `OpenLess-android-debug-armeabi-v7a-run-<n>.apk` |
| x86 | `OpenLess_<version>_x86.apk` | `OpenLess-android-debug-x86-run-<n>.apk` |
| x86_64 | `OpenLess_<version>_x86_64.apk` | `OpenLess-android-debug-x86_64-run-<n>.apk` |
Actions artifact names: `openless-android-release-{abi}` (tag) or `openless-android-debug-{abi}` (dispatch).
Updater manifests: `latest-android-aarch64.json` (arm64), `latest-android-armv7.json`, `latest-android-i686.json`, `latest-android-x86_64.json` (+ `-beta` / `-mirror` variants).
### Command chain (CI)
```bash
cd openless-all/app
npm ci && npm run build
npm run tauri -- android init --ci
node scripts/copy-android-scaffolding.mjs
node scripts/merge-android-v1-manifest.mjs
node scripts/merge-android-overlay-manifest.mjs
node scripts/merge-android-updater-manifest.mjs
# tag only:
node scripts/configure-android-release-signing.mjs
npm run tauri:android:build:debug # dispatch
npm run tauri:android:build:release # tag
# tag only:
node scripts/sign-android-apks.mjs <apks...>
OPENLESS_UPDATE_APK_DIR=... OPENLESS_UPDATE_TARGET=android OPENLESS_UPDATE_ARCH=aarch64 node scripts/write-updater-manifest.mjs
```
`ci.yml` also runs `cargo check --target aarch64-linux-android` as a lightweight mobile cfg gate.
### Manifest merge
- `merge-android-v1-manifest.mjs` — `RECORD_AUDIO` (v1 in-app dictation)
- `merge-android-overlay-manifest.mjs` — overlay + accessibility service
- `merge-android-updater-manifest.mjs` — `REQUEST_INSTALL_PACKAGES` + `FileProvider` for APK install
### In-app updater (Android)
Custom Rust module [`openless-all/app/src-tauri/src/android/updater.rs`](../openless-all/app/src-tauri/src/android/updater.rs) + shared helpers [`updater_logic.rs`](../openless-all/app/src-tauri/src/android/updater_logic.rs). Desktop continues to use `tauri-plugin-updater`.
**Manifest URLs** (generated by [`write-updater-manifest.mjs`](../openless-all/app/scripts/write-updater-manifest.mjs)):
| Channel | Filename | GitHub path |
|---|---|---|
| Stable | `latest-android-{arch}.json` | `releases/latest/download/...` |
| Beta | `latest-android-{arch}-beta.json` | `releases/download/{v*-beta-tauri tag}/...` |
Client tries mirror URL first (`-mirror.json`), then direct GitHub. Beta tag is resolved from `releases.atom` (first `-beta-tauri` entry).
**User-facing behavior**:
- **Settings → About**: manual “Check stable update” always fetches stable manifest.
- **Settings → Advanced**: Beta toggle sets `prefs.updateChannel` for background checks; “Check Beta update” always fetches beta manifest (independent of toggle).
- **Settings → Advanced → Auto-update** (Android): `autoUpdateCheck` toggle — when on, `AutoUpdateGate` checks on launch (+4s) and every 60 minutes using `updateChannel`, then **automatically downloads**, minisign-verifies, and opens the **system APK installer**. When off, only manual buttons run.
- **Desktop**: `autoUpdateCheck` only auto-checks; user confirms in `UpdateDialog` before download/install/restart.
**Install flow**: download to app cache → minisign verify → JNI `install_apk_from_path` → Android system installer. Over-the-air replace is **not** implemented in-app; the user completes install via the system UI. Requires matching APK signature for upgrade.
**Prefs**: `updateChannel` (`stable` | `beta`) = background auto-update channel only; manual buttons pass explicit channel and ignore this pref.
---
## 8. 验收标准
### 构建验证
```bash
cd openless-all/app
npm run build
cargo check --manifest-path src-tauri/Cargo.toml
# 需 Android SDK / NDK(与 CI 一致:debug APK):
npm run tauri:android:init
npm run merge:android-v1-manifest
npm run tauri:android:build
# 等价于 CI 的 debug 构建:
# CI=true npm run tauri -- android init --ci && node scripts/merge-android-v1-manifest.mjs && CI=true npm run tauri:android:build
```
### APK v1
- 首次启动进入主界面
- 麦克风授权流程可触发
- 应用内录音 → 云端转写 → 历史 + 复制
- 桌面专属命令不导致前端白屏(返回 unavailable)
- 桌面 `cargo check` 仍通过
### IME v2 / 悬浮窗 v3
见原 Summary 中 Test Plan 章节(启用输入法后跨 App 提交;悬浮窗授权与前台服务稳定性)。
---
## Compatibility fixes(2026-06-07)
- **`app_invoke_handler_mobile`**:仅保留 dictation / settings / credentials / history / cloud ASR / platform capabilities / Android IME·overlay / permissions / marketplace / style packs / mic devices;已移除 `get_hotkey_*`、`set_shortcut_recording_active` 及全部 desktop-only 命令(热键 setter、updater、local ASR、coding agent、tray 等)。前端 `ipc.ts` 在 `supportsDesktopHotkey === false` 时本地返回 stub,不再 invoke 这些命令。
- **`mobile_stubs`**:`unicode_keystroke` 补齐 `typed_chars()` / `Partial`(与 coordinator 流式插入一致);`shortcut_binding::binding_from_legacy_trigger` 与桌面实现对齐。
- **`Cargo.toml`**:`enigo` / `global-hotkey` / updater / single-instance / autostart 仅在 `cfg(not(mobile))`;Android 侧 `jni` + `ndk-context` 已声明。
---
## 9. 相关参考
- Tauri Android:https://v2.tauri.app/develop/mobile/
- 主听写链路:`openless-all/app/src-tauri/src/coordinator/dictation.rs`
- Windows IME unavailable 模式:`openless-all/app/src-tauri/src/windows_ime_profile.rs`
- Tauri Android:<https://v2.tauri.app/develop/mobile/>
- 主听写链路:`src-tauri/src/coordinator/dictation.rs`
- Windows IME unavailable 模式对照:`src-tauri/src/windows_ime_profile.rs`
+31 -35
View File
@@ -1,49 +1,45 @@
# Core 2.0 架构
# OpenLess 2.0 架构
状态:canonical;阶段:implementation-backed;更新:2026-09-07;源码基线:`fc38edd1`,应用版本 `2.0.0-Beta.1`,公开合同 `2.0.0`。
状态:canonical(2026-09-07 以源码为准重写);更新:2026-09-07。
## 分层与入口
## 1. 分层与工作区
```mermaid
flowchart LR
UI[React 桌面界面] --> IPC[typed IPC / 启动合同]
IPC --> Tauri[Tauri Host]
Egui[Linux egui 界面] --> Linux[Linux Host facade]
Tauri --> Core[openless-core 共享业务]
Linux --> Core
Core --> Ports[Host interfaces]
Ports --> Native[录音 / 系统凭据 / 热键 / 目标输入 / 窗口]
Core --> Events[带 sequence 与 session 的事件]
Events --> Tauri
Events --> Linux
```
应用唯一源是 `openless-all/app/`。Cargo workspace 成员为 `crates/openless-core` + `linux-egui`;`src-tauri`(及其 `backend-tests` 测试 crate)被 exclude,独立构建。
应用工作目录为 `openless-all/app`。
| 层 | 实现位置 | 职责 |
| 层 | 位置 | 职责 |
| --- | --- | --- |
| React | `src/App.tsx`、`src/components/FloatingShell.tsx`、`src/pages/` | 启动显示、页面、草稿、操作与结果反馈 |
| IPC | `src/lib/ipc/shared.ts`、`src/lib/ipc/` | `2.0.0` 启动检查,typed invoke,业务调用前等待 Core 可用 |
| 共享 Core | `crates/openless-core/src/lib.rs`、`api.rs`、`domains.rs` | 公开门面、领域服务、状态机、配置、迁移与会话规则 |
| 原生接口 | `crates/openless-core/src/ports.rs` | 录音、插入、资源、目标上下文、窗口等 Host effect 合同 |
| Tauri Host | `src-tauri/src/commands/`、`core_adapters.rs`、`tauri_coordinator_host.rs`、`tauri_events.rs` | 连接现有 Windows/macOS 原生实现并桥接 React |
| Linux Host/UI | `linux-egui/src/{backend,lib,main}.rs` | 共享 Core 接入、现有 Linux Adapter、egui 页面与事件显示 |
| 合同 | `contract/backend-2.0.json`、`crates/openless-core/tests/`、前端 IPC 测试 | 版本、DTO、事件和领域行为回归 |
| 界面(桌面/移动) | `src/`(React/TypeScript/i18next,五语言) | 唯一桌面 UI;页面、设置分类、窗口分支;不另写业务规则 |
| Host(Win/mac/Android) | `src-tauri/`(crate `2.0.0-Beta.1`) | IPC 命令面(`src/lib.rs` 两个 `generate_handler!`,共 188 个注册);原生 Adapter:窗口、热键、音频、凭据、插入、IME、coordinator |
| 共享 Core | `crates/openless-core/`(crate `0.1.0`) | 全部业务规则与状态机;平台动作只经 `ports.rs` 接口注入 |
| Linux Host + UI | `linux-egui/`(crate `openless-linux-egui` 0.1.0) | 与 Core 同进程:`backend.rs` 组装 `OpenLessBackend`,`main.rs` 跑 egui 界面,不依赖 Tauri/WebKitGTK |
## 听写与状态
Android 侧:`src-tauri/src/android/`(JNI/桥接)+ `android/`(aidl、kotlin、manifests、frontend);`android/frontend` 经 vite 别名 `@android` 被 `src/` 引用;manifest 由 `scripts/merge-android-*.mjs` 合成。
UI/热键发起 → Core 获取会话及真实目标 → Host 录音与识别 → Core 转写/润色 → Host 向原目标输入 → 真实结果、历史与事件反馈。停止录音、取消会话和正常结束分别调用已有门面,页面导航不接管录音资源。
## 2. 数据流
主窗口与胶囊、问答、选区预览、Agent 等 WebView 共用启动合同检查。布局改进只改变启动说明,不跳过检查。`recordingReady` 由首帧 PCM 证据驱动,不能用定时器伪造就绪。
- 桌面: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。
- 旧 React command/event 名称只保留在 Tauri 兼容 Adapter;跨平台合同以 `contract/backend-2.0.json` 为准。
事件以 `sequence` 去重,以 `sessionId` 归属;Unicode delta 的 offset 是 scalar 数量。QA 全量 messages 校准与 Agent 多轮重放仍遵循各自规则。具体语义的规范来源是 [Linux 事件与会话合同](linux-egui-handoff/06-events-and-sessions.md)及对应代码。
## 3. Core 模块地图(按域,见 `src/lib.rs` pub mod 清单)
凭据状态、provider descriptor 与有效 pipeline 由 Core 提供。UI 不在渠道 ID 上猜认证规则,不回显已有密钥,不把“已配置”当成设备或网络测试成功。普通写入、已发送粘贴、已复制回退和未知结果分别反馈。
- 听写链路:`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 注入点
当前完整支持范围、Linux 交接要求与发布验收统一指向 [2.0 当前范围](2.0-requirements.md)。Linux 有运行与界面起点,但 [缺口登记](linux-egui-handoff/02-gap-register.md)仍存在。React 的 Linux 外观预览不能替代 egui 产品验收。
Core 不直接做平台动作;Host 通过 `ports.rs` 接口注入能力。注入清单以 `linux-egui/src/backend.rs` 的 builder 为准:`with_task_spawner` / `with_recorder` / `with_auxiliary_polisher` / `with_text_inserter` / `with_credential_store` / `with_services` / `with_host_actions` / `with_settings_runtime` / `with_local_asr_runtime` / `with_polish_failure_policy`。业务规则缺失时回报 Core 修复,不在 UI/Host 复制规则。
## 验证入口
## 5. 窗口体系
`npm test` 会先构建 React,再跑前端与源码合同;其中 `check-hotkey-injection.mjs` 还会运行 Core 的实际快捷键测试。Core 全量验证使用 `cargo test -p openless-core --locked`;Linux 使用 `cargo test -p openless-linux-egui --locked`,原生目标验证遵循 [Linux 验收](linux-egui-handoff/07-acceptance.md)。所列命令不代表已执行;验证结论以当次运行输出为准。
`tauri.conf.json` 声明 `main`、`capsule` 两个窗口;其余窗口由运行时按 `?window=` 种类创建(`src/App.tsx` 分支):`qa`、`selection` 追问、划词润色预览、语音意图选择、Less Computer(含 glow 变体)与移动端变体。Linux 单实例由 `linux-egui/src/single_instance.rs` 守护并转发启动意图。
## 6. 验证入口
在 `openless-all/app/`:`npm test`(构建 + 前端/合同测试,含 Core 快捷键回归);`cargo fmt --check`(三 crate);`cargo test -p openless-core --locked`;`cargo test -p openless-linux-egui --locked`。`src-tauri` 按平台独立构建,macOS 需 `git submodule update --init --recursive`(vendored qwen-asr C 引擎)。
+19 -24
View File
@@ -1,32 +1,27 @@
# Alibaba Bailian ASR models
# 阿里百炼(DashScope)ASR 模型
OpenLess uses one Alibaba Bailian provider and selects the DashScope protocol from the model name. Configure the API key, region or workspace endpoint, and model in Settings > Providers.
状态:canonical(2026-09-07 以源码为准重写);更新:2026-09-07。
| Mode | Model examples | Behavior |
| --- | --- | --- |
| Realtime WebSocket | `fun-asr-realtime`, `fun-asr-flash-8k-realtime`, `paraformer-realtime-v2`, `sensevoice-realtime-v1` | Streams text while recording. OpenLess downsamples 16 kHz recorder audio for 8 kHz models. |
| Qwen realtime | `qwen3-asr-flash-realtime`, versioned snapshots | Streams text through the Qwen Realtime WebSocket API. |
| Synchronous recording | `fun-asr-flash-*`, `qwen3-asr-flash`, `qwen-audio-3.0-asr-flash` | Sends the recording after capture and waits for one synchronous response. These models are intended for short recordings (`qwen-audio-*-streaming` variants are not supported). |
| Asynchronous file transcription | `fun-asr`, `fun-asr-mtl`, versioned snapshots, `paraformer-v2` | Uploads the recording, starts an asynchronous task, polls it, then downloads the transcript. |
## 1. 代码中的定义
Future dated snapshots that retain one of these model prefixes are routed to the same protocol, so they can be entered manually before they are added to the model picker.
- Provider:`bailian`,`authRequirement = api_key`;默认端点 `wss://dashscope.aliyuncs.com/api-ws/v1/inference/`;默认模型 `fun-asr-realtime`。
- 协议按模型名选择(DashScope WebSocket 推理协议);验证探针 `asr_silence`。
- 目录来源:`provider_rules::provider_descriptors` → 生成文件 `src/lib/ipc/provider-descriptors.generated.json`(重新生成:`cargo run --locked -p openless-core --example export_provider_descriptors`)。
## Endpoints
## 2. 当前静态模型目录(14 个)
The default endpoint is `dashscope.aliyuncs.com`. A workspace or regional host may also be entered, for example `https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com`. OpenLess preserves the host and derives the protocol-specific path and scheme automatically.
| 模型 | 说明 |
| --- | --- |
| `fun-asr-realtime`(默认) | Fun-ASR 实时转写 |
| `fun-asr-flash-8k-realtime` | 8k 采样实时版 |
| `fun-asr-flash-2026-06-15`、`fun-asr`、`fun-asr-2025-11-07`、`fun-asr-2025-08-25` | Fun-ASR 各版本 |
| `fun-asr-mtl`、`fun-asr-mtl-2025-08-25` | 多语种(MTL) |
| `qwen3-asr-flash-realtime`、`qwen3-asr-flash-realtime-2026-02-10`、`qwen3-asr-flash-realtime-2025-10-27`、`qwen3-asr-flash` | Qwen3 ASR 实时/闪速 |
| `qwen-audio-3.0-asr-flash` | Qwen Audio 3.0 ASR |
| `paraformer-v2` | Paraformer v2 |
API keys are region-specific. The endpoint and API key must belong to the same region.
模型 ID 直选与目录以生成文件为准;新增模型先改 Core `provider_rules.rs` 再重新生成目录,不要手工编辑生成文件。
## Temporary uploads
## 3. 在应用内配置
Asynchronous models require a URL. OpenLess obtains a temporary upload policy from DashScope, uploads the local WAV to the returned OSS host, and submits the resulting `oss://` URL with `X-DashScope-OssResourceResolve: enable`.
Alibaba documents this temporary storage as a development and low-concurrency facility. It is rate limited, and uploaded URLs expire after 48 hours. OpenLess uploads one file per completed recording; it does not retain or reuse the temporary URL.
For deployment or high-concurrency use, use stable Alibaba Cloud OSS storage and the official DashScope API directly.
## References
- [Fun-ASR non-real-time HTTP API](https://help.aliyun.com/en/model-studio/fun-asr-recorded-speech-recognition-http-api)
- [Qwen-ASR API reference](https://help.aliyun.com/en/model-studio/qwen-asr-api-reference)
- [DashScope temporary file upload](https://help.aliyun.com/en/model-studio/get-temporary-file-url)
设置 → AI 服务 → 语音识别 → 添加渠道,选择百炼;填入 API Key(DashScope),选择模型;保存后执行“验证”。
+30 -7
View File
@@ -1,11 +1,34 @@
# OpenLess 文档入口
状态:canonical;阶段:implementation-backed;更新:2026-09-07。
状态:canonical;更新:2026-09-07。本目录 2026-09-07 以源代码为唯一依据重写(基于本地 `beta` `639c2fbf`);文档与代码冲突时,以代码为准修文档,不保留历史叙述。
- [Core 2.0 当前范围](2.0-requirements.md):Windows/macOS 交付边界与 Linux 移交责任,范围的规范来源。
- [2.0 架构](architecture.md):共享 Core、平台 Host、界面与事件如何连接。
- [桌面验收清单](2.0-desktop-acceptance.md):逐项功能保留和原生验收要求。
- [Linux 接口契约](linux-egui-backend-contract.md):长接口与历史实现参考,范围冲突以下一条为准。
- [Linux 接入交接](linux-egui-handoff/README.md):Linux 已有能力与剩余工作;从对应小文档继续阅读。
## 范围与架构
已了结的规划、审计与复审过程文档已于 2026-09-07 清理;范围与完成口径一律以当前范围、架构与验收清单为准。
- [2.0 范围](2.0-requirements.md):平台边界、版本锚点与完成条件。
- [架构](architecture.md):Core / 平台 Host / 界面分层、模块地图与验证入口。
- [桌面验收清单](2.0-desktop-acceptance.md):功能域 → 源码入口 → 必须的真实证据。
## Linux 接入交接(egui,目录内互链)
- [交接总览](linux-egui-handoff/README.md):阅读顺序、责任与复用起点。
- [01 Core 合同](linux-egui-handoff/01-core-contract.md)、[02 缺口登记](linux-egui-handoff/02-gap-register.md)(L01–L12)、[03 热键与窗口](linux-egui-handoff/03-hotkeys-and-windows.md)、[04 页面与领域](linux-egui-handoff/04-ui-domains.md)、[05 原生宿主与数据](linux-egui-handoff/05-native-host-and-data.md)、[06 事件与会话](linux-egui-handoff/06-events-and-sessions.md)、[07 验收](linux-egui-handoff/07-acceptance.md)。
## 接口契约
- [Linux egui 后端契约](linux-egui-backend-contract.md):`contract/backend-2.0.json`、启动快照、事件面与公开签名。
## 平台与运营
- [Android APK / 悬浮窗计划](android-mobile-apk-overlay-plan.md)(实施中)
- [火山引擎 ASR 配置](volcengine-setup.md)
- [讯飞(iflytek)ASR 配置](xfyun-asr.md)
- [百炼(DashScope)ASR 模型](bailian-asr-models.md)
- [Tauri CSP 边界](tauri-csp.md)
- [qwen-asr 子模块升级清单](qwen-asr-submodule-upgrade-checklist.md)
## 验证入口(在 `openless-all/app` 执行)
- `npm test`:构建 React + 全部前端/合同测试(含 Core 快捷键回归)。
- `cargo fmt --check`:openless-core、linux-egui、src-tauri 三 crate。
- `cargo test -p openless-core --locked`、`cargo test -p openless-linux-egui --locked`。
- `src-tauri` 被 workspace exclude,按平台独立构建;macOS 构建需 `git submodule update --init --recursive`。
+24 -764
View File
@@ -1,778 +1,38 @@
# Linux egui 后端接口契约(2.0.0)
> **2026-09-06更新**:接手请先读[Linux交接目录](./linux-egui-handoff/README.md)中的短文档;本文保留长接口与历史实现参考,不再作为唯一交接材料。
> 当前范围以[2.0需求](./2.0-requirements.md)为准:Windows/macOS完整保留各自Tauri 1.x功能;Linux交付可接入Core。egui团队承接剩余Linux Host原生接线、UI、设备与发布验收,不仅负责绘制;Linux产品未完成不单独阻塞本批Windows/macOS交付。
>
> 2026-09-04 当前状态:2.0.0 facade、repository、Provider/Credential、听写 Pipeline、Local ASR
> 原子激活、主热键仲裁、录音生命周期、上下文/手改观察、静默重试、插入 reconciliation、
> Transcript reducer、Less Computer/Coding Agent 以及全部领域 Interface 均由 Core 持有。
> Tauri 与 Linux 共用云 ASR/LLM/Omni/Auxiliary Implementation;平台 Host 只注入原生录音、
> 凭据、窗口、进程、焦点和插入 Adapter。Linux production factory 已注入真实 QA、Remote Input、
> Selection preview/revert 与打包的 Qwen runtime;AppImage 会在 listener 前安装 fcitx5 插件。
> 自动 contract 不替代真实平台证据:Ubuntu/Windows/macOS/Android 的设备、安装、升级和签名
> 结果仍须单独记录,当前责任与门槛按[桌面验收](./2.0-desktop-acceptance.md)和[Linux验收](./linux-egui-handoff/07-acceptance.md)区分;以下旧日期状态不能证明Linux无剩余缺口。
>
> 原生预加载必须使用请求的 target/provider type;平台无法准备流式时以 `supports_streaming=false`
> 保留一次性落字。Remote stop 保持可取消的 session,socket 下行只转发本连接所属 session 的事件。
状态:canonical(2026-09-07 以源码为准重写);更新:2026-09-07。范围以[2.0 需求](2.0-requirements.md)为准;本文是长接口与实现参考,交接材料以[交接目录](linux-egui-handoff/README.md)为准。
## 1. 依赖与职责
## 1. 合同文件
Linux UI 只能依赖 `openless-linux-egui -> openless-core`。`openless-core` 的正常
依赖树不得出现 `tauri`、`wry`、`webkit2gtk`、`egui` 或 `eframe`;Tauri 只能在
`src-tauri` adapter 中出现。
`openless-all/app/contract/backend-2.0.json`(`contractVersion` 2.0.0)顶层键:
```toml
[dependencies]
openless-core = { path = "../crates/openless-core" }
```
核心拥有业务状态、会话、provider 选择、持久化和语义事件。Linux host 拥有
Tokio runtime、窗口/托盘、fcitx5、麦克风、凭据和系统操作。egui frame 只读取
快照、非阻塞地 drain 事件,并在事件到达后请求重绘。
## 2. 构造与生命周期
核心构造不创建窗口、不申请权限、不注册全局热键,也不启动网络任务。宿主按
以下顺序初始化:
1. 解析 `data_dir`、`cache_dir`、资源目录、locale 和 `PlatformCapabilities`。
2. 创建各 platform port,组装 `BackendDependencies`。
3. 调用 `OpenLessBackend::new`。
4. 创建事件订阅和 host action sink。
5. 调用 `start()`,把返回的 `StartupSnapshot` 放入 UI view model。
6. 只有启动成功后才启用热键、设备 watcher 和下载 watcher。
Linux 生产宿主不自行组装 provider,也不读取 credential account。以下生产 factory 必须在Host已有的Tokio runtime上下文中调用;同步GUI初始化用短作用域`runtime.enter()`包住构造,随后退出作用域再启动运行时任务。具体executor生命周期与自定义注入约定见[当前接入说明](./linux-egui-handoff/01-core-contract.md)。
```rust
let runtime = LinuxBackendBuilder::from_shared_providers(config)?.build()?;
let backend = Arc::clone(&runtime.backend);
```
`LinuxBackendBuilder::new(config, transcription, polisher)` 仅供测试或特殊宿主显式替换 provider;
egui UI 不应调用它,也不应注册 router 或解析 endpoint/model/extra headers/temperature。
关闭时先禁止新会话,取消活动任务,停止 host watcher,flush 必要持久化,停止
事件桥,再调用幂等的 `shutdown()`。
```rust
let backend = Arc::new(OpenLessBackend::new(config, dependencies)?);
let mut events = backend.subscribe();
let startup = backend.start().await?;
let _session = backend.start_dictation().await?;
let result = backend.stop_dictation().await?;
backend.shutdown().await?;
```
`start()` 和 `shutdown()` 可重复调用;重复启动不会重复发布
`BackendStarted`,重复关闭不会 panic。`snapshot()` 是同步、无副作用的 owned
读取,不返回内部锁或引用:
```rust
let current: BackendSnapshot = backend.snapshot();
```
## 3. 当前公开接口
### 3.1 Facade
| 方法 | 线程/等待 | 语义 |
| --- | --- | --- |
| `new(config, deps)` | 同步 | 校验配置并注入依赖;不启动副作用 |
| `start()` | async | 初始化核心生命周期;幂等 |
| `shutdown()` | async | 停止核心生命周期;幂等 |
| `snapshot()` | 同步 | 返回可克隆快照 |
| `subscribe()` | 同步 | 创建独立事件订阅 |
| `start_dictation()` | async | 建立唯一活动 session,`Starting` 成功后进入 `Recording` |
| `stop_dictation()` | async | `Transcribing -> Polishing -> Inserting -> Completed`,执行 ASR/润色和插入 |
| `cancel_dictation(session)` | async | 取消指定或当前 session;session 不匹配时报错 |
同一时刻最多一个 dictation session。UI 不应通过按钮状态猜测可用性;调用仍需
处理 `Busy`、`InvalidState` 和 `Cancelled`。
### 3.2 已建立的领域 Interface
数据领域直接由深 facade 提供,所有宿主共用同一份 repository 和规则:
| 分组 | 主要操作 |
| 键 | 内容 |
| --- | --- |
| preferences | `get_preferences`;完整文档和单项更新统一通过 `LinuxHost` 的 validated settings Interface |
| credentials/channels | status、显式 `SecretValue` read/write/remove、channel CRUD/reorder/test、active provider |
| history/activity | list/recent/append/update/delete/clear、activity snapshot/bump |
| vocabulary/correction | list/add/remove/enable/hits、preset、correction-rule lifecycle;`enabled_vocabulary_phrases()` 与 `asr_vocabulary_phrases()` 返回 Core 过滤/排序后的 owned 词条 |
| style packs | list/get/create/update/activate/enable/reset/delete、安全 ZIP import/export;`preview_style_pack_runtime(style_pack)` 返回由 Core 统一组装的单轮/多轮 prompt 诊断 |
| dictation | start/stop/cancel、snapshot、session-scoped progress 与插入终态 |
| `startupSnapshot` | 启动快照结构与版本校验规则;UI 必须先消费快照再渲染 |
| `backendEvent` | 语义事件清单、顺序与重放规则 |
| `lessComputerVoice` | Less Computer 语音事件面 |
| `androidJni` | Android JNI 合同(src-tauri android 桥接共用) |
| `linuxFacade` | Linux 专用 facade 面(`LinuxHost` 公开方法对应) |
| `enums` | 共享枚举(provider 类型、状态、错误等) |
有平台、网络、进程或 runtime 变化点的复杂领域通过
`OpenLessBackend::services() -> &BackendServices` 暴露:
## 2. Linux 侧公开签名(源码为准)
| Interface | 已冻结的 use-case |
| --- | --- |
| `ProviderApi` | validate、list models,按 ASR/LLM/Omni 和可选 channel 选择 |
| `LocalAsrApi` | settings/catalog/status/remote info、目录/模型/镜像/keep-loaded 设置、download/prepare/preload/release/delete/test |
| `ModelStore` | Core 统一 catalog、HF tree 分页、文件过滤、Range/断点下载、SHA-256、staging/ready sentinel、归档解压与 1.x 模型目录迁移 |
| `SelectionApi` | snapshot、begin polish、confirm、cancel、revert |
| `SelectionVoiceApi` | begin/mark processing、Core-owned transcript 处理、intent confirm、edit delivery、QA preview create/revise、preview apply ticket/finish、cancel/revert |
| `QaApi` | snapshot、toggle recording、按每轮token stop_recording、submit text、edit-instruction mode、session cancel、dismiss |
| `LessComputerApi` | submit、cancel、dismiss、begin turn、approval decision;Core 统一 provider/model/permission/workdir/prompt/guard/continuation |
| `RemoteInputApi` | 同步 status、configure、显式读取/重新生成 pairing PIN、locale、local IPs、connect/disconnect、start/feed/stop/cancel stream |
| `MarketplaceApi` | list/detail/install/download/upload/like/delete、my lists、GitHub device flow、logout |
| `CodingAgentApi` | detect/list models/risk/run/cancel/approve |
| `PlatformApi` | microphone devices/permission、accessibility permission、permission request、hotkey status |
| `AuxiliaryApi` | 对既有文本执行单轮 repolish;对宿主提供的规范 PCM 执行单轮 retranscription |
- `linux-egui/src/lib.rs`:`pub struct LinuxHost`;`LinuxHost::new(Arc<OpenLessBackend>)`、`with_settings_runtime`、`backend()`、`subscribe() -> EventSubscription`、`snapshot() -> BackendSnapshot`、`save_settings(...)`、`update_settings_strict(...)`、`drain_events(...)`、`feed_less_computer_pcm(&[u8])`。
- `linux-egui/src/backend.rs`:`LinuxBackendRuntime`;`LinuxBackendBuilder::from_shared_providers(BackendConfig)` + `with_task_spawner / with_recorder / with_auxiliary_polisher / with_text_inserter / with_credential_store / with_services / with_host_actions / with_settings_runtime / with_local_asr_runtime / with_polish_failure_policy` → `build() -> LinuxBackendRuntime`。
- 事件消费:`drain_events`(`lib.rs`)批量取走 Core 语义事件;订阅经 `EventSubscription`。
`BackendServices::unsupported()` 是正式的降级 Adapter:每个调用返回
`BackendErrorCode::Unsupported`,不会启动 task 或返回空成功。egui crate 不得直接 include
`src-tauri/src/*.rs`,也不得为了暂时可用而复制业务逻辑。
## 3. 注入接口(`crates/openless-core/src/ports.rs`)
`preview_style_pack_runtime(style_pack)` 是同步、无 I/O 的 Core 查询。它读取当前偏好和已启用
词典,使用与生产润色路径相同的 prompt composer,返回 `StylePackRuntimeDiagnostics`(包括
单轮/多轮 prompt、上下文 premise、热词块及字符数)。宿主只渲染返回 DTO;不得在 Tauri 或
egui 中重新拼接 prompt、过滤热词或推导字符数。
`AudioRecorder`、`TextPolisher`、`TextInserter`、`CredentialStore`、`SettingsRuntime`、`TaskSpawner`、`EditObservationSink`/`EditObservationAdapter`(默认 `NoopEditObservationAdapter`)、`LinuxHostActions`。缺省实现表示"未接线",不是"不支持"。
### 3.3 `ProviderApi`
## 4. 与桌面共享
`ProviderApi::validate` 与 `list_models` 由 Core `ProviderService` 统一实现。Tauri
`commands/providers.rs` 只负责 `kind` 字符串解析、请求构造和旧错误字符串转换;Linux
生产入口 `LinuxBackendBuilder::from_shared_providers` 注入同一 service,egui 不读取凭据或
构造 HTTP/WS client。
- 云 ASR/LLM/Omni/Auxiliary 实现两端共用(Core `asr/`、`provider_*`、`omni`、`llm_gemini`);平台 Host 只注入原生录音、凭据、窗口、进程、焦点与插入 Adapter。
- 旧 React command/event 名称只保留在 Tauri 兼容 Adapter;Linux 与 Core 同进程,经类型化 Rust Interface 调用。
- provider 公开目录:Core `provider_rules::provider_descriptors` → 生成 `src/lib/ipc/provider-descriptors.generated.json`(`cargo run --locked -p openless-core --example export_provider_descriptors`)。
- `ProviderRequest { kind, channel_id }`:`channel_id = None` 使用该类别的 active provider;显式
channel 必须存在于 metadata。Omni 不接受 channel id,返回 `InvalidArgument`。
- channel 的 `provider_type` 来自 credential metadata,不能用 channel id 猜协议;凭据通过
`CredentialKey { namespace, provider_id, account }` 读取,A/B channel 不会串线。
- `validate` 使用与正式 ASR/LLM/Omni 调用相同的 Core provider 构造路径;静音探活不保存用户音频。
- `list_models` 对 Codex、Bailian、Qwen、Mimo、ElevenLabs 等无远端列表接口返回 Core 静态清单;
OpenAI-compatible 与 Gemini 使用受大小上限约束的远端列表响应,并校验 JSON schema。
- 参数/模型缺失返回 `InvalidArgument`,凭据或网络/HTTP/WS 失败返回 `Provider`,超时/连接失败
标记 `retryable`,取消返回 `Cancelled`,native/未知 provider 返回 `Unsupported`。
- 模型响应最大 2 MiB;状态码只以 `providerHttpStatus:<code>` 形式返回。API key、token、Authorization、
endpoint credential 和响应 body 不进入 DTO、错误详情、日志或 `Debug`。
## 5. 行为约定(合同级)
Core、Tauri wire 和 Linux factory contract 覆盖 channel 隔离、Omni 拒绝、静态/远端模型解析、
错误脱敏和非 `Unsupported` 生产接线。未注入的其他领域仍稳定返回 `Unsupported`。
当前 provider 管理面已可供 egui 调用。模型列表请求通过 Core `ProviderTransport` seam;生产实现使用
无 redirect、显式 timeout 和 2 MiB 响应上限,`FakeProviderTransport` 已覆盖 HTTP 状态、超时、取消、
无效 JSON、响应过大和 redirect 拒绝,并验证秘密不会出现在请求 `Debug`、错误或 fixture 输出。Core
静态模型清单已按迁移前 Tauri 顺序完成 provider parity 与去重测试;LLM extra headers/temperature
写入已按显式 `provider_id` 定位并有 A/B channel 回归测试。上述 Interface 收口升级为 2.0.0 公共调用面,
但真实 provider 网络、keyring/Secret Service、取消/超时和各平台 runner 仍需在 M9/M10 留下独立证据,
不能用 fake、WSL 或本地 parser/unit 测试代替。
Less Computer 文字入口可直接提交;语音/物理热键入口必须先建立 Core capture lease,
再把同一个 session id 贯穿宿主录音、取消和提交:
```rust
let result = backend.submit_less_computer("列出当前目录的文件".to_string()).await?;
let session_id = SessionId::new();
backend.begin_less_computer_capture(session_id)?;
// Host starts its recorder/native ASR. Startup failure must release the lease:
// backend.abort_less_computer_capture(session_id)?;
// 录音热键完成转写后,使用同一 session 以便 Esc/release 精确取消:
let result = backend
.submit_less_computer_with_session(session_id, transcript)
.await?;
// Esc/cancel must signal Core before the host drops recorder/ASR resources.
backend.cancel_less_computer(Some(session_id)).await?;
// If submission has not promoted the lease to a run, release the capture lease:
backend.abort_less_computer_capture(session_id)?;
```
两个 submit 方法都只接受用户文本;provider、executable、model、permission mode、workdir、
autonomous prompt、命令护栏、审批重跑和 dsh continuation 均由 Core 从 preferences 与实例状态
解析。`begin_less_computer_capture` 是实例级、session-scoped 的互斥 lease;重复 reserve 返回
`Busy`,`less_computer_active_session` 可用于重连/诊断,`less_computer_capture_cancelled` 在
宿主释放 capture lease 前保持可见。`abort_less_computer_capture` 只释放仍处于 capture 阶段的
lease,对已提升为 Agent run 的 session 是幂等 no-op。`LessComputerRuntimeAdapter` 只负责宿主
进程/Git/临时文件/stream transport;未注入 runtime 时返回 `Unsupported`。同一实例同时提交
第二轮返回 `Busy`,不能覆盖当前运行。
返回的 `LessComputerRunResult` 只表达该轮唯一终态:
`Completed { text, cost_usd }`、`Failed { message }` 或 `Cancelled`。UI 不应根据 delta、窗口关闭
或进程退出自行猜测终态;必须等待对应的 `LessComputerEvent`。
Linux 生产 factory 会注入 Core `MarketplaceApi`。需要把归档保存到用户文件系统时,UI 调用
`LinuxHost::download_marketplace_archive(pack_id, target)`:Core 负责 HTTP/OAuth/大小限制与 ZIP
校验,Linux host 只接受已有父目录下的绝对路径,以 create-new 方式写入并拒绝覆盖;写入或
`sync_all` 失败时删除不完整文件。UI 不处理 bearer token,也不自行重复校验归档。
### 3.3 `AuxiliaryApi`
`AuxiliaryApi` 是“重新润色”和“从既有录音重新转写”的共享 use-case,不拥有文件选择、
WAV 解码、窗口或进度 UI:宿主先把输入转换为契约数据,再调用 Core。
- `repolish(RepolishRequest)` 接收 owned `raw_text`、可选 `style_pack_id` 和可选
`front_app`。Core 在调用开始时冻结 preferences、指定或当前 style pack、启用词典以及
LLM/Omni provider;它不解析 ASR provider。该方法只执行一轮,不写 history/activity、
不插入文本、不改变 active style pack,也不发流式 delta。当前模式不使用 polisher 时,
原文原样返回。
- `retranscribe_pcm(Vec<u8>)` 只接受非空、偶数字节的 16 kHz mono signed Int16
little-endian PCM;文件读取、WAV header 去除和格式转换归宿主。Core 只冻结 ASR provider,
不要求 LLM/Omni 凭据,且把全部 PCM 精确送入一个 `TranscriptionSession` 后只 finalize 一次。
成功返回 `RetranscriptionResult { text, duration_ms, asr }`;`asr` 是 Adapter 实际使用的
provider/model(包含默认模型解析后的值),不是 UI 提交值。
- 失败返回 `RetranscriptionFailure { error, attempted_asr }`。ASR session 尚未建立时
`attempted_asr` 可为 `None`;建立后必须携带实际归因。Foundry 的 terminal fallback 使用
`details.terminal = "foundry_fallback"`,`is_terminal() == true` 且 `retryable == false`;
其他 transcription 启动/finalize 失败统一标为可重试。
- 如果调用方 future 在 finalize 完成前被丢弃,Core 的取消 guard 通过宿主注入的
`TaskSpawner` 调用该 session 的幂等 `cancel()`;宿主不能对同一 PCM 自动启动第二次转写。
Linux 生产 factory 已注入共享 Auxiliary polisher 与 transcription router;它与 Tauri 使用同一套
provider 选择、凭据路由、默认值、取消和错误语义。native/local ASR 仍需 Linux 对应 runtime;
未注册的 native/local provider 稳定返回 `Unsupported`,不能把 headless fixture 当作真实能力。
### 3.4 `SelectionVoiceApi`
Selection Voice 的业务入口是高层 use-case,不是让宿主拼装 prompt 或 EditPlan 的工具箱:
- 宿主完成平台录音/ASR 后,先调用 `mark_processing(session_id)`,再把未加工的 transcript 交给
`process_transcript(session_id, transcript)`。Core 依次执行 correction rules、指令润色、按冻结偏好
选择 manual/heuristic/prompt/auto intent;auto 模型失败时只由 Core 回退 heuristic。宿主不得预先
润色、分类或传入模型分类结果。
- `SelectionVoiceDisposition::Question` 只要求宿主打开 QA surface、提交 Core 返回的 instruction,
成功后调用 `complete`;`AwaitingIntent` 只要求显示 prompt 并把用户选择交给 `confirm_intent`。
- `SelectionVoiceDisposition::Edit` 后调用 `prepare_edit(session_id, owner)`。Core 根据
`selection_polish_output_mode` 返回 `SelectionVoiceEditAction::OpenConversation`,或在内部完成
translation target 推断、provider 调用、EditPlan 解析/应用后返回 `ReadyToApply { preview }`。
- QA 编辑模式调用 `edit_preview(SelectionVoiceEditRequest)`。首次调用建立或复用匹配的 Core session;
后续调用以当前 preview 为 draft,只保留一步 revert。`replaced_existing` 是 QA 按钮状态的唯一真相,
`answer_text()` 是稳定的 assistant message 投影;Adapter 不应重复格式化 summary。
- 真正替换文本仍是双阶段握手:Core `begin_preview_apply` 返回 ticket,平台 Adapter 用 opaque target
校验/插入,再以 `finish_preview_apply(ticket_id, outcome)` 回报。只有 `Inserted` 或
`CopiedFallback` 才会消费 preview 并写 history/activity;`Failed` 保留 preview。宿主无法确认
插入结果时必须返回错误,且不得自动重试。
`resolve_instruction`、`set_preview` 和 `replace_preview` 是 compatibility/headless 测试原语,不是新 UI
工作流入口。平台仍拥有麦克风、ASR native handle、窗口/热键、焦点恢复和 opaque insertion target;
correction、prompt、intent、EditPlan、translation 和 output-mode 判断不得进入 Tauri 或 egui。
### 3.5 设置事务
egui view model 必须从同一份快照取得偏好和 revision,再通过 `LinuxHost` 提交完整文档:
```rust
let snapshot = host.snapshot();
let outcome = host.update_settings_strict(
preferences,
snapshot.preferences_revision,
)?;
```
公开入口只有两种产品语义:
| 方法 | 冲突策略 | style 字段 | 适用场景 |
| --- | --- | --- | --- |
| `save_settings(preferences, revision)` | `Reconcile`:按 Core 固定优先级恢复旧值或停用低优先级键 | 保留当前值,防止陈旧整表覆盖刚发生的 style 切换 | 设置页整表保存 |
| `update_settings_strict(preferences, revision)` | `Reject`:任何冲突返回 `InvalidArgument` | 使用提交值 | 单项快捷键或明确的聚焦更新 |
Core 在单写入 gate 内完成 legacy 字段同步、style-pack 对齐、冲突校验/协调和 typed effect plan,
再按 `prepare -> commit effects -> persist once -> publish once` 执行。Linux Adapter 只消费计划中的
显式目标,不读取或修改 `UserPreferences`;失败时按 typed receipt 逆序恢复已应用的 fcitx5/凭据
副作用。成功只增加一次 `preferences_revision` 并发布一次 `PreferencesChanged`。
revision 不匹配时返回 `BackendErrorCode::Busy`、`retryable = true`,`details` 包含
`expectedPreferencesRevision` 和 `actualPreferencesRevision`。UI 必须重新读取 snapshot/偏好、合并
用户仍想保留的编辑后再提交;不得无条件重放陈旧整表。
Linux settings Adapter 当前支持 dictation、QA、Selection Polish、translation、Coding Agent 的 fcitx5 热键和
active ASR provider metadata;启动时同步保存的热键,禁用 Coding Agent 时解绑其语音键。修改 switch-style、open-app、style-pack hotkey 或
Windows keyboard effect 会稳定返回 `Unsupported`,且不写偏好、不增加 revision、不发布事件。
UI 不得直接调用 `OpenLessBackend::set_preferences*`;这些低层兼容方法不属于 Linux UI Interface,
也不能绕过 `LinuxHost` 的 revision、冲突和补偿契约。
## 4. DTO 字段契约
### 4.1 `DictationStateSnapshot`
| 字段 | 类型 | 规则 |
| --- | --- | --- |
| `phase` | `DictationPhase` | `idle/starting/recording/transcribing/polishing/inserting/completed/cancelled/failed` |
| `sessionId` | `SessionId?` | 非 `idle` 时存在;用于丢弃晚到结果 |
| `elapsedMs` | `u64` | 毫秒,不在 UI 侧换算成秒后再回写 |
| `level` | `f32` | 规范化到 `0..=1`;无音频时为 `0` |
| `message` | `String?` | 非敏感、用户可读提示;不可放 token/PIN |
| `translationActive` | `bool` | 会话开始时冻结;用于两个宿主显示当前会话的翻译状态 |
阶段是后端事实,不由 UI 猜测:
| phase | 含义 | egui 操作规则 |
| --- | --- | --- |
| `idle` | 无活动 session | 允许开始 |
| `starting` | engine/录音资源正在启动 | 显示准备态;允许取消,不允许重复开始/停止 |
| `recording` | 正在采集音频 | 允许停止或取消 |
| `transcribing` | ASR 正在处理/输出增量 | 禁用重复停止;可显示 `TranscriptDelta` |
| `polishing` | LLM/规则润色正在处理 | 可显示 `PolishDelta` |
| `inserting` | 已提交文字插入请求 | 不自动重试;等待明确/fallback/unknown 结果 |
| `completed` | 本 session 唯一成功终态 | 读取 `DictationResult` 后清理本地 session |
| `cancelled` | 用户、关闭或 generation guard 取消 | 丢弃该 session 后续增量 |
| `failed` | 本 session 失败 | 按 `BackendErrorCode` 提供重试或用户操作 |
### 4.2 `DictationResult`
`sessionId`、`rawText`、`polishedText` 和 `inserted` 均为 owned 字段。公开 Rust 类型
`DictationInsertStatus` 是 `InsertStatus` 的稳定契约别名;`inserted` 的 serde 值为
`inserted`、`copiedFallback` 或 `unknown`。`unknown` 表示插入请求超时后结果不可证明,
UI 必须显示待确认状态,不得自动重试以免重复输入。
### 4.3 能力与秘密
`PlatformCapabilities` 只描述能力布尔值和平台标识。`CredentialsStatus` 只包含
已配置的 provider id;凭据值、Authorization header、PIN 和完整请求永不进入
快照、事件、错误 details 或日志。
`PreferencesChanged` 事件只携带单调递增的 `revision`,UI 收到后重新调用设置
查询;不要把任意 JSON 或秘密塞进事件 payload。
所有 DTO 使用 Rust 的 `serde` 定义;与 React IPC 的兼容字段由 Tauri adapter
显式转换,不能让 egui 依赖 React 字段名。
### 4.4 `QaSnapshot` 与 Remote Input
`QaSnapshot` 是 QA view model 的唯一状态来源:
| 字段 | 类型 | 规则 |
| --- | --- | --- |
| `phase` | `QaPhase` | `idle/recording/thinking/awaiting_approval/completed/cancelled/failed` |
| `sessionId` | `SessionId?` | 每轮 turn 的 generation token;每个成功 follow-up turn 都分配新 ID,所有 progress、cancel 和迟到结果 guard 均绑定此 ID |
| `conversationId` | `SessionId?` | 同一面板成功多轮间稳定的 Selection Voice preview owner;仅在 dismiss/clear 后清空,不用于接受上一轮迟到结果 |
| `messages` | `QaMessage[]` | 有序 user/assistant message log;UI 不自行补写 provider 结果 |
| `editInstructionMode` | `bool` | 只能在非活动 turn 修改;活动 turn 修改返回 `Busy` |
| `pendingApprovalToken` | `String?` | 只用于显示和提交明确审批;不得当作跨 session 全局 token |
| `lastError` | `String?` | 已脱敏的公开错误;不得包含 provider body、Authorization 或选择全文 |
`QaLevel` 规范化为 `0..=1`;`AnswerDelta` 只对匹配当前 `sessionId` 且处于
`thinking/awaiting_approval` 的 turn 有效。成功 turn 保留 `conversationId`,`sessionId` 继续标识
该轮终态;下一轮开始时必须替换为新的 generation token,因此上一轮迟到 delta 不能污染 follow-up。
`dismiss()` 幂等地取消活动 runtime、清空 snapshot、清理与 `conversationId` 匹配的 Selection
Voice preview 并请求宿主隐藏面板;窗口 focus、NSPanel 和键盘仲裁不是 `QaSnapshot` 字段。
`LessComputerEvent` 是 Coding Agent 对话的唯一 UI 事件源:
| `kind` | 字段 | 规则 |
| --- | --- | --- |
| `voice_state` | `sessionId`, `phase`, `level`, `elapsedMs` | Core语音快照;phase为`starting/recording/transcribing/idle`,按原`seq`去重;旧session终态不得覆盖新录音 |
| `user` | `text`, `fresh` | Core 接受输入后发布;`fresh=true` 表示 dismiss 后的新会话 |
| `started` | — | runtime 已启动 |
| `delta` | `text` | 增量输出,按事件 `seq` 去重后追加 |
| `tool` | `name` | 仅展示工具活动;UI 不执行工具 |
| `compaction` | — | provider 正在压缩上下文,可作为非阻塞提示 |
| `approval` | `token`, `command`, `reason` | 只展示脱敏 command/reason,并把 token 原样回传 `approve`;不得写日志/持久化 |
| `completed` | `text`, `costUsd?` | 唯一成功终态 |
| `error` | `message` | 唯一失败终态,message 已脱敏 |
| `cancelled` | — | 唯一取消终态 |
Core 维护实例级 conversation flag、最多两轮 dsh continuation、approval token registry 和
90 秒 approval timeout。`dismiss()` 会取消当前 runtime、拒绝所有 pending approval、清空
continuation,并使下一轮 `user.fresh=true`。egui 只保存渲染所需的派生消息,不复制上述状态机。
语音显示还可从`backend.event_publisher().latest_less_computer_voice_state()`读取最后一条有效投影,保留原session/seq且占用固定一条空间。Tauri既有`less_computer_sync`返回可选`voiceState`,即使长转写的阶段事件已被2048条replay驱逐,重开也能恢复;该投影不推进聊天事件水位。合同版本仍为`2.0.0`,没有新增IPC入口。
`RemoteInputStatus` 字段和规则如下:
| 字段 | 类型 | 规则 |
| --- | --- | --- |
| `enabled` / `running` | `bool` | 前者是期望配置,后者是真实 transport 状态;两者不能互相替代 |
| `port` | `u16` | `1..=65535`;端口变化由 `configure` 串行 stop/restart |
| `urls` | `String[]` | 仅在 transport 成功绑定后存在 |
| `locale` | `String` | 仅接受 `zh-CN/zh-TW/en/ja/ko` |
| `connectionCount` | `usize` | 当前认证连接数,不包含已断开的历史连接 |
| `activeSessionId` | `SessionId?` | 任一连接正在推流时存在;仅作状态展示,不代替 connection/session 校验 |
pairing PIN 只能通过 `read_pairing_pin()` 的 `SecretValue` 显式读取;不得加入
`RemoteInputStatus`、事件、错误、`Debug` 或普通日志。PCM frame 必须为非空、偶数字节、最多
65536 bytes 的 signed Int16 little-endian;音频格式固定 16 kHz mono。每个连接最多一个活动
stream,重复 start 返回 `Busy` 且不得覆盖原 session lease。stop/cancel/disconnect 后的 frame
返回 `Cancelled`,宿主不得自动新建 session 重放。transport restart 必须先取消旧 stream、
使旧 connection/session lease 失效;旧 lease 再次 start/feed/stop 时稳定返回 `Cancelled`。
## 5. 事件契约
M0 的可机器读取基线位于
[`linux-egui-command-event-baseline.json`](./linux-egui-command-event-baseline.json)。该文件
记录从当前 Tauri handler、React `listen()` 和 Rust `emit*()` 调用点观察到的 command/event
名称;迁移期间新增或删除名称必须先更新该基线,再更新对应 adapter 和兼容测试。
`BackendEvent` 包含 `sequence: u64`、可选 `session_id` 和 `BackendEventKind`。
每个 backend 实例的 sequence 从 1 开始严格递增;事件总线是通知而不是状态
真相。backend 同时保留最近 2048 条实例级 backlog;`replay_events_after(sequence)` 返回
`events/oldestSequence/latestSequence/truncated`。`truncated=true` 表示调用方的游标早于当前
backlog,必须先重新读取 facade/领域 snapshot,再从最新 sequence 续订。
Less Computer 的 Tauri compatibility UI 使用同一 replay 水位:mount 时先安装实时 listener,
再以 `afterSequence` 拉取 replay;同步窗口内到达的事件先进入 pending 队列,随后按
`replay.events -> pending` 顺序合并。带 `seq` 的事件按最大已应用 sequence 去重,无 `seq` 的
legacy fallback 仍需保留。`truncated=true` 时旧派生时间线不再可信,调用方先清空本地时间线,
把水位重置为 `oldestSequence - 1`,从本次保留的 replay 重新构建,再接续 pending;最终水位
至少推进到 `latestSequence`。egui 不需要复用 React helper,但必须实现相同的顺序与去重语义。
| 事件 | UI 处理 |
| --- | --- |
| `BackendStarted` / `BackendStopping` | 更新宿主生命周期状态 |
| `DictationStateChanged` | 替换录音/处理/终态和 level |
| `TranscriptDelta` | 按 session 和 offset 合并原文增量 |
| `PolishDelta` | 按 session 和 offset 合并润色增量 |
| `DictationCompleted` | 展示结果并根据 `inserted` 显示确认/回退提示 |
| `InsertFallback` | 显示 clipboard/fallback 状态,不重试插入 |
| `PreferencesChanged` | 使设置缓存失效并重新读取 |
| `CredentialsChanged` | 更新 provider 配置状态,不显示秘密 |
| `HistoryChanged` / `VocabularyChanged` / `StylePacksChanged` | 使列表缓存失效 |
| `DownloadProgress` | 更新模型下载进度 |
| `PermissionChanged` / `HotkeyStatusChanged` | 更新能力和降级文案 |
| `Notification` | 放入非阻塞通知队列 |
| `CodingAgentTest` | 按 `session_id` 消费 started/delta/tool/completed/error/cancelled 流 |
| `LessComputerEvent` | 按 payload `seq` 构建会话时间线;fresh user 清空旧会话 |
| `LocalAsrPrepareProgress` / `LocalAsrDownloadProgress` | 按 runtime/model 更新本地模型准备与下载状态 |
| `LocalAsrEngineChanged` | 替换当前本地 ASR runtime 快照 |
| `MicrophoneDevicesChanged` | 使设备列表失效并重新调用 `PlatformApi::microphone_devices()` |
| `QaLevel` / `QaState` | 按 QA session 更新录音电平、阶段、增量和消息列表 |
| `RemoteInputStatusChanged` / `RemoteInputFailed` | 更新远程输入状态/错误;事件永不携带 pairing PIN |
| `VocabularySuggestionsChanged` | 替换当前待确认纠正建议 |
当前 29 个 `BackendEventKind` 已由机器基线和 serde fixture 校验。30 个旧 Tauri event 均已
分类;12 个原 `migrationRequired` 事件现已通过 typed core event 进入同一事件总线,再由
`tauri_events.rs` 映射为旧 React 名称。复杂领域后续迁移只能替换事件生产者的
Implementation,不能重新建立 host-only 状态流。
订阅可能返回 `EventRecvError::Lagged(n)`。收到该错误时,UI 必须丢弃本地增量,
重新读取 `snapshot()` 或对应领域查询,然后继续订阅;不能静默使用旧状态。
`Closed` 表示 backend 已被销毁。终态事件只发布一次,旧 session 的任何晚到
事件都必须被 session id guard 丢弃。
egui frame 不得 `.block_on()`。`EventSubscription` 同时提供异步 `recv()` 和非阻塞
`try_recv()`;frame 只应使用后者:
```text
后台订阅任务 -> 有界 UI channel -> frame 每次非阻塞 drain
-> 更新 view model
-> request_repaint()
```
最小的非阻塞循环如下(`Empty` 只表示本帧没有更多事件,`Lagged` 必须触发快照重同步):
```rust
loop {
match events.try_recv() {
Ok(event) => view_model.apply(event),
Err(EventRecvError::Empty) => break,
Err(EventRecvError::Lagged(_)) => {
view_model.replace_from_snapshot(backend.snapshot());
break;
}
Err(EventRecvError::Closed) => view_model.mark_backend_closed(),
}
}
```
`openless-linux-egui` 另提供 `drain_events()`,把上述循环收敛成
`EventDrainOutcome::{Idle, Lagged, Closed}`;它不引用 egui 类型,因此 UI 组可在
`eframe::App::update` 中直接调用,只有 `Lagged` 时才回读 `LinuxHost::snapshot()`。
## 6. 错误与取消
核心返回 `BackendError { code, message, retryable, details }`。UI 只判断 code,
不解析 message。
| code | 典型情况 | UI 行为 |
| --- | --- | --- |
| `invalid_argument` | 参数或 session id 不合法 | 修正输入;不重试原请求 |
| `invalid_state` | 未启动或无活动 session | 重新读取 snapshot |
| `busy` | 已有活动 session | 禁用重复开始 |
| `cancelled` | 用户取消、关闭或 session 过期 | 清理本地草稿 |
| `permission_denied` | 麦克风/辅助功能被拒绝 | 提供系统设置入口 |
| `unsupported` | fcitx5、托盘或平台能力不存在 | 显示降级,不假装成功 |
| `provider` | ASR/LLM 请求失败 | 依据 `retryable` 提供重试 |
| `persistence` | 读写数据失败 | 保留当前页面状态并提示 |
| `platform` | host adapter 失败 | 显示平台诊断 |
| `internal` | 未分类内部错误 | 显示通用错误并记录 request id(若有) |
取消必须绑定 session。取消与停止并发时,停止路径在调用 inserter 前再次检查
session/phase;已取消 session 不得产生插入副作用。shutdown 对活动 session 发送
`Cancelled` 状态,并等待 host 侧录音/热键任务退出。
Windows TSF Adapter 把失败分成两类:连接/准备阶段的 definite failure 可以按冻结的策略
尝试 SendInput/clipboard fallback;请求写入 pipe 后的超时、断连或无法判定的响应属于
outcome-unknown,返回明确的 `BackendError` 且不得再次插入。Core 的公开结果只包含
`Inserted`、`CopiedFallback` 或错误;这样即使 TSF 提交迟到,也不会与 fallback 形成重复文本。
`DictationEngine::finish` 返回 `Result<EngineResult, EngineFailure>`。`EngineFailure`
除 `BackendError` 外,还携带 `EngineFailureStage::{Transcribing, Polishing}`、可选原文、
录音时长、ASR/润色实测耗时和实际录音归档状态。facade 据此统一写失败历史:
- ASR 启动、录音停止或 ASR finalize 失败使用 `transcribeFailed`;
- ASR 返回空白文本使用 `emptyTranscript`;
- 禁止回退的润色失败使用 `polishFailed`,并保留已产生的原文;
- 插入失败使用 `insertFailed`,并保留原文、最终文本和 `polishSource`;
- 失败不增加 activity,Failed 事件仍携带原 session id,资源释放后同步 snapshot 回到 Idle。
## 7. Host ports
`BackendDependencies` 由宿主注入,核心不在方法内部创建系统对象:
- `TaskSpawner`:后台任务执行器;Linux 可使用 Tokio,测试可使用确定性执行器。
- `DictationEngine`:完整的 `start(session, progress) -> finish(session, progress) ->
cancel(session)` 录音、ASR/润色生命周期;`EngineProgressSink` 使用
`RecordingLevel { elapsed_ms, level }`、`EngineStage`、`TranscriptDelta` 和 `PolishDelta`
回报进度;`cancel` 也会在 backend shutdown 时调用,不暴露窗口或 UI 类型。迟到进度若
session 已失效会返回 `Cancelled`,adapter 必须停止发送。
- `AudioRecorder` / `ActiveRecording`:宿主采集设备音频并输出规范化的
16 kHz / mono / signed Int16 little-endian PCM;`ActiveRecording::stop(self)` 消费句柄,
保证 finish/cancel 竞争时最多释放一次。可恢复录音通过 `RecordingArchive` 精确表示,
`is_available()` 报告真实状态,`read_pcm()` 支持冻结 provider 的 silent retry,`discard()`
删除实际归档;`RecordingEvent::Fatal` 实时进入 Core controller,不得仅按 preferences 猜测
`hasAudioRecording` 或等到 stop 才报错。
- `TranscriptionEngine` / `TranscriptionSession`:在录音前建立 ASR session,持续消费 PCM,
`finish()` 返回最终原文,`cancel()` 终止 provider 请求。
- `TextPolisher`:接收最终原文并产生润色结果/可选增量;失败是否回退原文由
`PolishFailurePolicy` 统一决定,宿主不得另写一套 fallback 判断。
- `TextInserter`:fcitx5、AX、TSF 或 clipboard fallback 的统一会话接口:
`begin(session, context)` 在录音/ASR 启动前捕获 opaque target 并返回
`TextInsertionSession::{supports_streaming,write,copy,finish,cancel}`。Core `ActiveTextInsertion` 独占 streamed prefix、
Unicode tail、final divergence 与 clipboard fallback reconciliation;Host 报告已消费源 Unicode scalar 前缀长度,包含按约定吞掉的 CR,失败字符不计入。平台准备不能流式时返回 `supports_streaming=false`,仍可一次性落字。
target 无法恢复时必须返回明确 copied/error,不能向当前焦点盲写;所有终态都要恢复输入法、
剪贴板和平台资源。
- `HostActions`:`ShowDictationFeedback`、`HideDictationFeedback`、打开系统设置、
外部 URL 和通知等语义动作;不传窗口 label。
此外已经存在:
- `CredentialStore`:status、显式 secret read/write/remove、provider channel metadata;
- `ResourceResolver`:只解析相对资源路径,拒绝绝对路径与 `..` traversal;
- `TaskSpawner`:由宿主注入 runtime,core 不创建窗口线程或专用全局 runtime;
- `BackendServices`:复杂领域 Adapter 集合,缺失时使用稳定的 unsupported 实现。
- `QaRuntimeAdapter`:捕获 selection host context、持有 recorder/ASR/LLM/Coding Agent 资源并执行
`prepare_text/start_recording/finish_recording/answer/cancel`;Core 拥有 session、phase、messages
和迟到结果 guard,Adapter 不得复制这些状态。Tauri 与 Linux production factory 均注入真实
runtime;编辑/回答路由继续由 `QaService` 决定,Adapter 只执行上下文、录音和 provider effect。
- `RemoteInputRuntimeAdapter`:PIN secret persistence、TLS/socket/WSS/H5、local IP 与共享听写桥接;
Core 拥有配置、连接/session 关联、PCM 校验和 transport 生命周期规则。
- `TranscriptionRouter` / `TextPolisherRouter` / `DictationEngineRouter`:分别按会话快照中的
ASR、LLM 与 traditional/Omni 选择固定 Adapter;`provider_id` 是 channel/scoped credential
标识,`provider_type` 才是协议路由 key,二者不能混用;ID、type、model 在 session 开始时
一次冻结,运行中切换 active channel 或更新注册项只影响下一会话,缺失 provider 返回
`Unsupported`。重复 session 必须原子返回 `Busy`,不得覆盖原 Adapter 或 cancellation route。
- `SharedCloudTranscriptionEngine` / `SharedCloudTextPolisher` /
`SharedAuxiliaryTextPolisher` / `SharedOmniDictationEngine`:Core 生产 Implementation,负责
credential account、默认 endpoint/model、协议选择、extra headers/temperature 校验、流式输出、
取消和 session 占用;宿主注入 `CredentialStore`、`AudioRecorder` 和 `TaskSpawner`,UI 不接触
这些细节。实时 ASR provider 的发送、接收和关闭任务必须使用该 `TaskSpawner`,core 不得创建
私有 Tokio runtime。
Omni 的 API key、endpoint、model、extra headers 和 temperature 必须使用
`CredentialKey.providerId == DictationContext.omni.providerId` 读取;宿主不得在读取期间改写或借用
active provider。活动 provider 切换只影响下一会话,任何公开错误都不得包含旧、新 provider secret。
`PipelineDictationEngine` 固定执行顺序为:启动 ASR session → 启动录音并推送 PCM/level →
停止录音 → finalize ASR → 按会话归档策略处理成功录音 → 发布最终原文 delta → 润色 →
发布最终润色 delta → 返回结果。ASR 失败和空转写保留可恢复录音;非空 ASR 成功且
`recordAudioForDebug == false` 时请求 Adapter 删除归档;删除失败时继续报告真实的
`hasAudioRecording == true`,不能产生“历史显示无录音但文件仍在”的假状态。
Facade 在 session capture 时冻结启用的 correction rules;Pipeline 在 ASR 成功后、任何
Less Computer/polisher 调用前先应用,并只在实际变化时把规则前文本写入 `asrTranscript`。
非流式最终文本在插入前按同一规则收口;已流式落字的路径不得事后改写 history 制造屏幕/记录
不一致。禁用或格式无效的规则不生效,读取失败只记录非敏感 warning,不丢整段听写。
History 的 provider 归因也由 facade 统一完成:traditional 流程从冻结 context 记录 ASR
channel/model,并仅在实际使用 LLM 时记录 LLM channel/model;multimodal 流程的 ASR 字段和
`asrMs` 为 `None`,LLM 字段记录冻结的 Omni channel/model,`polishMs` 保留 Omni 调用耗时。
成功和失败记录遵循同一规则,宿主不得自行重写归因。
这些细分 ports 只用于宿主组装和测试注入,不是 UI use-case;egui view model 只调用 facade。
热键状态通过 `PlatformApi` 查询;Host 只发送携带同一 `press_id` 和单调时间的
pressed/released/combined 边沿。Core `HotkeyInterpreter` 统一解释 Toggle/Hold/Auto、modifier
grace、250ms debounce 和 450ms terminal cooldown;组合键在 start await 前后均能取消同一代次。
Host 不得再保存 cooldown、began-session 或重复的 mode policy。
## 8. Linux 非 UI Adapter 契约
`openless-linux-egui` 已交付以下宿主能力,不包含 `eframe::App`:
- `LinuxCredentialStore`:secret value 只写 Linux Secret Service/keyring;
`credential-metadata.json` 仅保存 channel、active provider 与已配置 key 标识,并原子替换;
删除按namespace+channel清理全部secret,失败保留可重试元数据;读操作不暴露尚未提交索引的孤立secret。
只有Host提供`BackendConfig.home_dir`时才尝试导入旧`com.openless.app` vault/分片与该目录下的旧JSON;Core解析旧格式,Host执行幂等写入,新配置优先,全部成功才写标记,保留旧来源。None配置不隐式访问系统旧凭据或真实HOME。
- `LinuxResourceLayout` / `LinuxResourceResolver`:分别定义 development、AppImage、deb、rpm
的资源根与 fcitx5 插件相对路径;
- fcitx5 Adapter:availability、DBus commit、selection read、hotkey sync、clipboard fallback;
普通听写使用 `CaptureDictationTarget(s: session) -> b`、`CommitDictationTarget(ss: session, text) -> b`、
`CancelDictationTarget(s: session) -> b` 冻结并释放原输入上下文;新目标不能替换活动 session 的原目标。
Selection 同时冻结原上下文及 surrounding text/cursor/anchor;取消选中、移动光标或修改文本后拒绝替换,全局 PRIMARY 不能证明原选区仍存在。
`CommitText(s: text) -> b` 的 `true` 才表示文字已提交到输入上下文,`false` 表示当前没有
可用焦点输入上下文(例如启动或无焦点/headless 场景)。Rust Adapter 必须把 `false` 转为
明确的插入失败/平台错误并按策略决定 clipboard fallback,不能向 Core 或 UI 报告假成功;
DBus 调用本身也不得因 no-context 让 fcitx5 进程崩溃。AppImage 可从版本化资源同步用户插件,
deb/rpm 只验证系统安装,不能覆盖用户文件;
- `LinuxCapabilitySnapshot`:明确区分 X11、Wayland 与 headless,以及 tray、overlay、fcitx5、
updater 和麦克风能力;未知权限返回 `Unknown`/`Unsupported`,不伪造 granted;
- `LinuxHostActions`:线程安全队列、非阻塞 drain 与可选 wake/repaint callback;
- `LinuxSettingsRuntime`:只执行 Core `SettingsEffectPlan` 的显式目标,通过 fcitx5 DBus 同步
dictation/QA/Selection Polish/translation/Coding Agent,并通过 Linux credential metadata 同步 active ASR
provider;不支持能力返回稳定 `Unsupported`,失败按 receipt 逆序恢复;
- `LinuxCpalRecorder`:选择偏好设备或默认输入设备,在专用线程持有 cpal stream,把常见
sample format 下混、重采样和量化为 core PCM 契约,并报告 `0..=1` level;runtime fault
实时进入 Core recording controller,不等待用户 stop。
- `Fcitx5HotkeyListener`:监听 dictation press/release/combined、QA、selection polish 和
translation signals,提供非阻塞 `drain()`、`take_error()` 和可停止/join 生命周期;
selection 信号调用共享 `SelectionApi`,空闲态先到达的 translation 信号会固定到下一次
dictation press 的会话快照,活动 session 不会被中途改写。
- `SingleInstanceBroker`:私有 Unix socket + process lock;第二实例把 typed launch intent 转发
给 primary 并等待 acknowledgement,primary 非阻塞 drain 后通过 `LinuxHost` 调用 core。
- `LinuxBackendBuilder::from_shared_providers(config)`:唯一生产 factory,组装 Core 共享云
ASR/LLM/Omni/Auxiliary router、Marketplace、QA、Remote Input、Selection、打包 Qwen runtime、
传统 `PipelineDictationEngine`、recorder、inserter、credentials、platform services、host actions
与 settings runtime,返回不包含 egui 类型的
`LinuxBackendRuntime`;`new(...)` 只用于测试/特殊宿主。
- `LinuxHost::download_marketplace_archive`:保存 Core 已校验归档;只接受绝对 filesystem path,
不创建缺失父目录、不覆盖已有文件,失败时不遗留部分文件。
`linux-egui/src/main.rs` 已使用 `eframe::run_native` 接入 `LinuxHost`、Core event、fcitx5
hotkey 与 Single Instance Adapter;UI 不读取 Core 私有模块,也不复制业务规则。
## 9. 测试夹具
`openless_core::testing` 提供:
- `RecordingHostActions`:记录 host action 顺序;
- `FixtureAudioRecorder`:推送固定 PCM/level,并记录 stop 次数;
- `FixtureTranscriptionEngine` / `FixtureTextPolisher`:固定 ASR/润色结果、错误和取消行为;
- `FixtureDictationEngine::successful/failing`:在不测试细分 Pipeline 时提供固定结果;
- `FixtureTextInserter::with_outcome/failing`:覆盖 inserted、fallback、unknown 和失败,并通过
`actions()` 暴露 prepare/insert/cancel 的 session-scoped 调用顺序。
- `FixtureSelectionRuntime`:记录 capture、preview、apply、revert、cancel;Linux production
Adapter 以 fcitx5 ticket 实现可见 preview、confirm/cancel/revert 与 stale guard。
- `RecordingRemoteInputRuntime`:不绑定 socket 的内存 transport,记录 server/audio
start/stop/cancel 次数和 PCM frame,用于验证单 connection 单 stream、restart 取消、stale
lease 与 secret-surface 契约;它不代表生产宿主具备 WSS 能力。
- `LinuxCapabilityFixture::x11_full/wayland_degraded/headless`:覆盖 X11 完整能力、
Wayland/fcitx5/托盘/权限降级以及无桌面会话;这些 fixture 只描述状态,不探测测试机。
egui view model 测试应只使用这些 fixture 和 `BackendSnapshot`/事件,不启动窗口、
麦克风、网络或真实凭据库。最低 contract test 集合:
1. 启动/关闭幂等和事件顺序;
2. 开始 → 处理 → 完成的主链路;
3. ASR 失败、插入失败、fallback、unknown;
4. 错误 session 取消不会改变活动 session;
5. 事件 lagged 后 snapshot resync;
6. 序列化 DTO 不包含秘密字段。
## 10. 能力降级矩阵
| 能力 | Linux 状态 | UI 规则 |
| --- | --- | --- |
| 全局热键 | available / unavailable | 不可用时隐藏快捷键设置或给出降级说明 |
| fcitx5 插入 | plugin missing / ready | missing 时允许 clipboard fallback,不能假成功 |
| 托盘 | available / unavailable | 不可用时保留主窗口内退出入口 |
| 悬浮反馈 | X11 / Wayland limitation | feedback 失败不升级为 ASR 失败 |
| 本地 ASR | model absent / ready | 显示下载、准备、释放状态 |
| 自动更新 | package-dependent | 不显示假更新按钮 |
| 麦克风 | granted / denied / no device | 区分权限拒绝和无设备 |
## 11. 版本与变更流程
当前代码常量为 `openless_core::BACKEND_CONTRACT_VERSION = "2.0.0"`。运行时 wire 只接受
2.0.0;1.x 兼容仅存在于 preferences、history、activity、credentials、model 和 style-pack
持久化迁移读取器中,不暴露 legacy runtime contract 常量。本文的 contract version 随破坏性
接口变更递增。新增可选 DTO 字段必须有默认
值;删除字段、改变枚举值、改变事件顺序或单位必须:
1. 更新 contract version;
2. 在计划文档的待决事项和迁移表记录影响;
3. 同时更新 Tauri mapping、Linux fixture 和示例;
4. 先让 contract tests 通过,再通知 egui 组切换。
egui 组发现缺少能力时,应提交一个只依赖 facade/DTO/event 的可复现测试;不得
读取 core 私有字段或复制内部实现。
### 11.1 从 0.1.0 迁移到 0.2.0
0.2.0 把可变偏好读取收敛为每会话 `Arc<DictationContext>`,属于有意的破坏性 Interface
变更:
- `DictationEngine::start` 新增 context;Pipeline 在 session 生命周期内持有同一快照;
- `AudioRecorder::start`、`TranscriptionEngine::start`、`TextPolisher::polish` 和
`TextInserter::insert` 都接收同一 context;
- UI 不构造 context。宿主仍调用 `OpenLessBackend::start_dictation()`,或在需要翻译时调用
`start_dictation_with_options(DictationStartOptions)`;facade 从 preferences、active style pack、
provider metadata 与 vocabulary 一次性生成快照;
- 会话开始后修改麦克风、provider、模型、语言、风格包或插入策略只影响下一会话;
- 自定义 Adapter 必须停止在执行中重新读取 preferences,并只使用传入 context。
### 11.2 从 0.2.0 迁移到 1.0.0
1.0.0 扩展了听写结果和润色结果,用于让共享 core 独立持久化完整历史:
- `TextPolisher::polish` 的成功类型从 `String` 改为 `PolishOutput`;Adapter 应把最终文本放入
`text`,组合润色加翻译时把润色后的源文放入 `source_text`,其他模式使用 `None`;
- `DictationEngine::finish` 的错误类型从 `BackendError` 改为 `EngineFailure`;Adapter 必须标注
`EngineFailureStage`,并在已经产生时保留原文、录音时长、ASR/润色耗时和归档状态;
- `EngineResult` 新增 `polish_source`、`polish_failed`、`asr_ms`、`polish_ms` 和
`has_audio_recording`;公开 `DictationResult` 新增 `polish_source` 与 `duration_ms`;
- `ActiveRecording::has_archived_recording` 被 `archive() -> Option<Arc<dyn RecordingArchive>>`
取代,使 Pipeline 能在 stop 消费录音句柄后精确保留失败录音或删除成功录音;
- `DictationResult` 的两个新增 serde 字段均有兼容默认值,因此 0.2.0 JSON fixture 仍可读取:
缺失 `polishSource` 时为 `None`,缺失 `durationMs` 时为 `0`;
- 宿主不得在 Tauri/egui Adapter 重复拆解组合翻译输出或重复写入成功 history;这些语义由
core Pipeline 与 facade 统一负责。
- `TextInserter` 现在具有 `prepare/insert/cancel` 会话生命周期,且三个方法都接收或绑定
`SessionId`;旧的只实现 `insert(context, text)` 的 Adapter 必须迁移。Core 在 engine 启动前
调用 prepare,并在启动失败、处理失败、取消和 shutdown 路径调用幂等 cancel。
- `DictationInsertionContext` 额外冻结 `windows_sendinput_newline_mode` 与
`android_insert_strategy`,平台 Adapter 不得在会话执行中重新读取偏好。
### 11.3 从 1.0.0 迁移到 2.0.0
2.0.0 将跨平台业务 Implementation 收口到 `openless-core`:
- 模型清单、Range 下载、断点索引、SHA-256、staging/ready sentinel 和旧目录迁移统一由
`ModelStore` 提供;宿主只注入模型根目录、原生 runtime 和 typed progress sink;
- Coding Agent 由 Core `CodingAgentRunner` 统一构造四种 provider 的请求、解析 stream、过滤
`session_id` 并产生唯一终态,宿主只实现进程创建、stdio、kill/wait 和临时文件;
- 文档窗口、最小差异/词汇学习和其它协议纯函数位于 Core,Tauri/Linux 仅保留 AX、窗口、
输入法、socket、keyring 等 Adapter;
- 1.x preferences/history/activity、凭据元数据、旧模型根目录/mirror/sentinel 与 style-pack
origin 字段继续按迁移规则读取,不因 contract 升级丢失。
## 12. 当前交付状态
| 交付物 | 状态 |
| --- | --- |
| `openless-core` package 和无 Tauri 依赖门禁 | 已建立 |
| facade 生命周期、听写状态机、事件 sequence | 已建立 |
| headless Linux host 示例 | 已完成,位于 `linux-egui/examples/headless_host.rs`;覆盖生命周期、数据领域、Less Computer、听写、Selection/Selection Voice、QA 与 Remote Input contract;真实 socket/窗口/设备证据另记 |
| fake host/recorder/transcription/polisher/engine/inserter/selection/remote transport | 已建立;fixture 固定完整业务状态,Linux production Adapter 另实现真实 TLS/WS、fcitx5 preview/revert 与 cpal effect |
| preferences/history/activity/vocabulary/correction/style-pack/credentials 共享实现 | 已建立;Tauri/Linux persistence Adapter 使用同一 Core mutation 与 active policy |
| Linux validated settings Interface | 已建立;`save_settings`/`update_settings_strict` 强制携带 snapshot revision,Core 统一校验、协调、持久化、事件和补偿,Linux Adapter 只消费显式 target |
| 全部复杂领域 DTO/Interface 与 unsupported 语义 | 已建立,位于 `domains.rs` / `BackendServices` |
| 2.0 公共 re-export 边界 | 已冻结;`openless-core`/`openless-linux-egui` 只公开 facade/DTO/event/host Interface/fixture,repository 与内部状态机不属于 UI 契约;`check-linux-public-surface.ps1` 防止边界回退 |
| Tauri command/event 完整迁移 | Core 业务路径已收口;React/CLI/Android JNI/Remote Input/桌面听写使用 2.0 contract,Tauri 仅保留 command/event wire 与平台 Adapter |
| 复杂领域真实共享 Adapter | Core `ModelStore`、`CodingAgentRunner`、Voice session、Provider policy、Remote Input 与 Style Pack 已接入;Linux Qwen runtime 已进入打包链,Foundry/Sherpa 与真实设备/发布物仍需平台证据 |
| 会话级 provider router | 已建立;ID/type/model 在 session 开始时固定,Core 持有云 ASR/LLM/Omni 协议 Implementation;Tauri 与 Linux 注册同一共享实现,Tauri 另行追加 native/local ASR |
| provider 验证/模型列表管理面 | 已建立;Core `ProviderService` 统一 channel-scoped credential、静态/远端模型列表、验证探活和错误脱敏;Tauri command 只做 wire 转换,Linux shared factory 注入同一 service;真实网络/keyring 和平台 runner 仍按主计划 M9/M10 留证 |
| Linux credentials/resources/fcitx5/capabilities/host-actions | 已建立非 UI Adapter 和 contract tests;WSL Ubuntu 已显式通过真实 Secret Service set/read/remove、fcitx5 plugin/method/listener/signal contract;无焦点输入时 plugin 不抛异常导致 fcitx5 崩溃 |
| Linux cpal 录音、共享 Pipeline builder、热键 listener、第二实例 intent 转发 | 已建立;selection/translation 已路由到共享 Interface;WSL 当前无 ALSA 设备时 cpal contract 已证明稳定分类错误,真实设备和桌面 runtime 生命周期仍见计划 M8/M9 |
| Linux 打包 workflow/manifest 契约 | 已建立但正式发布仍需真实 Ubuntu 安装、运行、升级和回滚证明 |
| egui UI | 已使用真实 `eframe::run_native`,覆盖 startup/error、听写、QA、Remote Input、Selection preview/revert、Less Computer/approval、Provider/Credential、模型、history 与 settings;视觉深化不属于 2.0 Core 收口 |
完整验收以主计划第 12 节为准;本契约证明 Linux UI 可以在不依赖 Tauri 的前提下使用冻结的
2.0.0 Interface。真实 Ubuntu 原生能力、发行包与安装升级回滚仍由 Linux runner 门禁证明。
## Less Computer 语音接口(2.0)
`OpenLessBackend::start_less_computer_voice(session_id, recording_control)` 返回 Core-owned
`LessComputerVoiceSession`。Host 必须只发送 16 kHz、mono、signed 16-bit little-endian
PCM;空帧、奇数长度和累计超过 provider 上限会返回 `InvalidArgument`。`finish` 只允许调用
一次,ASR 失败或空 transcript 不会启动 Agent,并释放 capture lease;`cancel` 同时取消
ASR/Agent 并释放尚未提升的 lease。`recording_control` 是窄平台 effect:Core 的共享
`SilenceAutoStop`/fault controller 决定 stop 或 cancel,Host 只关闭自己持有的 capture handle。
实时 provider 的 interim 文本通过既有 `BackendEventKind::TranscriptDelta` 发布,使用同一
`session_id` 且 `offset` 单调递增;批式 provider 只发布一次 `is_final=true`。Agent 阶段继续
使用 `LessComputerEvent`(approval、stream、completed、cancelled、error),不新增 ASR 事件
类型,Backend contract 版本为 `2.0.0`。
Linux `LinuxHotkeyEvent::{LessComputerPressed,LessComputerReleased,LessComputerCombined}`
只表达热键边沿;Hold/Toggle/Auto(Auto 长按阈值 350ms)由 Core 解释。三种录音入口与
`silence_auto_stop_enabled` 共用同一设置,冲突时保留当前会话并返回 `Busy`。
- 原生预加载必须使用请求的 target/provider type;平台无法准备流式时以 `supports_streaming=false` 保留一次性落字。
- Remote stop 保持可取消的 session;socket 下行只转发本连接所属 session 的事件。
- 自动 contract 不替代真实平台证据:Ubuntu/Windows/macOS/Android 的设备、安装、升级与签名结果按[验收](linux-egui-handoff/07-acceptance.md)与[桌面验收](2.0-desktop-acceptance.md)分别记录。
+19 -58
View File
@@ -1,68 +1,29 @@
# 01:Core接入合同
# 01:Core 合同(Linux 可调用的面)
前置:[交接入口](./README.md)。本文只约束接入,不规定页面布局。
状态:canonical(2026-09-07 以源码为准重写);更新:2026-09-07。前置:[交接入口](./README.md)。本文只约束接入,不规定页面布局。
## 1. 可以依赖的层
## 1. 合同文件与启动门
`egui UI → LinuxHost / Linux Adapter → openless-core`。
- 公开合同:`openless-all/app/contract/backend-2.0.json`,顶层键 `contractVersion` / `startupSnapshot` / `backendEvent` / `lessComputerVoice` / `androidJni` / `linuxFacade` / `enums`。启动时校验版本;UI 必须先消费 startup snapshot 再渲染状态。
- 构造:`LinuxBackendBuilder::from_shared_providers(config)`(`linux-egui/src/backend.rs`)创建与 Tauri 同源的 Core 后端;云 ASR/LLM/Omni 实现两端共用,不复制第二套。
- 事件:Core `events.rs` 语义事件 → `LinuxHost::drain_events`(`lib.rs`)→ UI reducer;订阅用 `LinuxHost::subscribe` 返回 `EventSubscription`。
- 会话/取消:所有长操作(听写、转写、下载、QA、Agent)走 Core session 取消语义;UI 只触发与显示,不自建取消规则。
- Core拥有provider协议、渠道/模型/偏好事务、会话与取消、业务路由、纠错/历史、Agent命令和结果语义。
- Linux Host拥有音频、全局热键、窗口、fcitx5、Secret Service、TLS/socket、进程与文件效果。
- UI拥有页面、编辑草稿、进度/错误展示和事件消费;不另建业务状态机。
- 不引用Tauri源码、不读取Core私有模块、不把`AppHandle`/egui类型加入Core公开合同。
## 2. Core 公开模块(`crates/openless-core/src/lib.rs`)
## 2. 构造、启动、退出
按域的模块地图见[架构文档](../architecture.md)第 3 节。Linux 直接相关:
1. Host确定显式`BackendConfig`中的data/cache/home等路径及能力;不要在测试中隐式使用真实用户目录。
2. 使用`LinuxBackendBuilder::from_shared_providers(config)?.build()?`复用生产组装;测试可用显式注入入口。
3. 创建事件订阅/Host action消费,再调用`LinuxNativeRuntime::start`;该方法内部启动Core,调用方不重复启动。
4. 检查启动快照`running`和合同版本`2.0.0`。未就绪时禁用业务操作并展示错误。
5. 从Core读取设置/渠道/模型/各领域快照,启动非阻塞UI消费;原生热键须同步保存的配置。
6. 退出时禁止新请求,取消活动会话,停止Host listener/服务并调用幂等shutdown。
- 业务门面:`api`(对外接口)、`domains`(领域出口)、`events`、`ports`(Host 注入接口,含 `NoopEditObservationAdapter` 等默认实现)。
- 听写:`dictation_engine`、`silence_auto_stop`、`streaming_insert`、`hotkey_interpreter`。
- 服务:`provider_*` 系列、`omni`、`credentials`、`endpoint_security`。
- 本地模型:`model_store`、`local_asr_service`、`local_asr_catalog`。
- 领域:`history`、`vocabulary`、`correction`、`style_pack_store`、`marketplace`、`qa_service`、`selection_*`、`less_computer`、`remote_input_service`。
现有[backend.rs](../../openless-all/app/linux-egui/src/backend.rs)、[runtime.rs](../../openless-all/app/linux-egui/src/runtime.rs)和[headless示例](../../openless-all/app/linux-egui/examples/headless_host.rs)是实际构造依据。
## 3. 注入点(`ports.rs`)
`from_shared_providers`和未显式注入executor的`build`必须在Host已经创建的Tokio runtime内调用;同步GUI初始化可用短作用域`runtime.enter()`包住构造,退出该作用域再`block_on`启动。builder捕获同一runtime的Handle,录音回调、静音终止和资源析构即使来自普通OS线程也能提交任务。未进入runtime时构造明确失败,不新建runtime或静默丢任务。使用自定义`LinuxBackendBuilder::new`的Host可通过`with_task_spawner`提供自己的实现,但它必须接受任意原生线程调用;Host保持executor存活直至shutdown与原生清理完成。不要用只查“当前线程runtime”的默认spawner承接cpal回调。
Host 必须实现并注入:`AudioRecorder`、`TextPolisher`、`TextInserter`、`CredentialStore`、`SettingsRuntime`、`TaskSpawner`、`EditObservationAdapter`(未接时为 `NoopEditObservationAdapter`,见缺口 L02)、`LinuxHostActions`。注入清单与默认值以 `backend.rs` builder 为准;缺省实现表示"未接线",不是"不支持"。
## 3. 公开能力与调用位置
## 4. 纪律
| 领域 | 入口/来源 | Host/UI使用要求 |
| --- | --- | --- |
| 听写 | `OpenLessBackend` start/stop/cancel、snapshot | 使用Core session;不要另做Hold/Toggle/Auto/静音/重试策略 |
| 设置 | `LinuxHost::save_settings` / `update_settings_strict` | 带读到的revision;执行Core effect plan,失败按receipt恢复 |
| 渠道/凭据 | Core channel facade、`ProviderApi` | 保留channel ID与provider type区别;默认值/认证规则来自descriptor |
| 模型 | `LocalAsrApi` / `ModelStore` / activation | 用一次activation事务,不拆成UI连续写prefs和active provider |
| 历史/词典/纠错/风格包 | Core facade,[api.rs](../../openless-all/app/crates/openless-core/src/api.rs) | 通过领域方法修改;不直接写JSON或重做ZIP/文本规则 |
| QA | `services().qa` | stop使用每轮token;conversation owner不等于每轮capture token |
| Selection/Selection Voice | 对应`services()`接口 | 预览/确认/撤回/意图由Core决定;Host保存原生目标并执行效果 |
| Less Computer / Agent | 对应`services()`及Core语音入口 | 复用命令、审批、解析与取消;Host只启动/终止进程 |
| Remote Input | `services().remote_input` | Core处理认证/会话/帧序号;Host负责TLS/WSS与H5传输 |
| 能力/权限 | Core snapshot + Linux capability probe | 未实现/未授权/未知要区分,UI不能凭平台名伪造可用 |
DTO和平台Interface集中在[domains.rs](../../openless-all/app/crates/openless-core/src/domains.rs)、[ports.rs](../../openless-all/app/crates/openless-core/src/ports.rs)。
## 4. 业务不变量
- 听写、QA、Selection Voice、Less Computer共享语音互斥:Busy拒绝新会话,不抢占旧会话。
- provider/channel/model与上下文按会话冻结;只有已有合同允许的停止时翻译切换可更新该轮。
- Core按实际管线解析渠道:Omni不依赖传统ASR/LLM,Less/Selection录音只依赖ASR,QA文本不依赖ASR。Raw允许没有可用LLM;若停止时改为翻译,Pipeline使用冻结的LLM或`deferred_llm_error`,不会偷偷切到录音期间新增/启用的渠道。
- 停止、取消、timeout、设备fault与迟到结果必须保持单一终态,不能让旧任务改变新session。
- 逻辑取消会立即使本代token失效;原生初始化、stop或ASR清理仍在途时,Core资源hold继续阻止新语音。收到取消终态不表示已经可以绕过Core强行打开另一个麦克风。
- 自定义`DictationEngine`须原样接收/转发`start_voice_capture`与`start_audio_capture`的`CancellationToken`;ASR启动返回后、开麦前及原生初始化完成后均检查它,迟到句柄必须关闭。生产factory已接好,不要替换成一个永不取消的新token。
- `Inserted`、`PasteSent`、`CopiedFallback`、`NotRequested`和错误`OutcomeUnknown`不可互换。结果未知不得自动再插一次。
- 流式尾段协调、纠错执行顺序、历史/统计归因留在Core;Host只回报真实效果。
- 插入`begin()`本身也可能切换输入源。Core在准备前登记同一可等待结果,取消/丢弃调用方不能跳过尚未完成的原生恢复。
- `TextInserter::capture_target()`在听写认领后、上下文/凭据等待和反馈前同步调用;焦点敏感Host返回仅持本轮原生目标的插入器,异步`begin()`再准备输入源。原生句柄不进入Core DTO,无需插入时不捕获;不依赖焦点的Adapter保留默认`None`。Linux Host接入原目标快照时应覆写此入口,不能等凭据读取后重新抓当前焦点。
## 5. 秘密和数据
- 凭据秘密只通过`CredentialStore`/`SecretValue`传递;通用状态、事件、日志和可序列化UI快照不包含API密钥、OAuth access/refresh token或PIN。配对页面通过显式`read_pairing_pin`受控显示PIN,不广播到通用状态。
- 审批票据(如`Approval.token`、`pending_approval_token`)是Core业务合同的一部分,不是上述凭据秘密;UI须按原合同接收、关联并回传,不能自行伪造。
- Linux已经提供旧凭据解码/迁移起点;只有显式home目录才触发旧来源访问,完成标记最后提交。
- 设置revision冲突后重新读快照合并草稿,不无条件重放旧整表。
- 1.x数据格式兼容与runtime `2.0.0`握手是两回事:继续读旧数据,不接受旧runtime合同冒充已就绪。
## 6. 接口缺口的回报
提交:用户操作、已有公开入口、缺少的业务结果/Host效果、期望事件/错误、最小复现或fixture。
Core负责人修共享规则/接口;egui团队修平台适配/界面。详情按[缺口登记](./02-gap-register.md)分类,不能通过公开私有内部对象绕过。
- 保留启动、版本校验、事件与退出合同;改动先过 `cargo test -p openless-core --locked` 与合同测试。
- 合同测试不替代真实平台证据(见[07 验收](07-acceptance.md))。
+17 -35
View File
@@ -1,53 +1,35 @@
# 02:Linux 缺口登记与实施顺序
状态:draft;阶段:partial-implementation;更新:2026-09-07。
初始盘点基线:`2b315572`;本次UI整理基线:`fc38edd1`,更新反映当前工作区。责任依据:[当前范围](../2.0-requirements.md)。以下是源码盘点,不是通过声明。
状态:canonical(2026-09-07 以源码为准重写,L-ID 体系沿用);更新:2026-09-07。本表是源码盘点,不是通过声明。
## 1. 状态定义
- **接线缺口**:Core 已有业务,Linux Host 尚未执行必要的原生效果。
- **界面缺口**:已有 Core/Host 入口,生产 UI 未提供完整操作。
- **待实测**:存在真实实现,但还需要对应桌面、设备或安装证据。
- 本表由 egui 团队推进 Linux Host/UI;若发现共享接口不能承载该业务,回报 Core 负责人修复,不在 UI 复制规则。
- **待实测**:存在真实实现,还需要对应桌面、设备或安装证据。
## 2. 待办登记
| ID | 当前状态与明确缺口 | 关闭标准 / 详细说明 |
| ID | 状态与缺口(源码锚点) | 关闭标准 |
| --- | --- | --- |
| L01 | 接线+界面:`switch_style`、`open_app`、`style_packs` 热键修改被 Linux settings 明确拒绝 | 实际全局注册、触发、重绑、失败恢复和重启还原;分别声明 X11/Wayland 支持。[热键](./03-hotkeys-and-windows.md) |
| L02 | 接线:HostContext/EditObservation 仍为默认 Noop,Selection 的 source_app 为 None | 按隐私设置捕获真实应用/允许的上下文;原生手改观察能产生 Core 纠错建议并拒绝迟到结果。[原生](./05-native-host-and-data.md) |
| L03 | 接线:CPAL 无 RecordingArchive,未执行录音期间系统静音/恢复;缺提示音与胶囊 | 录音归档、保留策略、失败恢复、同归档重试和历史重转可用;所有终态恢复音量,反馈可见。[原生](./05-native-host-and-data.md) |
| L04 | 接线+界面:Selection Voice 未形成 Linux 生产触发/捕获/意图路由;QA 缺编辑模式/应用/撤回 UI | 完整触发至 Core intent、预览、目标核验、应用/取消/撤回;不能用已有 Selection polish 页面代替。[领域](./04-ui-domains.md) |
| L05 | 界面:词典、纠错规则/建议、风格包管理没有页面 | 各领域增删改/启停/预设/导入导出和失败反馈;通过 Core facade 持久化。[领域](./04-ui-domains.md) |
| L06 | 界面:Marketplace service 已接入,市场页面缺失 | 浏览、安装、上传/下载、点赞/作品管理、设备 OAuth 登录/取消/退出;复用 Core 协议。[领域](./04-ui-domains.md) |
| L07 | 界面+L03依赖:历史仅只读最近20条,缺完整历史/统计/录音操作 | 浏览和原有历史操作、重润色/重转写、录音播放/导出/清理与统计;真实归因不由 UI 拼装。[领域](./04-ui-domains.md) |
| L08 | 界面:本地 Qwen 有下载/激活/取消,缺完整模型管理和运行时控制 | 路径/镜像、详情/状态、删除、预载/释放和准备/测试取消;真实推理另附证据。[领域](./04-ui-domains.md) |
| L09 | 界面部分改善:已增加领域导航、开始/环境引导、独立现有设置和手机输入页、跨页审批/取消;多数 Linux 适用设置、Agent 检测/模型/路径/权限配置仍缺失 | 设置有实际消费者、revision/错误处理;保留现有 Less Computer 输出/审批/取消流程。本次未关闭L09。[领域](./04-ui-domains.md) |
| L10 | 接线+界面:无托盘/自启;通知仅状态栏,重启仅提示,AppImage能力判定不等于更新器 | 窗口/后台运行、通知、自启、检查/下载/安装更新与重启有真实 Host 效果。[原生](./05-native-host-and-data.md) |
| L11 | 已实现待实测:fcitx5 输入/选区、CPAL、Secret Service、Qwen、CLI 进程、Remote TLS/H5 | 通过真实桌面/设备矩阵;未通过的逐项记录,不把整模块称为缺失。[验收](./07-acceptance.md) |
| L12 | 已有包构建,待 Linux 产品验收与正式分发 | 完成上述应用缺口及 Linux 安装/升级/回滚、签名和更新证据;不阻塞 Windows/macOS 首批交付。[验收](./07-acceptance.md) |
| L01 | 接线+界面:`switch_style` / `open_app` / `style_packs` 热键修改在 `settings.rs:56-61` 被明确拒绝 | 实际全局注册、触发、重绑、失败恢复与重启还原;分别声明 X11/Wayland 支持 |
| L02 | 接线:未注入 `EditObservationAdapter`,Core 默认 `NoopEditObservationAdapter`(`ports.rs:539`)生效,Selection `source_app` 为 None | 按隐私设置捕获真实应用/允许的上下文;手改观察能产生纠错建议并拒绝迟到结果 |
| L03 | 接线:`audio.rs` `LinuxCpalRecorder` 无录音归档,未执行录音期间系统静音/恢复;缺提示音与胶囊 | 录音归档、保留策略、失败恢复、历史重转写可用;所有终态恢复音量 |
| L04 | 接线+界面:Selection Voice 未形成生产触发/捕获/意图路由;QA 缺编辑模式/应用/撤回 UI | 完整触发至 Core intent、预览、目标核验、应用/取消/撤回 |
| L05 | 界面:词典、纠错规则/建议、风格包管理没有页面(`ui_state.rs` 10 页无对应项) | 各领域增删改/启停/预设/导入导出与失败反馈,经 Core facade 持久化 |
| L06 | 界面:marketplace 服务已接入(`marketplace.rs`),市场页面缺失 | 浏览、安装、上传/下载、设备 OAuth 登录/退出,复用 Core 协议 |
| L07 | 界面+L03 依赖:历史仅只读最近条目,缺完整历史/统计/录音操作 | 浏览与既有历史操作、重润色/重转写、录音播放/导出/清理与统计 |
| L08 | 界面:本地 Qwen 有下载/激活/取消,缺完整模型管理与运行时控制 | 路径/镜像、详情/状态、删除、预载/释放与准备/测试取消;真实推理另附证据 |
| L09 | 界面部分改善:10 页导航、开始/环境引导、独立设置与手机输入页、跨页审批/取消已落地;多数 Linux 适用设置、Agent 检测/模型/路径/权限配置仍缺失 | 设置有实际消费者、revision/错误处理;保留 Less Computer 输出/审批/取消流程 |
| L10 | 接线+界面:无托盘/自启(`capabilities.rs` 仅探测 `supports_tray`,未实现);通知仅状态栏;AppImage 能力判定不等于更新器 | 窗口/后台运行、通知、自启、检查/下载/安装更新与重启有真实 Host 效果 |
| L11 | 已实现待实测:fcitx5 输入/选区(`fcitx5.rs`)、CPAL(`audio.rs`)、Secret Service(`credentials.rs`)、Qwen 运行时、CLI 进程、Remote TLS/H5(`remote_input.rs`) | 通过真实桌面/设备矩阵;逐项记录,不把整模块称为缺失 |
| L12 | 已有包构建(deb/rpm/AppImage workflow),待 Linux 产品验收与正式分发 | 完成上述应用缺口与安装/升级/回滚、签名和更新证据;不阻塞 Windows/macOS 首批交付 |
## 3. 已有且应直接复用
- 云 ASR/LLM/Omni、Provider descriptor 和渠道管理、Linux vault、Core ModelStore、Generic Qwen runtime。
- 普通听写与现有 fcitx5 热键、ticket 化落字、Selection polish 预览/撤回、QA 文本/语音、Less Computer 工具审批。
- Remote Input TLS/H5、配对和会话桥接、单实例、Core 合同校验、事件重放、native shutdown。
对应实现位置见[Core合同](./01-core-contract.md)、[领域](./04-ui-domains.md)和[原生](./05-native-host-and-data.md)。这些代码是接入起点,不需要另建第二套后端。
## 4. 依赖顺序和完成口径
## 3. 依赖顺序
1. 保留启动、版本校验、事件与退出合同,跑通一个现有听写流程。
2. 补 L01–L03 原生能力;页面可并行,但历史音频/自动纠错不能脱离其 Host 依赖单独宣布完成。
3. 补 L04–L09 领域入口及完整成功/失败/取消流程,再补 L10 桌面集成。
4. 按 L11–L12 获取真实环境与发布证据。
任务记录格式:`ID / owner / commit / 已完成效果 / 自动证据 / 设备证据 / 剩余限制`。
“待实测”不是“未实现”,也不是“已完成”;新发现必须按层归属,不能仅因 Linux 故障就判定 Core 缺失。
## 5. 本轮 UI 进展(2026-09-06)
`L04/L07/L08/L09/L11 / Linux UI / 共享工作区未提交`:现有领域从单页拆为导航分页;开始页与环境准备解释fcitx/plugin、桌面会话、麦克风与Secret Service的实际可知状态。原生未连接时保留诊断步骤,未展示成功。Agent审批固定在页面外,QA/Selection/Agent后台事件保留操作或导航提示,未改变Core接口或扩充原生适配。
本轮只改善入口与显示,不关闭Selection Voice、完整历史/模型管理、Agent高级配置、托盘/更新器及Linux设备/分发缺口。实现、官方来源、定向命令和实际自动证据统一记录在[04本轮记录](./04-ui-domains.md#5-2026-09-06-导航与-linux-准备引导),不在此重复维护。
任务记录格式:`ID / owner / commit / 已完成效果 / 自动证据 / 设备证据 / 剩余限制`。“待实测”不是“未实现”,也不是“已完成”。
@@ -1,51 +1,19 @@
# 03:全局热键与窗口
对应缺口:L01、L10。系统注册和窗口效果由 egui 团队的 Linux Host 负责,业务动作继续调用 Core。
状态:canonical(2026-09-07 以源码为准重写);更新:2026-09-07。对应缺口:L01、L10。系统注册和窗口效果由 Linux Host 负责,业务动作继续调用 Core。
## 1. 历史与当前差异
## 1. 已有实现
Tauri 1.x 在 **X11** 上已有切换风格、打开主窗和风格包热键,经过 `global-hotkey` 路径。
源码锚点为 `v1.3.18-tauri` 的 `src-tauri/src/lib.rs` 三个 `start_*_hotkey_listener` 和 `coordinator/hotkey_loops.rs` 的实际处理分支。
这不能证明原生 Wayland/fcitx 路径曾具备同等能力,也不能写成“1.x 从未实现”。
- **fcitx5 热键监听**:`hotkeys.rs` `Fcitx5HotkeyListener::start()/drain()/take_error()`——经 fcitx5 插件路由热键事件(已实现待实测,见 L11)。
- **fcitx5 输入/选区**:`fcitx5.rs`——插件安装(AppImage 在 listener 前安装)、PRIMARY 选区、落字。
- **单实例**:`single_instance.rs` `SingleInstanceGuard::acquire(path)` + `SingleInstanceBroker::acquire_or_forward()/drain()`——二次启动意图转发给首实例。
- **设置侧拒绝逻辑**:`settings.rs:56-61` 对 `switch_style` / `open_app` / `style_packs` 三类热键修改给出明确拒绝信息(L01 的直接证据)。
- **能力探测**:`capabilities.rs` `LinuxCapabilitySnapshot`(session 类型、`PlatformCapabilities`、权限快照),`supports_tray` 只是探测字段。
当前 Linux 已用 fcitx5 接听写、QA、选区润色、翻译和 Less Computer;但以下三项仍缺 Host 接线:
## 2. 缺口(L01)
| Core配置目标 | 当前缺口 | 触发后的业务入口 |
| --- | --- | --- |
| `HotkeyRuntimeTarget.switch_style` | Linux settings 拒绝修改,没有对应 native event 分支 | `activate_previous_style_pack` |
| `HotkeyRuntimeTarget.open_app` | 同上 | `request_host_action(HostAction::ShowMain)` |
| `HotkeyRuntimeTarget.style_packs` | 同上 | `activate_style_pack`,使用绑定的稳定风格包ID |
全局热键修改(改绑 `switch_style`、`open_app`、`style_packs`)被 settings 拒绝,因为 Host 尚未提供真实的系统级注册/重绑/失败恢复/重启还原。关闭标准:实际注册、触发、重绑、失败恢复与重启还原,分别声明 X11/Wayland 支持范围。
入口见[Core api.rs](../../openless-all/app/crates/openless-core/src/api.rs)与[HostAction](../../openless-all/app/crates/openless-core/src/ports.rs)。
## 3. 缺口(L10,窗口侧)
## 2. 应修改的生产路径
- [settings.rs](../../openless-all/app/linux-egui/src/settings.rs):`reject_unsupported_hotkey_changes`、`LinuxSettingsEffects::apply_hotkeys` 和 prepare/commit/restore。
- [lib.rs](../../openless-all/app/linux-egui/src/lib.rs):`LinuxHotkeyEvent` 消费及业务派发。
- [runtime.rs](../../openless-all/app/linux-egui/src/runtime.rs):启动同步、listener 重连及 pump。
- [fcitx5.rs](../../openless-all/app/linux-egui/src/fcitx5.rs)与[插件](../../openless-all/scripts/linux-fcitx5-plugin):按所选原生机制扩展注册和事件传输。
- [main.rs](../../openless-all/app/linux-egui/src/main.rs):快捷键编辑、冲突/不可用提示及窗口消费。
不能仅删除 `Unsupported` 判断:只有原生注册和实际事件路径一起完成,配置才算生效。
## 3. 注册事务
1. UI 提交草稿和读取时的 preferences revision,经 LinuxHost 调 Core 设置验证。
2. Host 按 effect plan 注册新目标、清除旧目标;资源占用/无权限必须返回错误。
3. 部分失败按 receipt 恢复旧注册及配置;不能保存“新键”却仍监听“旧键”。
4. 启动和 fcitx5 重连读取同一 Core target 重新同步;退出/禁用时释放注册。
5. 听写的 Hold/Toggle/Auto、去抖/冷却仍归 Core;Host 提供真实边沿,不另建解释器。
X11 与 Wayland/桌面环境分别报告支持情况。窗口内的 egui 键盘事件不等于系统全局热键;焦点离开 OpenLess 后仍须按承诺范围工作。
## 4. 窗口和后台行为
现有 `ShowMain`/`FocusMain` 已映射 egui viewport,单实例有转发机制;保留这些实现。
尚需完整托盘/后台窗口行为、录音反馈层、桌面通知与真正重启。`ShowDictationFeedback`/`HideDictationFeedback` 当前不产生效果,通知只进状态栏。
窗口聚焦不得覆盖录音开始时捕获的输入目标。关闭业务面板应取消其会话;隐藏主窗、退出进程、关闭面板是不同操作。
## 5. 关闭证据
逐项覆盖:首次保存、重绑、旧键失效、冲突和回滚、重启还原、插件重载、后台触发、禁用/退出释放;风格包删除/禁用后的绑定也需一致。
测试结果分别标注 X11/Wayland、桌面环境、实际前台应用。目标环境无法提供某动作时明确禁用并说明限制,不返回假成功。
托盘、窗口/后台运行策略、通知、自启、更新器均未实现(capabilities 仅探测)。参考[05](05-native-host-and-data.md)第 4 节。
+19 -103
View File
@@ -1,112 +1,28 @@
# 04:页面与领域操作
状态:draft;阶段:partial-implementation;更新:2026-09-07。页面实现与自动证据不等于 Linux 原生验收通过。
状态:canonical(2026-09-07 以源码为准重写);更新:2026-09-07。页面定义:`linux-egui/src/ui_state.rs` `Page` 枚举(10 页);主循环与交互:`main.rs`。
对应 L04–L09。现有[main.rs](../../openless-all/app/linux-egui/src/main.rs)是可复用的真实 UI 起点,但并非功能齐全的产品。
接口查[Core api.rs](../../openless-all/app/crates/openless-core/src/api.rs)与[domains.rs](../../openless-all/app/crates/openless-core/src/domains.rs)。
## 1. 十页现状
## 1. 已有页面与需要补齐的操作
| 领域 | 当前生产UI | egui团队接入要求 |
| 页面 | 已有操作 | 主要缺口 |
| --- | --- | --- |
| Provider/渠道 | ASR/LLM/Omni渠道创建、重命名、启停、排序、激活、删除、编辑、列模型和校验已有 | 复用 descriptor;完善 vault 锁定/错误恢复、状态刷新和正式交互,不重做认证规则 |
| 本地模型 | Qwen列表、下载、激活、取消已有 | 接 `LocalAsrApi` 的路径/镜像、model_card/remote_info、状态、删除、preload/release、prepare/test_model及取消;激活使用 Core 原子事务 |
| QA | 文本/语音、流式回答、取消/关闭已有 | 接 `set_edit_instruction_mode`、编辑预览、应用/撤回及状态;用每轮 recording token 停止录音 |
| Selection polish | 可编辑预览、确认、取消、完成后撤回已有 | 保留原目标/session核验和错误反馈,不将剪贴板旧选区当有效原目标 |
| Selection Voice | Core service已有,Linux完整触发/意图路由与UI缺失 | 见下节;不是只加一个按钮即可完成 |
| Less Computer | 文本/语音、输出/工具状态、Allow/Deny审批和取消已有 | 接 `CodingAgentApi` 的detect/list_models/run_test/cancel_test和provider/model/executable/workdir/permission配置;不自建审批与续聊策略 |
| 风格包 | 无管理页 | 接Core列表、创建/编辑、启停/激活、内置重置、删除、提示词诊断、ZIP导入/导出;快捷键依赖L01 |
| 词典/纠错 | 无页面 | 接词条增删/启停、预设、规则;pending corrections接受/拒绝/清空。自动建议来源依赖L02 |
| Marketplace | service和下载归档Host已有,无页面 | 接 `MarketplaceApi` 的列表/详情/安装/下载/上传、点赞/作品、设备OAuth启动/轮询/取消/退出 |
| 历史/统计 | 仅最近20条只读和插入状态 | 完整浏览及历史操作、删除/清空、重润色/重转写、复制/导出、录音播放/定位/清理、活动统计;音频先补L03 |
| 设置 | 环境准备与现有streaming_insert、Agent启用;Remote开关/端口独立到手机输入页 | 补Linux适用的麦克风/静音、语言/翻译、模式/热键、QA历史、选区、隐私/日志和外观;每项必须有真实消费者 |
| Remote Input | 独立页面保留开关、地址、PIN与重置,增加服务状态/连接数/陈旧地址提示及LAN/证书说明,TLS/H5已接 | 继续补二维码、完整连接错误恢复与证书信任体验;复用服务并进行真手机验收 |
| Start | 下一步引导、环境准备说明(fcitx/插件、桌面会话、麦克风、Secret Service 的真实可知状态;未连接时保留诊断步骤,不展示成功) | — |
| Dictation | 普通听写(复用 Core `dictation_engine`)、ticket 化落字 | — |
| Qa | QA 文本/语音(`qa.rs`)、全局待办、审批、取消 | 编辑模式/应用/撤回 UI(L04) |
| Selection | 划词润色预览/撤回(`selection.rs`) | Selection Voice 生产触发/捕获/意图路由(L04) |
| Agent | Less Computer 工具调用、审批固定在页面外、后台事件保留 | Agent 检测/模型/路径/权限配置(L09) |
| Services | AI 服务配置、渠道管理(`settings.rs` + Core provider 面) | Omni 有效模式配置入口完整性(L09) |
| Models | 本地 Qwen 下载/激活/取消 | 完整模型管理:路径/镜像/详情/删除/预载/释放(L08) |
| Remote | 手机输入独立页、连接状态、陈旧地址处理 | — |
| History | 只读最近条目 | 完整历史/统计/录音操作(L07,依赖 L03) |
| Settings | 现有设置独立页 | 词典/纠错/风格包/市场无页面(L05/L06);多数设置缺实际消费者(L09) |
不要给 Linux 暴露无实现的 Windows TSF 或 macOS AX 权限按钮。当前 Linux 本地运行时为 Generic Qwen;Foundry/Apple MLX 不是这一轮要求跨平台移植的功能。
## 2. 跨页约定
## 2. Selection Voice 接入链
- Agent 审批固定在页面外展示,QA/Selection/Agent 的后台事件保留操作或导航提示。
- 事件消费不得因导航停止(见[06](06-events-and-sessions.md));会话取消语义由 Core 承担。
- 每个旧操作保留真实调用;页面增删不改变 Core 接口。
1. Linux Host 捕获原生目标、选区及会话身份,接入 Core 的共享语音互斥和停止/取消路径。
2. 使用 `SelectionVoiceApi` 的 `begin`、`process_transcript`、`confirm_intent`;由 Core 决定 `route_disposition`。
3. UI 消费 Selection Voice 事件,展示意图选择或路由后的 QA/编辑结果;当前 main.rs 尚未消费这些事件。
4. 编辑经过 `prepare_edit`、`begin_preview_apply`,Host 按 ticket 核验并执行替换,再以真实结果调用 `finish_preview_apply`。
5. 目标失效、取消、重复确认和未知插入结果都应保持安全,不能绕过 Core 事务直接粘贴。
## 3. 新增页面的实施要求
Linux QA 的 target rekey 适配和现有 Selection runtime可以复用;headless直接调用Core的测试不算生产接线完成。
QA 内的预览必须传当前 turn token,使用 `QaApi::begin_edit_preview_apply`/`revert_edit_preview`;原生应用完成后用 `dismiss_session` 只关闭所属回合。不要拆成读取 conversation owner 后直接改 Selection Voice,再无条件关闭 QA。独立 Selection Voice 的 `begin_preview_apply`/`revert_preview` 是无异步原生效果的同步状态操作;`PasteSent` 只表示已发送,不能标成 `Inserted` 或失败。
## 3. 共同交互规则
- UI只保留编辑草稿、焦点等显示状态;持久化变更走Core facade,不直接读写JSON、凭据或风格ZIP内容。
- 设置携带revision,冲突后重读;渠道ID与provider类型分开,默认值/认证来自descriptor。
- 长操作在Host runtime调度,事件驱动重绘;egui frame不阻塞等待网络、模型下载或进程。
- 字段禁用、加载、取消、错误、空列表、重试与重开须齐备;按钮存在但效果未接入应标明不可用。
- 历史重转使用Auxiliary服务与`apply_history_retranscription`,真实provider/model/timing由Core生成;不要根据当前设置猜历史归因。
- API密钥只写入不读回;配对PIN通过显式配对接口受控显示,不进入通用日志/状态广播。
## 4. 关闭标准
每个领域交付“用户入口 → Core调用 → Host效果 → 事件/持久化”的实际流程;验证成功、失败、取消、重新打开和重启。
领域UI完成不自动关闭相关原生缺口;L02/L03/L01分别验收上下文、音频归档和全局热键。
## 5. 2026-09-06 导航与 Linux 准备引导
页面复用现有 Core 与 Linux Host 接口。
| 入口 | 当前行为 |
| --- | --- |
| 开始 | 环境准备 → AI 服务或本地模型 → 听写;按 Core 有效模式分别显示 ASR/LLM 或 Omni 配置,已配置与校验通过分开;连接后可直达问答、选区润色、Agent、手机输入、历史 |
| 工作页面 | 听写、问答、选区润色、Less Computer 分页;每页独立滚动,草稿与会话保存在同一个 App 状态中 |
| 准备与管理 | AI 服务保留原渠道操作;本地模型保留列表/下载/激活/取消;历史保留最近20条只读与插入结果分类 |
| 环境与设置 | 只提供已有消费者的流式插入与Agent启用,保留revision保存;手机输入配置共用原设置草稿,保存/重新读取明确说明影响 |
| 手机输入 | 配置、保存、状态刷新、连接数、配对码重置;停止、地址陈旧时隐藏PIN和URL,状态读取失败清除旧显示 |
宽窗口使用左侧导航;小于760逻辑像素时改用可换行的顶部导航。主循环仍先无条件`poll`再渲染当前页面,保留50ms重绘、sequence去重、session归属、lag重放、退出shutdown与Esc语音取消。
- QA/Selection/Agent与听写/模型/Remote后台事件给对应导航增加提醒,访问该页面只清除该页未读状态,不取消后台工作。
- `ShowQa`、`ShowSelectionPreview`、`ShowLessComputer`打开所属页面;普通`ShowMain`保留当前页面。隐藏窗口/面板的既有Host语义保留。
- Agent审批固定在页面滚动区外:显示原command与原token对应的允许/拒绝;所有页面可直接取消Agent。取消请求与审批结果仍走原Core facade。
- QA有打开、原文本/语音操作、关闭及显式取消;全局任务栏保留问答取消、选区预览取消和当前语音取消。选区确认/撤回仍核验原session。
- Agent `VoiceState`仅增加导航提示,不把录音session当聊天session,不伪造首帧或电平。完整typed语音反馈与投影恢复仍按[06](./06-events-and-sessions.md)继续接入。
### 环境状态的证据边界
启动失败仍能查看开始页与环境引导。现有插件安装检查的Ready/Updated/Missing/错误结果被保留;Updated/Missing仍阻止启动原生运行时。未连接Core的领域页面展示准备入口,不开放假可用操作。
- 桌面类型来自`LinuxCapabilitySnapshot`的环境判断,不是端到端桌面可用性测试。
- fcitx探测是现有D-Bus Peer Ping,只能说明探测有响应;插件文件存在不能证明加载,加载不能证明任意目标应用落字。
- “重新检测会话与D-Bus”在后台执行,只更新环境探测;不会重新安装插件或启动Core。本次启动的插件检查结果保留,修复后需重启OpenLess。
- 麦克风显示未验证录音/当前环境不支持,不将Unknown当授权成功。Secret Service没有独立连接/解锁探测,UI明确说明未知,并引导用户解锁桌面密钥环后通过原渠道保存/验证获得真实结果。
- 不因X11 overlay或AppImage auto-update能力flag显示假浮层、托盘或更新器,也不新增Windows/macOS专属设置。
可复制的`fcitx5-diagnose`用于诊断;`fcitx5-remote -r`仅重载配置,不能称为重新安装或重启插件。新插件仍未加载时,指引重新登录桌面并重启OpenLess。Wayland指向桌面/工具包对应官方指导,不给所有桌面统一写环境变量。
### 定向验证与剩余实测
导航状态位于[ui_state.rs](../../openless-all/app/linux-egui/src/ui_state.rs),可在非Linux平台测试。主文件定向测试覆盖后台审批跨页可见、迟到终态隔离、QA/Selection草稿与取消提示、启动失败引导、陈旧Remote地址/PIN隐藏,并保留原两轮QA/Agent回归。
在`openless-all/app`执行:
```sh
rustfmt --edition 2021 --check --config skip_children=true linux-egui/src/main.rs linux-egui/src/ui_state.rs
cargo test -p openless-linux-egui --locked
```
完整主文件的`linux_app::tests`由Linux cfg控制;macOS直接运行上面的Cargo测试不会编译egui主界面。Linux机器还须执行:
```sh
cargo check -p openless-linux-egui --all-targets --locked
cargo test -p openless-linux-egui --bin openless-linux-egui --locked
```
原生依赖沿用[07验收](./07-acceptance.md)与现有CI。真机仍须覆盖420×400短窄窗口与宽窗口、中文字体、后台QA/Selection/Agent触发、跨页审批/取消、插件首次安装及重载、X11/Wayland目标应用、麦克风、密钥环、真实Qwen/CLI与手机TLS。自动布局绘制不是实际窗口/设备验收。
本次实际自动证据(macOS,Homebrew rustc 1.97.1):
- `cargo test -p openless-linux-egui --locked`:50个Host单元测试、4个Host合同测试及首轮2个导航测试通过;设备测试在此平台为0个,不能作设备证据。
- 为检查Linux cfg内的主界面,在独立临时crate复制本次`main.rs`/`ui_state.rs`,仅临时启用其UI编译,使用仓库已有eframe 0.31.1和同一Core/Linux Host路径依赖;`cargo test --offline`共9个测试通过(含最新3个导航测试、原2个事件回归和新增4个场景绘制测试)。未运行生产启动函数,未改变仓库Cargo清单或锁文件。该证据仅覆盖macOS上的UI类型、状态与无窗口绘制,Host仍使用macOS cfg。
- 尝试`cargo check -p openless-linux-egui --all-targets --target x86_64-unknown-linux-gnu --locked`,因目标`core/std`缺失(E0463)失败;本机Docker服务未运行。**没有Linux目标编译通过证据**,须在配置好原生依赖的Linux环境执行上方两条命令。
### 官方资料
来源、版本、获取日期和有效期以各官方项目发布页为准。
L04–L08 每个领域补完整成功/失败/取消路径后再算完成;领域功能扩展时优先拆分 `main.rs`(当前较大),避免继续单文件增长。
@@ -1,60 +1,29 @@
# 05:原生宿主、数据和系统集成
# 05:原生宿主、数据与系统集成
对应 L02、L03、L10、L11。平台效果归 Linux Host;现有代码可复用,具体缺口如下。
状态:canonical(2026-09-07 以源码为准重写);更新:2026-09-07。对应 L02、L03、L10、L11。平台效果归 Linux Host;现有代码可复用。
## 1. 音频、存档和反馈
## 1. 音频(`audio.rs`)
现有[audio.rs](../../openless-all/app/linux-egui/src/audio.rs)提供 CPAL 设备、PCM 转换、音量和致命错误回调;Core决定静音停止和重试策略。
- 已有:`LinuxCpalRecorder`(CPAL 录制,可指定首选设备)——已实现待实测(L11)。
- 缺口(L03):无录音归档(RecordingArchive)、未执行录音期间系统静音/恢复、缺提示音与胶囊。关闭标准:归档、保留策略、失败恢复、同归档重试与历史重转写可用;所有终态恢复音量且反馈可见。
- `LinuxCpalRecorder::start`目前只消费计划中的设备名称,没有执行录音期间系统静音/恢复。
- `LinuxActiveRecording`没有覆写`archive()`;Core默认返回None。因此录音归档、失败归档恢复、同归档静默重试的音频来源尚不完整。
- 补充[RecordingPlan](../../openless-all/app/crates/openless-core/src/dictation_context.rs)的适用效果及[RecordingArchive/read_pcm](../../openless-all/app/crates/openless-core/src/ports.rs);`archive_enabled=false`(QA/划词语音)时不得创建临时归档,不能用成功后删除代替不落盘。主听写/Less Computer 的保存与清理须遵循独立音频保留设置,不能误用历史条数。
- 用户取消、设备断开、启动失败和正常停止都必须恢复已改变的系统音量;不要覆盖录音期间用户主动修改的新状态。
- 提示音、录音胶囊/overlay尚未接入;消费Core阶段/反馈事件,避免UI自己猜测处理已结束。
- Less Computer成功非debug录音的归档清理由Core执行;Host需提供真实`RecordingArchive`和可执行的`discard`。未提供archive不等于已经验证文件保留策略,失败/debug场景也不能无条件删除。
## 2. 上下文与观察(L02)
验收:真实输入设备切换/拔出、无声停止、权限失败、归档开关/上限、失败重试与取消、历史录音播放/重转、各终态音量恢复。
Core `ports.rs` 提供 `EditObservationSink` / `EditObservationAdapter`(`ports.rs:523-539`);Linux 未注入实现,默认 `NoopEditObservationAdapter` 生效,Selection 的 `source_app` 为 None。关闭标准:按隐私设置捕获真实应用/允许的上下文;原生手改观察能产生 Core 纠错建议并拒绝迟到结果。
## 2. 输入目标、上下文和手改观察
## 3. 数据与凭据
[fcitx5.rs](../../openless-all/app/linux-egui/src/fcitx5.rs)已有ticket化插入、失败复制与结果区分;[selection.rs](../../openless-all/app/linux-egui/src/selection.rs)已有捕获/应用/取消/撤回和原目标核验。
- `credentials.rs`:Secret Service 凭据存储(L11 待实测)。
- `model_store`(Core):模型目录与下载;Linux 管理 UI 缺口见 L08。
- `remote_input.rs`:Remote Input TLS/H5、配对与会话桥接(L11 待实测)。
- `resources.rs`:打包资源路径。
**确定缺口**:Linux factory 未覆写默认`NoopHostContextAdapter`与`NoopEditObservationAdapter`,选区`source_app`仍为None。
接入[HostContextAdapter与观察合同](../../openless-all/app/crates/openless-core/src/ports.rs),采集平台能够可靠提供的应用身份和授权上下文,报告无法提供的能力。
## 4. 桌面集成(L10)
- 关闭光标上下文时不读取文档;必要前台应用元数据与文档内容分开处理。
- 记录开始时固定原目标,不因主窗获得焦点而改成当前窗口;迟到观察须核验generation/session。
- 不把PRIMARY旧值或剪贴板副本当成当前原控件仍匹配的证明。
- Core继续拥有流式尾段协调与纠错规则;Host不能因未知结果再粘贴一次。
未实现:托盘(`capabilities.rs` 仅探测 `supports_tray`)、自启、通知(仅状态栏)、检查/下载/安装更新与重启。AppImage 能力判定(`LinuxPackageKind`)不等于更新器已接好。关闭标准:各能力有真实 Host 效果与失败反馈,不以配置状态伪造就绪。
需在X11/Wayland各自的GTK、Qt、浏览器、终端验证;组合环境不支持可靠替换时明确降级/禁用,不伪造已插入。
## 5. 进程与运行时
## 3. 凭据和1.x数据
[credentials.rs](../../openless-all/app/linux-egui/src/credentials.rs)已有Secret Service、channel metadata与旧凭据迁移;[backend.rs](../../openless-all/app/linux-egui/src/backend.rs)仅在显式home路径下启用旧来源访问。
- 复用CredentialStore,不在UI读取vault数据库、推导账号名或保存明文密钥。
- 旧来源读取、目标写入、完成标记的顺序必须可重入;失败保留来源,不能凭“开始迁移”删除旧数据。
- 只操作OpenLess所属账号/命名空间;测试用独立data/cache/home和测试keyring,不触碰真实凭据。
- vault锁定/拒绝/不存在分别展示,保存失败不可显示成功;渠道删除不能误删其他渠道的秘密。
- 旧模型和自定义目录迁移复用Core ModelStore,不由UI复制目录并猜测ready状态。
## 4. 模型、进程与Remote
| 已有实现 | 需要保留/验证 |
| --- | --- |
| Linux Generic Qwen runtime、Core下载/激活事务 | 包内runtime路径、真实推理、timeout/cancel和旧释放不卸载新模型;Foundry/MLX不在Linux移植要求内 |
| Linux ProcessAdapter | 使用Core AgentCommand;验证桌面PATH、stdin背压、取消后进程组清理、审批/拒绝与自然结束 |
| Remote TLS/WSS、H5、Core配对/音频桥接 | 多连接隔离、首帧、停止时仍可取消、断线恢复、端口占用、手机证书信任;不要另写认证或会话状态机 |
这些模块有生产实现;缺真实凭据、模型或设备结果时写“待实测”,不要整模块标“未实现”。
若Host实现驻留模型缓存,须在`ModelRuntimeAdapter::claim_lease/preload_lease/release_lease`中原子维护模型与激活代次;同属Generic不代表同一缓存。普通使用应撤销旧激活的释放权,迟到加载不能覆盖新缓存,也不能取消仍有效的旧会话。Core负责激活失败补偿与metadata提交,Host不得仅按模型ID查询后再无条件释放整个runtime。
## 5. 系统集成仍需实现
现有[main.rs](../../openless-all/app/linux-egui/src/main.rs)将tray能力传为false;通知仅显示状态栏,`RequestRestart`仅提示手动重启,缺自启与实际更新器。
[capabilities.rs](../../openless-all/app/linux-egui/src/capabilities.rs)按AppImage布局判断`supports_auto_update`,这只是能力判定,不能当作更新服务已完成的证据。
egui团队补窗口/托盘/通知/自启,以及AppImage检查、验证、下载、替换和重启;系统包遵循其分发方式,不硬套AppImage更新流程。直到接线完成,UI不得依据此flag展示假可用操作。
缺fcitx5插件时当前程序进入启动失败页;产品可用性方案须明确安装/重载指引,若选择允许配置模式则只开放不依赖该插件的操作。
- `runtime.rs`:任务派生与生命周期。
- `coding_agent.rs`:CLI 进程管理(L11 待实测)。
- `single_instance.rs`:单实例守护与启动意图转发(已实现)。
@@ -1,63 +1,22 @@
# 06:事件、会话与取消
入口:[Core events.rs](../../openless-all/app/crates/openless-core/src/events.rs)、[types.rs](../../openless-all/app/crates/openless-core/src/types.rs)、[Linux主循环](../../openless-all/app/linux-egui/src/main.rs)。
当前Linux已有事件消费/重放和若干时序修复,正式UI应保留这些语义,不仅复制页面布局。
状态:canonical(2026-09-07 以源码为准重写);更新:2026-09-07。
## 1. 订阅与恢复
入口:Core `events.rs` / `types.rs`、`linux-egui/src/lib.rs`(`LinuxHost`)、`linux-egui/src/main.rs`(主循环)。
1. 建立订阅后初始化Core,读取startup和领域快照,检查`running`及`contract_version`。
2. 按`sequence`去重,按`session_id`归属分发;不能将任何最新事件都归给当前页面。
3. 消费落后时使用已有replay/快照恢复机制;不要跳过缺失事件后继续在旧状态上追加。
4. 配置变更按revision刷新缓存;UI草稿不能反向覆盖较新的后台配置。
5. 事件到达后请求重绘;不要在每帧启动新订阅、网络请求或后台任务。
## 1. 订阅与消费
恢复时保留各领域的不同策略:听写恢复状态,QA以完整messages校准,Less Computer按轮归属处理;不是一个通用“取最新事件”就能替代。
- `LinuxHost::subscribe()` 返回 `EventSubscription`;`drain_events()` 批量取走语义事件交给 UI reducer。
- 主循环必须在每帧消费事件;导航切换不得停止事件消费或丢弃会话(QA/Selection/Agent 的后台事件保留操作或导航提示)。
听写`DictationStateSnapshot.recording_ready`的JSON字段为`recordingReady`,每轮初始为`false`;`phase=recording`仅说明原生启动已返回,不能据此提前显示麦克风就绪。AudioRecorder必须先向consumer交付非空PCM,再报告该帧的level;Core仅在这个首帧回调后将`recordingReady`置为`true`,即使首帧是`elapsedMs=0,level=0`也必须发布状态。不得用启动定时器或预填零电平伪造首帧。UI在`starting/recording`且`recordingReady=false`时显示待命;终态退出待命,新会话重新从`false`开始。旧JSON缺字段时Core按`false`读取。
## 2. 会话与取消
Less Computer语音提供`voice_state {sessionId, phase, level, elapsedMs}`及`BackendEventPublisher::latest_less_computer_voice_state()`固定一条最新投影。`phase`为`starting/recording/transcribing/idle`;投影保留原seq,供阶段事件被有界replay驱逐后恢复显示,不能推进聊天去重水位。Linux Host/UI接手此显示消费;旧session的Idle或迟到电平不得覆盖当前录音。
该语音投影复用相同首PCM合同:原生start返回后仍保留`starting`,首帧level才转为`recording`;Host/UI不能自行补发就绪态。
- 所有长操作走 Core session:创建/取消/终态由 Core 裁决,UI 只触发与显示。
- Remote stop 保持可取消的 session;迟到结果按 epoch 丢弃(桌面同规则,见[桌面验收](../2.0-desktop-acceptance.md)划词域)。
- 事件顺序与重放语义以 `contract/backend-2.0.json` 的 `backendEvent` 与 Core `events.rs` 为准;UI 不得自行重排或合并事件。
## 2. TranscriptDelta不是简单追加
## 3. 已有时序约定(保留,不因重写界面丢失)
`offset`表示Unicode scalar(Rust `char`)数量,不是UTF-8字节或UTF-16单元。
新文本为旧文本前`offset`个scalar加`delta.text`,原尾段被替换。优先复用`TranscriptAccumulator`。
例:已有`你好世`,收到`offset=2,text="世界"`,结果是`你好世界`,不是`你好世世界`。
中文、emoji、provider interim回修与最终delta都需测试。越界offset是错误,不截断猜测;迟到的上一轮delta不能改新轮文本。
## 3. QA与Less Computer的归属
- QA对话owner与每轮录音token不同。停止录音使用该轮token,不能把延迟的静音回调转换为无条件toggle。
- QA增量事件的`messages=None`并不表示清空历史;追加chunk,完整消息到达时再校准。
- 面板关闭/重开时丢弃旧owner的迟到输出;语音finish期间仍须能取消实际ASR。
- Less Computer每个新User轮更新当前session,`fresh`仅决定是否清除会话显示;续聊也必须接收新轮输出与审批。
- 工具/审批事件按Core标识关联,不在UI另建可绕过Core的approval token registry。
- Less Computer录音反馈使用typed `voice_state`:`sessionId`、`phase`(starting/recording/transcribing/idle)、`level`、`elapsedMs`。与同一事件流的sequence一起消费,旧session的idle/电平不能清除新录音;Linux页面接入这组反馈仍由egui团队完成。
源码参考:[qa.rs](../../openless-all/app/linux-egui/src/qa.rs)、[Core QA/Agent接口](../../openless-all/app/crates/openless-core/src/domains.rs)。
## 4. 取消和原生资源
共享语音使用Busy拒绝新会话。停止录音、取消整个会话和正常结束不是同一动作。
异步启动前登记owner;每个慢await返回后核验身份和取消状态,迟到得到的资源直接关闭。
录音移交给finish后仍要保留共享取消句柄;不能`take()`唯一资源后让取消路径找不到ASR。
Core已有capture控制与单一终态规则;Host保留有效句柄并如实返回错误,UI只显示最终状态。
Core资源hold覆盖在途原生初始化与收尾;关闭页面、取消回复或丢弃调用future不能让旧任务恢复新会话的静音/输入源。不要把Core暂时返回Busy当成可以在Host绕开的锁。
Remote socket下行只发本连接所属session;客户端stop后到finish结束之间仍保留取消路由。
停止native listener/服务后再shutdown Core;退出过程中不接新业务。
## 5. 插入结果
- `Inserted`:原生确认写入;`PasteSent`:派发粘贴但不能证明目标接收。
- `CopiedFallback`:已经复制且需要用户处理;`NotRequested`:本次没有请求插入。
- `OutcomeUnknown`是错误语义:原生可能已写入,不能自动重试造成双写。
UI通知、历史和Remote结果不得把这些统一显示为“输入成功”。
## 6. 最小回归集合
重复/乱序/lag重放、Unicode回修、两轮QA、两轮Less Computer、旧session迟到事件;Starting/录音/转写/finish各阶段取消;旧静音回调到达新轮;面板关闭重开;插入未知结果不重复提交。
复用[host contract](../../openless-all/app/linux-egui/tests/host_contract.rs)和Core各领域contract;fixture通过之后仍需验证原生Host效果。
- 听写终态、录音停止与音量恢复事件必须成对(见[05](05-native-host-and-data.md) L03)。
- 审批/待办事件跨页存活;审批动作固定在页面外展示。
- 启动阶段先消费 startup snapshot,再消费增量事件(见[01](01-core-contract.md))。
+20 -62
View File
@@ -1,72 +1,30 @@
# 07:验收和证据
# 07:验收与证据
状态:canonical(2026-09-07 以源码为准重写);更新:2026-09-07。
## 1. 两个不同的完成门
- **本批Core移交门**:可调用的`2.0.0`合同、真实共享业务、平台Interface、无设备示例/fixture、跨平台依赖检查和逐项缺口文档。
- **egui团队Linux产品门**:补齐[登记项](./02-gap-register.md),取得真实桌面、设备、安装升级与正式分发证据。
- **本批 Core 移交门**:可调用的 `2.0.0` 合同、真实共享业务、平台 Interface、无设备示例/fixture、跨平台依赖检查、逐项缺口文档(已完成,见[01](01-core-contract.md))。
- **egui 团队 Linux 产品门**:补齐[登记项](02-gap-register.md) L01–L12,取得真实桌面、设备、安装升级与正式分发证据。
第二项不阻塞本批Windows/macOS 2.0;共享Core缺陷或不可调用的承诺接口仍属于本批问题。
Windows/macOS自己的完整功能与设备验收见[桌面清单](../2.0-desktop-acceptance.md)。
## 2. 自动验证(在 `openless-all/app/` 执行)
## 2. 无设备验证
在仓库`openless-all/app`目录执行;Linux原生依赖安装方式沿用[CI](../../.github/workflows/ci.yml),不要把Windows上的Linux stub编译当作Linux目标验证。
```sh
cargo test -p openless-core --locked
cargo test -p openless-linux-egui --locked
cargo run -p openless-linux-egui --example headless_host --locked
cargo check -p openless-linux-egui --all-targets --locked
pwsh -NoProfile -File "scripts/check-core-deps.ps1"
pwsh -NoProfile -File "scripts/check-core-deps.ps1" -Package openless-linux-egui
pwsh -NoProfile -File "scripts/check-core-secret-surface.ps1"
pwsh -NoProfile -File "scripts/check-core-test-isolation.ps1"
pwsh -NoProfile -File "scripts/check-core-runtime-seam.ps1"
pwsh -NoProfile -File "scripts/check-linux-public-surface.ps1"
```
headless示例使用fixture,不证明音频/fcitx5/Secret Service可用。上述命令是接手方可复用的验证入口,不表示本次文档更新重新执行过测试。
插件从仓库根目录构建,在已安装Fcitx5开发依赖的Linux环境执行;build目录使用独立路径:
```sh
cmake -S "openless-all/scripts/linux-fcitx5-plugin" -B "build/linux-fcitx5-contract" -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=ON
cmake --build "build/linux-fcitx5-contract"
ctest --test-dir "build/linux-fcitx5-contract" --output-on-failure
```
这验证C++目标合同;不替代真实前台应用输入。保留发行版最低Fcitx版本编译覆盖,避免只在较新本机头文件通过。
## 3. 必须显式执行的环境测试
返回仓库的`openless-all/app`目录。以下测试默认ignored,只有在适当的测试账号、运行中服务和设备上才执行:
```sh
cargo test -p openless-linux-egui --test cpal_contract -- --ignored
cargo test -p openless-linux-egui --test fcitx5_contract -- --ignored
OPENLESS_RUN_SECRET_SERVICE_CONTRACT=1 cargo test -p openless-linux-egui --test secret_service_contract -- --ignored
```
以上为Linux shell命令。Secret Service须在已启动测试keyring的DBus会话中执行;其显式环境开关只授权该次测试。
先阅读相应[测试文件](../../openless-all/app/linux-egui/tests):fcitx5测试会修改热键并尝试向焦点目标提交文本,必须使用隔离桌面/测试编辑器,结束后恢复配置;keyring测试会写入并移除测试凭据。
CPAL测试允许明确分类的无设备/权限错误,fcitx5部分调用也允许不可用错误;测试绿色不等于录到音频或成功落字,仍需下面的真机流程。不要导出真实密钥。
| 环境/领域 | 必须记录的实际结果 |
| 验证 | 命令 |
| --- | --- |
| X11与Wayland,声明支持的桌面环境 | 安装/加载插件、后台全局热键、重绑/冲突/重载、主窗与托盘行为;分别声明限制 |
| GTK/Qt/浏览器/终端 | 普通/流式Unicode落字、换行、切焦点、目标失效、选区确认/撤回、未知结果不双写 |
| 音频 | 真麦克风、切换/拔出、静音恢复、录音archive/重试/重转、Starting至finish全过程取消 |
| Provider/vault/model | 锁定与解锁、保存失败恢复、旧数据幂等迁移、真实云请求、Qwen推理与取消/释放 |
| QA/Selection Voice/Agent | 多轮、意图/预览、审批/拒绝、旧事件隔离、关闭重开、CLI stdin与进程树清理 |
| Remote | 真手机LAN/TLS、PIN更新、首帧、并发隔离、断连/重连、端口冲突与stop后cancel |
| deb/rpm/AppImage | 内容/依赖、安装/启动、插件路径、1.x升级数据、卸载/回滚、权限和签名/更新 |
| 格式 | `cargo fmt --check`(openless-core / linux-egui / src-tauri) |
| Core | `cargo test -p openless-core --locked` |
| Linux Host/合同 | `cargo test -p openless-linux-egui --locked` |
| Linux 目标编译 | `cargo check -p openless-linux-egui --all-targets --target x86_64-unknown-linux-gnu --locked`(需配置好带 `core/std` 的目标工具链) |
| 打包 | `release-linux-egui.yml`(deb/rpm/AppImage,独立 manifest) |
仅包内文件存在、`--help`成功或CI绿色,不能证明上述完整用户流程。
## 3. 真机矩阵(L11/L12)
## 4. Linux分发与回报
- 桌面会话:X11 与 Wayland 分开记录。
- 输入:fcitx5 插件热键路由、PRIMARY 选区、落字目标应用矩阵。
- 音频:CPAL 设备、系统静音/恢复终态。
- 凭据:Secret Service(含锁定/未解锁态)。
- 打包:AppImage / deb / rpm 安装、升级、回滚、卸载残留;签名与分发渠道证据。
已有[Linux打包脚本](../../openless-all/app/scripts/package-linux-egui.sh)和[独立工作流](../../.github/workflows/release-linux-egui.yml)供复用。
无正式签名、manual URL或调试签名的制品按验证产物记录,不宣传为正式更新源;只有管理员可按[发布政策](../../RELEASING.md)发布。
## 4. 记录格式
每项关闭记录:`L编号 / commit / OS与桌面版本 / 前提 / 操作 / 期望 / 实际 / 自动日志 / 设备证据 / 仍有限制`。
接口不足附最小复现、期望Core方法/事件/错误回报Core负责人;平台实现或UI缺口由egui团队继续负责。
`ID / owner / commit / 已完成效果 / 自动证据 / 设备证据 / 剩余限制`。“待实测”不是“未实现”,也不是“已完成”;验收报告按证据分级表述(见[范围](../2.0-requirements.md)第 4 节)。
+18 -28
View File
@@ -1,36 +1,26 @@
# Linux egui 接入交接
# Linux 接入交接总览
更新:2026-09-06;接口以本目录所在提交的代码为准。初始缺口盘点基于`2b315572`,后续资源生命周期与VoiceState合同更新见01/06。需求依据:[2.0范围](../2.0-requirements.md)。
状态:canonical(2026-09-07 以源码为准重写,基线 `openless-linux-egui` + Core `639c2fbf` 后的 beta);更新:2026-09-07。
本批2.0完整交付Windows/macOS。Linux交付共享Core及接入资料;egui团队负责余下Linux Host、UI、集成和Linux发布验收。
已有`linux-egui`代码和包构建是可复用起点,不等于Linux产品已完成。
## 1. 责任与范围
## 阅读顺序
Linux Host Adapter 的剩余实现、全局热键、窗口/托盘/权限/更新等宿主效果、egui 界面与 Linux 产品验收由 egui 团队承接;共享业务全部在 `openless-core`,发现业务接口缺项时回报 Core 负责人,不在 UI/Host 复制规则。Linux 产品完成度不阻塞 Windows/macOS 交付(见[范围](../2.0-requirements.md))。
| 文档 | 回答什么问题 |
| --- | --- |
| [01 Core接入合同](./01-core-contract.md) | 从哪里接、哪些规则归Core、生命周期和秘密边界 |
| [02 缺口与实施顺序](./02-gap-register.md) | 当前Linux还缺什么、由谁做、依赖顺序和关闭标准 |
| [03 全局热键与窗口](./03-hotkeys-and-windows.md) | 三类1.x X11热键缺口及原生注册、恢复和窗口效果 |
| [04 页面与领域操作](./04-ui-domains.md) | 已有页面能做什么、哪些Core能力尚缺操作入口 |
| [05 原生宿主与数据](./05-native-host-and-data.md) | 音频/插入/凭据/模型/进程/托盘/更新的接入与验证 |
| [06 事件与会话](./06-events-and-sessions.md) | sequence、重放、Unicode增量、QA/Agent和取消时序 |
| [07 验收与证据](./07-acceptance.md) | 自动命令、设备矩阵、Linux发布条件和回报模板 |
## 2. 已有起点(直接复用,不另建第二套后端)
## 工作规则
- 组装:`linux-egui/src/backend.rs` — `LinuxBackendBuilder::from_shared_providers(BackendConfig)` + `with_*` 注入(recorder/polisher/inserter/credential store/host actions/settings runtime/local ASR runtime/polish failure policy)。
- Host 门面:`lib.rs` `LinuxHost` — `snapshot` / `subscribe` / `save_settings` / `update_settings_strict` / `drain_events` / `backend()`。
- 已实现模块:`audio.rs`(CPAL 录制)、`credentials.rs`(Secret Service)、`fcitx5.rs`(输入/选区)、`hotkeys.rs`(fcitx5 热键监听)、`selection.rs`、`qa.rs`、`coding_agent.rs`、`marketplace.rs`、`remote_input.rs`、`settings.rs`、`single_instance.rs`、`capabilities.rs`、`resources.rs`、`runtime.rs`、`ui_state.rs`。
- 页面:`ui_state.rs` `Page` 枚举 10 页(Start/Dictation/Qa/Selection/Agent/Services/Models/Remote/History/Settings),主循环 `main.rs`。
- 打包:`release-linux-egui.yml`(deb/rpm/AppImage,独立 manifest)。
- 先复用现有Core和Linux Adapter;不复制Tauri业务、不让UI读取秘密或私有Coordinator状态。
- Linux系统功能由团队实现Host Adapter,egui框架只提供UI能力,不会自动注册全局热键或管理系统服务。
- Core接口不足:带最小复现、所需业务语义和期望事件回报Core负责人;不要在UI临时补另一套规则。
- 文档中的“已有”仅表示本快照有相应代码/合同;设备与发布证据单独确认。
- 任务关闭时更新[缺口登记](./02-gap-register.md)及对应小文档,附准确commit、测试和设备结果。
## 3. 阅读顺序
## 入口文件
1. [01 Core 合同](01-core-contract.md):可调用的 Core 面与合同文件。
2. [02 缺口登记](02-gap-register.md):L01–L12 现状、责任与关闭标准(推进主表)。
3. [03 热键与窗口](03-hotkeys-and-windows.md)、[05 原生宿主与数据](05-native-host-and-data.md):接线缺口细节。
4. [04 页面与领域](04-ui-domains.md):10 页现状与界面缺口。
5. [06 事件与会话](06-events-and-sessions.md):订阅、重放、取消语义。
6. [07 验收](07-acceptance.md):两个完成门与证据要求。
- [Core公开出口](../../openless-all/app/crates/openless-core/src/lib.rs)
- [Linux生产factory](../../openless-all/app/linux-egui/src/backend.rs)
- [LinuxHost facade](../../openless-all/app/linux-egui/src/lib.rs)
- [现有egui主循环/UI](../../openless-all/app/linux-egui/src/main.rs)
- [headless示例](../../openless-all/app/linux-egui/examples/headless_host.rs)
旧的[长接口文档](../linux-egui-backend-contract.md)保留历史参考;范围冲突以新需求和本目录为准。
接口细节见[后端契约](../linux-egui-backend-contract.md);长接口与历史实现参考保留在契约文档。
+19 -44
View File
@@ -1,51 +1,26 @@
# qwen-asr submodule 升级检查清单
# qwen-asr 子模块升级清单
`openless-all/app/src-tauri/vendor/qwen-asr` 是 macOS 本地 Qwen3-ASR 的 C 引擎来源。OpenLess 只从组织 fork `https://github.com/Open-Less/qwen-asr.git` 拉取 submodule;`antirez/qwen-asr` 只作为上游同步来源,不直接进入主仓构建链路。
状态:canonical(2026-09-07 以源码为准重写);更新:2026-09-07。
## 升级原则
## 1. 子模块(`.gitmodules`,仓库根)
- 不直接把 `.gitmodules` 改回个人上游仓库。
- 不在未审查 upstream diff 的情况下推进 submodule commit。
- 每次升级只推进 submodule 指针;除非编译或 FFI 必需,不混入 OpenLess 主项目逻辑改动。
- 保留当前锁定 commit 的可回滚性,必要时能 `git checkout <old-commit> -- openless-all/app/src-tauri/vendor/qwen-asr` 回退。
| 子模块 | 路径 | 来源 |
| --- | --- | --- |
| qwen-asr | `openless-all/app/src-tauri/vendor/qwen-asr` | <https://github.com/Open-Less/qwen-asr.git>(组织 fork;`antirez/qwen-asr` 只作上游同步来源,不直接进构建) |
| qwen3-asr-rs | `openless-all/app/src-tauri/vendor/qwen3-asr-rs` | <https://github.com/Open-Less/qwen3_asr_rs.git> |
## 操作步骤
qwen-asr 是 macOS 本地 Qwen3-ASR 的 **C 引擎**,由 src-tauri 构建链接(vendored);qwen3-asr-rs 为 Rust 运行时来源。Linux workspace(openless-core + linux-egui)不包含 src-tauri,不依赖这两个子模块。
1. 在 `Open-Less/qwen-asr` fork 中同步 upstream。
2. 审查 fork 中待引入 commit:
- C 源码:`qwen_asr*.c`、`qwen_asr*.h`
- 构建脚本 / 模型下载脚本
- 新增二进制、大文件、网络下载地址或 shell 命令
- FFI API 是否改变:`qwen_load`、`qwen_free`、`qwen_set_token_callback`、`qwen_transcribe_audio`、`qwen_transcribe_stream`
- Dictation 行为是否改变:C 后端必须继续使用 stream、发送 `local-asr-token`,并保留 0.5 秒尾部静音收尾;MLX 后端保持 batch
3. 在 OpenLess 主仓更新 submodule:
```bash
git submodule sync --recursive openless-all/app/src-tauri/vendor/qwen-asr
git submodule update --init --recursive openless-all/app/src-tauri/vendor/qwen-asr
cd openless-all/app/src-tauri/vendor/qwen-asr
git fetch origin
git checkout <reviewed-commit>
cd -
```
4. 验证 submodule 来源仍是组织 fork:
```bash
git config --file .gitmodules --get submodule.openless-all/app/src-tauri/vendor/qwen-asr.url
git -C openless-all/app/src-tauri/vendor/qwen-asr remote get-url origin
git submodule status openless-all/app/src-tauri/vendor/qwen-asr
git diff --submodule=log -- openless-all/app/src-tauri/vendor/qwen-asr
```
5. 至少运行:
```bash
cd openless-all/app
npm run build
cargo check --manifest-path src-tauri/Cargo.toml
```
macOS 发布前还要用 `INSTALL=0 ./scripts/build-mac.sh` 验证 C 源编译和链接。
## 2. 升级步骤
## PR / commit 说明必须包含
1. `git submodule update --init --recursive`(先在干净工作区)。
2. 在子模块目录 `git fetch && git checkout <目标提交>`(只用 Open-Less fork 的提交;记录提交号)。
3. 主仓 `git add` 子模块指针并提交(提交信息注明引擎版本/目的)。
4. macOS 构建验证:`src-tauri` 按平台构建通过;如涉及 Metal/工具链,先跑 `npm run check:macos-metal-toolchain`;相关契约:`scripts/macos-compiler-runtime-contract.test.mjs`、`stage-macos-mlx-metallib.mjs`(MLX 路径)。
5. CI 注意:`scripts/ci-disable-macos-qwen3.mjs` 控制 macOS CI 中 qwen3 的启用范围,升级后核对其仍符合当前 CI 策略。
6. 行为验证:本地 ASR 下载/激活/取消与听写回归(见[桌面验收](2.0-desktop-acceptance.md)本地模型域)。
- 旧 submodule commit 和新 submodule commit。
- 已审查的 upstream commit 范围。
- 是否有 FFI API、模型文件结构、下载脚本或构建参数变化。
- 是否改变 stream/batch、token 回调、取消门控或尾部静音语义。
- 已运行的验证命令和未覆盖的平台。
## 3. 纪律
- 不修改 vendored 引擎源码本身的问题;修复走 fork 上游,再 bump 指针。
- 子模块指针升级必须与构建验证同批提交,不留"指针先行"状态。
+32 -6
View File
@@ -1,10 +1,36 @@
# Tauri CSP 边界
OpenLess 的桌面 WebView 已在 `openless-all/app/src-tauri/tauri.conf.json` 启用最小 CSP。该策略只放开前端渲染和 Tauri IPC 必需的来源:
状态:canonical(2026-09-07 以源码为准重写);更新:2026-09-07。
- `script-src 'self'`:脚本只允许随应用打包的前端产物;Tauri 会在构建时为自身注入脚本补齐必要 nonce / hash。
- `connect-src 'self' ipc: http://ipc.localhost http://localhost:1420 ws://localhost:1420`:允许同源连接、Tauri IPC,以及 Vite 开发模式的本机 HTTP / HMR 连接。
- `style-src 'self' 'unsafe-inline' https://fonts.googleapis.com` 与 `font-src https://fonts.gstatic.com`:当前 React 页面大量使用 `style={{ ... }}` 保持设计稿像素对齐,因此短期保留 inline style;外部字体只允许 Google Fonts 相关域名。
- `object-src 'none'`、`base-uri 'none'`、`form-action 'none'`、`frame-ancestors 'none'`:关闭插件对象、base URL、表单提交和嵌入入口。
桌面 WebView 的 CSP 定义在 `openless-all/app/src-tauri/tauri.conf.json`(`app.security.csp`),当前值为:
Provider 校验、ASR/LLM 请求、更新检查、本地 ASR 模型下载等网络访问都在 Rust / Tauri plugin 侧执行,不通过 WebView 的 `fetch` 直连外部服务,因此不在 WebView CSP 中放开 provider 域名。QA markdown 的 sanitizer 仍是第一道防线;CSP 只作为纵深防御。
```
default-src 'self' customprotocol: asset:
script-src 'self'
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com
font-src 'self' https://fonts.gstatic.com
img-src 'self' asset: http://asset.localhost blob: data: https://github.com https://avatars.githubusercontent.com
connect-src 'self' ipc: http://ipc.localhost http://localhost:1420 ws://localhost:1420
media-src 'self' data: asset: http://asset.localhost blob:
object-src 'none'
base-uri 'none'
form-action 'none'
frame-ancestors 'none'
```
## 放开项与用途
| 指令 | 用途 |
| --- | --- |
| `default-src 'self' customprotocol: asset:` | 仅打包产物与 Tauri asset 协议 |
| `script-src 'self'` | 脚本只允许应用自带前端产物(Tauri 构建时注入 nonce/hash) |
| `style-src` + Google Fonts | 界面样式与在线字体 |
| `img-src` 的 github/avatars | GitHub OAuth 头像(`GithubLoginModal`);`asset:/asset.localhost/blob:/data:` 服务于本地资源与音频波形 |
| `connect-src` 的 `ipc:/ipc.localhost` | Tauri IPC;`localhost:1420` 仅为 Vite 开发/HMR |
| `media-src` | 录音回放与提示音 |
`object-src`、`base-uri`、`form-action`、`frame-ancestors` 全部关闭。
## 修改规则
新增外部来源必须先确认用途与最小化范围(只加到对应指令),不得放宽 `object-src/base-uri/form-action/frame-ancestors`;改动随 `npm test` 与安全审查一起验证。
+12 -43
View File
@@ -1,50 +1,19 @@
# OpenLess 火山 ASR 配置
# 火山引擎(volcengine)ASR 配置
1. 登录火山引擎
<https://console.volcengine.com/auth/login/>
状态:canonical(2026-09-07 以源码为准重写);更新:2026-09-07。
![登录火山引擎](./images/volcengine-setup/01-login.png)
## 1. 代码中的定义
2. 创建旧版应用
创建时勾选 `豆包流式语音识别模型2.0 小时版`
<https://console.volcengine.com/speech/app?opt=create>
- Provider:`volcengine`(labelKey `asrVolcengine`),定义于 Core `provider_rules.rs`;`authRequirement = Volcengine`(专用鉴权形态),无内置默认端点/模型(`defaultEndpoint` / `defaultModel` 为空,按通道配置)。
- 验证探针:`asr_silence_allows_no_final`(静音段允许无 final 帧,验证以可取消的静音探测完成)。
- 凭据字段(`provider_rules.rs:300-302`):`volcengine_auth_mode`(鉴权模式,如 ApiKey/官方端点模式)、`volcengine_app_key`、`volcengine_access_key`(布尔项 + 模式选择;具体取值在设置界面录入,凭据走系统安全存储,不落明文)。
![创建旧版应用并勾选小时版](./images/volcengine-setup/02-create-legacy-app.png)
## 2. 在应用内配置
3. 打开豆包流式语音识别模型 2.0 管理页
`APP ID` 和 `Access Token` 在页面最下方
<https://console.volcengine.com/speech/service/10038?AppID=&opt=create>
设置 → AI 服务 → 语音识别 → 添加渠道,选择火山引擎;按界面提示填入鉴权字段,保存后执行“验证”得到真实验证结果(成功/失败与时间会记录在渠道列表)。
![流式语音识别模型 2.0 管理页](./images/volcengine-setup/03-streaming-asr-page.png)
## 3. 端点与排错
4. 复制到 OpenLess 的 `Settings` 页面
打开:
`Settings -> Providers -> ASR`
![复制到 OpenLess 的 Settings 页面](./images/volcengine-setup/04-openless-settings.png)
填这两个:
- `APP ID`
- `Access Token`
不用填:
- `Secret Key`
## 新版控制台(API Key 方式)
新版豆包语音控制台统一使用单个 `API Key` 鉴权,无需 `APP ID` / `Access Token`(旧版应用方式见上文)。
1. 在新版语音控制台创建 API Key
<https://console.volcengine.com/speech/new/setting/apikeys>
2. 在 OpenLess 的 `Settings -> Providers -> ASR` 中:
- 鉴权模式选择「新版控制台 API Key」
- 填入上一步创建的 `API Key`
- `Resource ID` 保持默认 `volc.seedasr.sauc.duration`(豆包流式语音识别模型 2.0 · 小时版)
新旧两种模式共享同一 WebSocket 端点(`wss://openspeech.bytedance.com/api/v3/sauc/bigmodel_async`),仅握手鉴权头不同(新版为 `X-Api-Key` 单头)。官方接口文档:<https://www.volcengine.com/docs/6561/1354869>
- ApiKey 模式使用火山官方实时 ASR 端点(历史修复 #931 后的行为,以 `crates/openless-core/src/asr/volcengine.rs` 当前实现为准)。
- 弱网行为:连接超时与重试在 Host/Core 实现,失败信息展示在渠道验证结果中。
- 开通服务、创建应用与获取密钥属火山控制台操作,以[火山官方文档](https://www.volcengine.com/docs)为准;本仓库只维护代码行为。
+12 -32
View File
@@ -1,39 +1,19 @@
# OpenLess 讯飞 ASR 配置
# 讯飞(iflytek)ASR 配置
OpenLess 支持讯飞开放平台的**实时语音转写(RTASR,标准版)**,用于把录音实时转写成文字。
状态:canonical(2026-09-07 以源码为准重写);更新:2026-09-07。
## 所需服务
## 1. 代码中的定义
- 服务:讯飞开放平台 **实时语音转写(标准版)**
- 接口文档:<https://www.xfyun.cn/doc/asr/rtasr/API.html>
- Provider:`iflytek`(labelKey `asrIflytek`),`authRequirement = Xfyun`;无内置默认端点/模型。
- 凭据(`crates/openless-core/src/asr/xfyun.rs:62-69`):`app_id` + `api_key`(两者非空才视为已配置);连接时以 `app_id + timestamp` 计算 `signa` 签名(`compute_signa`,xfyun.rs:637)。
- 验证探针:`asr_silence_allows_no_final`。
- 对接的是讯飞开放平台实时语音转写(RTASR);协议细节以 `asr/xfyun.rs` 当前实现为准。
> 说明:录音文件转写(IFASR)、实时语音转写大模型(202 方言 / 37 语种免切)目前
> **未接入**。若你有这些需求,欢迎在 issue #842 留言。
## 2. 在应用内配置
## 凭证获取
设置 → AI 服务 → 语音识别 → 添加渠道,选择讯飞;填入 AppID 与 API Key,保存后执行“验证”。验证结果(成功/失败与时间)显示在渠道列表。
1. 登录讯飞开放平台:<https://www.xfyun.cn/>
2. 控制台 → 我的应用 → 创建应用,添加 **实时语音转写** 服务(需先在服务页领取免费额度或购买)。
3. 在服务管理页查看并复制 **AppID** 与 **API Key**。
4. 打开 OpenLess `Settings → Providers → ASR`,选择「讯飞实时语音转写」,填入 AppID 与 API Key。
## 3. 开通与排错
## 音频约束
- 采样率 16 kHz、位深 16 bit、单声道 PCM(与 OpenLess 录音输出完全一致,无需转码)。
- 支持中文普通话、中英混合与英文;方言/小语种需在讯飞控制台开通后由 `lang` 参数指定,
首期 OpenLess 固定使用中文普通话(`lang=cn`)。
- IP 白名单:若在讯飞控制台开启了 IP 白名单,请把运行 OpenLess 机器的公网 IP 加入,
否则握手会返回 `10105 illegal access`。
## 已知限制
- 标准版 RTASR **没有请求参数级热词**:OpenLess 的词典热词不会传给讯飞,个性化热词
请在讯飞控制台上传。
- 实时出字由服务端驱动:OpenLess 的胶囊 UI 在收尾阶段一次性展示最终文本(与其它
流式 provider 行为一致)。
## 排障
- `10105` / `10110`:AppID / API Key 错误、IP 白名单未放行,或账号未开通实时语音转写服务。
- `10800`:超过授权连接数(并发路数已满)。
- `37005`:超过 15 秒未发送音频(弱网时偶发,可重试)。
- 服务开通、AppID/API Key 获取在[讯飞开放平台控制台](https://www.xfyun.cn/)完成(外部平台操作,以官方文档为准)。
- 鉴权失败多为 AppID 与 Key 不配套或时间偏差;渠道验证结果会给出可读错误。