diff --git a/docs/2.0-desktop-acceptance.md b/docs/2.0-desktop-acceptance.md index 7b0563e8..41d28ab3 100644 --- a/docs/2.0-desktop-acceptance.md +++ b/docs/2.0-desktop-acceptance.md @@ -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 证据分别记录;未执行项不得改写为通过。设备/安装/升级/回滚验收是发布门,不属于本清单可由代码自证的部分。 diff --git a/docs/2.0-requirements.md b/docs/2.0-requirements.md index 6da68cae..884ff872 100644 --- a/docs/2.0-requirements.md +++ b/docs/2.0-requirements.md @@ -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 原生结果。 diff --git a/docs/android-mobile-apk-overlay-plan.md b/docs/android-mobile-apk-overlay-plan.md index 34570a8c..4e62b3c3 100644 --- a/docs/android-mobile-apk-overlay-plan.md +++ b/docs/android-mobile-apk-overlay-plan.md @@ -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__arm64-v8a.apk` | `OpenLess-android-debug-arm64-v8a-run-.apk` | -| armeabi-v7a | `OpenLess__armeabi-v7a.apk` | `OpenLess-android-debug-armeabi-v7a-run-.apk` | -| x86 | `OpenLess__x86.apk` | `OpenLess-android-debug-x86-run-.apk` | -| x86_64 | `OpenLess__x86_64.apk` | `OpenLess-android-debug-x86_64-run-.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 -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: +- 主听写链路:`src-tauri/src/coordinator/dictation.rs` +- Windows IME unavailable 模式对照:`src-tauri/src/windows_ime_profile.rs` diff --git a/docs/architecture.md b/docs/architecture.md index 62f9aad8..0452f73e 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -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 引擎)。 diff --git a/docs/bailian-asr-models.md b/docs/bailian-asr-models.md index 5d1590ab..beb37f79 100644 --- a/docs/bailian-asr-models.md +++ b/docs/bailian-asr-models.md @@ -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),选择模型;保存后执行“验证”。 diff --git a/docs/index.md b/docs/index.md index dd955816..0122ca08 100644 --- a/docs/index.md +++ b/docs/index.md @@ -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`。 diff --git a/docs/linux-egui-backend-contract.md b/docs/linux-egui-backend-contract.md index 4aa15579..aa803fcf 100644 --- a/docs/linux-egui-backend-contract.md +++ b/docs/linux-egui-backend-contract.md @@ -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)`、`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:` 形式返回。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)` 只接受非空、偶数字节的 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`。`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`,属于有意的破坏性 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>` - 取代,使 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)分别记录。 diff --git a/docs/linux-egui-handoff/01-core-contract.md b/docs/linux-egui-handoff/01-core-contract.md index 28193028..4fe1ac77 100644 --- a/docs/linux-egui-handoff/01-core-contract.md +++ b/docs/linux-egui-handoff/01-core-contract.md @@ -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))。 diff --git a/docs/linux-egui-handoff/02-gap-register.md b/docs/linux-egui-handoff/02-gap-register.md index 3a65a361..19777c01 100644 --- a/docs/linux-egui-handoff/02-gap-register.md +++ b/docs/linux-egui-handoff/02-gap-register.md @@ -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 / 已完成效果 / 自动证据 / 设备证据 / 剩余限制`。“待实测”不是“未实现”,也不是“已完成”。 diff --git a/docs/linux-egui-handoff/03-hotkeys-and-windows.md b/docs/linux-egui-handoff/03-hotkeys-and-windows.md index 6a9522db..a9b6a7f7 100644 --- a/docs/linux-egui-handoff/03-hotkeys-and-windows.md +++ b/docs/linux-egui-handoff/03-hotkeys-and-windows.md @@ -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 节。 diff --git a/docs/linux-egui-handoff/04-ui-domains.md b/docs/linux-egui-handoff/04-ui-domains.md index 39de942d..5baab2a3 100644 --- a/docs/linux-egui-handoff/04-ui-domains.md +++ b/docs/linux-egui-handoff/04-ui-domains.md @@ -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`(当前较大),避免继续单文件增长。 diff --git a/docs/linux-egui-handoff/05-native-host-and-data.md b/docs/linux-egui-handoff/05-native-host-and-data.md index e294c967..a65945a3 100644 --- a/docs/linux-egui-handoff/05-native-host-and-data.md +++ b/docs/linux-egui-handoff/05-native-host-and-data.md @@ -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`:单实例守护与启动意图转发(已实现)。 diff --git a/docs/linux-egui-handoff/06-events-and-sessions.md b/docs/linux-egui-handoff/06-events-and-sessions.md index 38bdf3b6..20bd1125 100644 --- a/docs/linux-egui-handoff/06-events-and-sessions.md +++ b/docs/linux-egui-handoff/06-events-and-sessions.md @@ -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))。 diff --git a/docs/linux-egui-handoff/07-acceptance.md b/docs/linux-egui-handoff/07-acceptance.md index f50e68c3..a75977a9 100644 --- a/docs/linux-egui-handoff/07-acceptance.md +++ b/docs/linux-egui-handoff/07-acceptance.md @@ -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 节)。 diff --git a/docs/linux-egui-handoff/README.md b/docs/linux-egui-handoff/README.md index 81b67681..f6d42f54 100644 --- a/docs/linux-egui-handoff/README.md +++ b/docs/linux-egui-handoff/README.md @@ -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);长接口与历史实现参考保留在契约文档。 diff --git a/docs/qwen-asr-submodule-upgrade-checklist.md b/docs/qwen-asr-submodule-upgrade-checklist.md index 653faa72..d13ab3cf 100644 --- a/docs/qwen-asr-submodule-upgrade-checklist.md +++ b/docs/qwen-asr-submodule-upgrade-checklist.md @@ -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 -- openless-all/app/src-tauri/vendor/qwen-asr` 回退。 +| 子模块 | 路径 | 来源 | +| --- | --- | --- | +| qwen-asr | `openless-all/app/src-tauri/vendor/qwen-asr` | (组织 fork;`antirez/qwen-asr` 只作上游同步来源,不直接进构建) | +| qwen3-asr-rs | `openless-all/app/src-tauri/vendor/qwen3-asr-rs` | | -## 操作步骤 +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 - 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 指针。 +- 子模块指针升级必须与构建验证同批提交,不留"指针先行"状态。 diff --git a/docs/tauri-csp.md b/docs/tauri-csp.md index 2ec05077..e6212a12 100644 --- a/docs/tauri-csp.md +++ b/docs/tauri-csp.md @@ -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` 与安全审查一起验证。 diff --git a/docs/volcengine-setup.md b/docs/volcengine-setup.md index 868daaa6..842a703f 100644 --- a/docs/volcengine-setup.md +++ b/docs/volcengine-setup.md @@ -1,50 +1,19 @@ -# OpenLess 火山 ASR 配置 +# 火山引擎(volcengine)ASR 配置 -1. 登录火山引擎 - +状态:canonical(2026-09-07 以源码为准重写);更新:2026-09-07。 - ![登录火山引擎](./images/volcengine-setup/01-login.png) +## 1. 代码中的定义 -2. 创建旧版应用 - 创建时勾选 `豆包流式语音识别模型2.0 小时版` - +- 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` 在页面最下方 - +设置 → 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 - - -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` 单头)。官方接口文档: +- ApiKey 模式使用火山官方实时 ASR 端点(历史修复 #931 后的行为,以 `crates/openless-core/src/asr/volcengine.rs` 当前实现为准)。 +- 弱网行为:连接超时与重试在 Host/Core 实现,失败信息展示在渠道验证结果中。 +- 开通服务、创建应用与获取密钥属火山控制台操作,以[火山官方文档](https://www.volcengine.com/docs)为准;本仓库只维护代码行为。 diff --git a/docs/xfyun-asr.md b/docs/xfyun-asr.md index 84f6b1ae..34be6519 100644 --- a/docs/xfyun-asr.md +++ b/docs/xfyun-asr.md @@ -1,39 +1,19 @@ -# OpenLess 讯飞 ASR 配置 +# 讯飞(iflytek)ASR 配置 -OpenLess 支持讯飞开放平台的**实时语音转写(RTASR,标准版)**,用于把录音实时转写成文字。 +状态:canonical(2026-09-07 以源码为准重写);更新:2026-09-07。 -## 所需服务 +## 1. 代码中的定义 -- 服务:讯飞开放平台 **实时语音转写(标准版)** -- 接口文档: +- 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. 登录讯飞开放平台: -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 不配套或时间偏差;渠道验证结果会给出可读错误。