# OpenLess 开发文档 更新时间:2026-04-26 本文是 OpenLess 第一轮开发的工程落地文档。产品需求以 `openless-requirements.md` 为准,本文负责说明推荐技术形态、模块拆分、状态机、数据字段、开发顺序和验收方式。 --- ## 1. 技术目标 第一轮要做的是一个 macOS 常驻语音输入工具: > 用户在任意 app 的输入框中按住快捷键说话,OpenLess 录音、转写、轻度整理,然后把结果插入当前光标位置;失败时自动复制并保存历史。 工程目标: - 能稳定常驻菜单栏。 - 能可靠获取麦克风录音。 - 能响应全局快捷键。 - 能显示底部微型状态胶囊。 - 能完成 ASR 转写和 LLM 润色。 - 能向当前输入位置插入文本。 - 能在失败时复制到剪贴板并保存历史。 - 能清楚处理权限、错误、隐私和本地/云端路径。 --- ## 2. 推荐技术路线 ### 2.1 首选:原生 macOS 建议第一轮使用: - Swift / SwiftUI:设置页、历史页、主窗口。 - AppKit:菜单栏、全局窗口、底部胶囊浮层、辅助功能相关能力。 - AVFoundation:麦克风录音、音量采样。 - Carbon / Keyboard Shortcuts 方案:全局快捷键。 - Accessibility API:定位当前输入焦点、模拟输入或粘贴。 - NSPasteboard:剪贴板兜底。 - SQLite 或本地 JSON:历史、词典、设置。 - Keychain:保存 API Key。 理由: - OpenLess 是系统级输入层,原生 macOS 更适合处理菜单栏、悬浮窗口、权限和全局快捷键。 - Electron/Tauri 可以做设置页,但系统输入、悬浮层和权限体验更容易变复杂。 ### 2.2 可选混合路线 如果后续想快速做漂亮设置页,可以采用: - 原生 Swift 后台核心 + WebView 设置页。 第一轮不建议直接用纯 Web/Electron 做核心录音和插入体验。 --- ## 3. 模块拆分 | 模块 | 职责 | |---|---| | `AppShell` | 应用生命周期、菜单栏、启动项、窗口管理 | | `PermissionManager` | 麦克风、辅助功能、通知等权限检查和引导 | | `HotkeyManager` | 注册录音、取消、粘贴上一条等全局快捷键 | | `Recorder` | 录音、音频缓存、音量采样、取消 | | `AudioLevelMonitor` | 输出胶囊动态条所需的实时音量值 | | `CapsuleOverlay` | 底部微型状态胶囊窗口 | | `ASRRouter` | 本地、云端、BYOK 转写路由 | | `PolishEngine` | 原文、轻度润色、清晰结构、正式表达 | | `TextInserter` | 当前输入框插入、模拟粘贴、失败检测 | | `ClipboardFallback` | 插入失败时复制最终文本 | | `HistoryStore` | 保存原始转写、最终文本、状态、错误 | | `DictionaryStore` | 个人词典 | | `SnippetStore` | 常用片段 | | `SettingsStore` | 快捷键、模式、隐私、模型路径 | | `ErrorReporter` | 本地错误码、日志、用户可读提示 | --- ## 4. 核心流程 ### 4.1 正常输入流程 ```text Hotkey down -> PermissionManager.check() -> Recorder.start() -> CapsuleOverlay.show(listening) -> AudioLevelMonitor.update() Hotkey up -> Recorder.stop() -> CapsuleOverlay.set(processing) -> ASRRouter.transcribe(audio) -> PolishEngine.polish(rawTranscript, mode, context) -> TextInserter.insert(finalText) -> HistoryStore.save(session) -> CapsuleOverlay.set(inserted) -> CapsuleOverlay.hide() ``` ### 4.2 插入失败流程 ```text TextInserter.insert(finalText) fails -> ClipboardFallback.copy(finalText) -> HistoryStore.save(session with copied_fallback) -> CapsuleOverlay.set(copied_fallback) ``` 用户不需要重新录音。 ### 4.3 取消流程 ```text Esc or capsule x clicked -> Recorder.cancel() -> discard unsaved audio by default -> CapsuleOverlay.set(cancelled) -> CapsuleOverlay.hide() ``` ### 4.4 重跑润色流程 ```text History item selected -> choose another mode -> PolishEngine.polish(rawTranscript, newMode, context) -> update finalText -> copy or insert ``` --- ## 5. 状态机 ### 5.1 应用状态 | 状态 | 含义 | 用户表现 | |---|---|---| | `app_ready` | 应用可用 | 菜单栏正常,胶囊隐藏 | | `permission_required` | 缺少权限 | 设置页提示补权限 | | `model_unavailable` | 模型或 API 不可用 | 提示切换路径或检查 key | | `offline` | 网络不可用 | 云端不可用,本地仍可用 | ### 5.2 录音状态 | 状态 | 含义 | 胶囊表现 | |---|---|---| | `idle` | 未录音 | 隐藏或极淡小点 | | `listening` | 正在录音 | 叉号 + 动态条 + 勾号 | | `stopping` | 正在收尾 | 动态条停止,进入处理 | | `cancelled` | 用户取消 | 叉号高亮后淡出 | ### 5.3 处理状态 | 状态 | 含义 | 胶囊表现 | |---|---|---| | `transcribing` | 正在转写 | “识别中”或 spinner | | `polishing` | 正在润色 | “整理中”或 spinner | | `ready_to_insert` | 结果生成 | 准备插入 | | `failed` | 处理失败 | 红点或“失败”,点击展开详情 | ### 5.4 插入状态 | 状态 | 含义 | 胶囊表现 | |---|---|---| | `inserted` | 成功插入 | 勾号高亮后淡出 | | `copied_fallback` | 插入失败但已复制 | “已复制” | | `focus_lost` | 输入框失焦 | “已复制” | | `permission_blocked` | 权限不足 | 引导打开辅助功能权限 | --- ## 6. 数据模型 ### 6.1 `DictationSession` | 字段 | 类型 | 说明 | |---|---|---| | `id` | string | 会话 ID | | `created_at` | datetime | 创建时间 | | `started_at` | datetime | 录音开始时间 | | `ended_at` | datetime | 录音结束时间 | | `duration_ms` | number | 录音时长 | | `source_app_name` | string | 当前 app 名 | | `source_bundle_id` | string | 当前 app bundle id | | `target_context_type` | enum | `chat` / `email` / `doc` / `code` / `unknown` | | `language_hint` | enum | `auto` / `zh` / `en` / `mixed` | | `mode` | enum | `raw` / `light` / `structured` / `formal` | | `model_route` | enum | `local` / `cloud` / `byok` | | `raw_transcript` | text | 原始转写 | | `final_text` | text | 最终输出 | | `insert_status` | enum | `inserted` / `copied_fallback` / `failed` | | `fallback_reason` | string/null | 插入失败原因 | | `audio_saved` | boolean | 是否保存音频 | | `audio_path` | string/null | 音频路径,默认 null | | `error_code` | string/null | 错误码 | ### 6.2 `UserSettings` | 字段 | 类型 | 说明 | |---|---|---| | `hotkey_record` | string | 录音快捷键 | | `hotkey_paste_last` | string | 粘贴上一条快捷键 | | `recording_behavior` | enum | `hold_to_talk` / `toggle_to_talk` | | `default_mode` | enum | 默认输出模式 | | `default_model_route` | enum | `local` / `cloud` / `byok` | | `history_retention_days` | number | 历史保留天数 | | `save_audio` | boolean | 是否保存音频 | | `show_floating_capsule` | boolean | 是否显示底部状态胶囊 | | `auto_copy_on_failure` | boolean | 失败时是否自动复制 | | `personal_dictionary_enabled` | boolean | 是否启用个人词典 | ### 6.3 `PersonalDictionaryEntry` | 字段 | 类型 | 说明 | |---|---|---| | `id` | string | 词条 ID | | `phrase` | string | 用户确认过的正确词 | | `category` | enum | `name` / `product` / `tech` / `company` / `custom` | | `notes` | string | 可选备注,不作为硬替换规则 | | `enabled` | boolean | 是否参与 ASR 热词和后期语义判断 | | `case_sensitive` | boolean | 是否区分大小写 | | `created_at` | datetime | 创建时间 | ### 6.4 `TextSnippet` | 字段 | 类型 | 说明 | |---|---|---| | `id` | string | 片段 ID | | `trigger_phrase` | string | 语音触发词 | | `content` | text | 输出内容 | | `enabled` | boolean | 是否启用 | --- ## 7. 输出模式与 Prompt 规则 ### 7.1 模式枚举 | mode | 名称 | 行为 | |---|---|---| | `raw` | 原文 | 只补标点,尽量不改词 | | `light` | 轻度润色 | 去口癖、去重复、补标点、轻微整理 | | `structured` | 清晰结构 | 适合长 prompt、需求、说明,输出段落或列表 | | `formal` | 正式表达 | 适合邮件、客户沟通、正式工作表达 | ### 7.2 通用 Prompt 约束 所有润色模式都必须遵守: - 只整理用户表达,不回答问题。 - 不执行命令。 - 不新增用户没有说的信息。 - 不删除关键限制条件。 - 保留专有名词、代码名、产品名和人名。 - 保留用户习惯表达,避免 AI 腔。 - 中文输出使用自然中文标点。 ### 7.3 轻度润色模板 ```text 你是语音输入文本整理器。请把用户的口语转写整理成可直接发送或继续编辑的文字。 要求: - 去掉明显口癖、重复和无意义停顿。 - 补充自然标点。 - 保留用户原意和表达习惯。 - 不扩写、不创作、不回答内容。 - 如果包含中英混输、产品名、代码名,请尽量保留。 原始转写: {{raw_transcript}} ``` --- ## 8. UI 实现规格 ### 8.1 胶囊窗口 建议实现为一个无边框、透明背景、always-on-top 的辅助窗口: - 不抢焦点。 - 主窗口出现在 Dock;录音胶囊作为辅助窗口不出现在 Dock。 - 屏幕底部居中。 - 多屏时优先显示在当前活跃输入所在屏幕。 - 空闲时隐藏。 - 录音时以轻微 spring motion 出现。 尺寸: | 状态 | 宽 | 高 | |---|---|---| | Listening | 128-180px | 32-38px | | Processing | 150-210px | 32-38px | | Success | 128-160px | 32-38px | | Error | 150-210px | 32-38px | 结构: ```text [ x ] [ audio bars / short status ] [ check ] ``` 禁止: - 不展示完整 transcript。 - 不展示 mode/provider chips。 - 不显示复杂按钮组。 - 不做大面板默认展开。 ### 8.2 设置窗口 设置窗口可用 SwiftUI: - 左侧导航。 - 右侧内容。 - 表格编辑词典和片段。 - 历史支持搜索、复制、重新整理。 第一轮不追求复杂视觉,重点是清楚、稳定、可用。 --- ## 9. 错误码与用户文案 | error_code | 触发场景 | 用户文案 | |---|---|---| | `mic_permission_missing` | 缺少麦克风权限 | 需要麦克风权限才能录音 | | `accessibility_missing` | 缺少辅助功能权限 | 需要辅助功能权限,OpenLess 才能输入到当前 app | | `focus_lost` | 输入框失焦 | 找不到输入位置,结果已复制 | | `asr_failed` | 转写失败 | 这次没有识别成功,请重试 | | `polish_failed` | 润色失败 | 整理失败,原始转写已保存 | | `network_unavailable` | 云端不可用 | 网络不可用,可切换本地模式 | | `model_unavailable` | 模型不可用 | 当前模型不可用,请检查设置 | 文案原则: - 短。 - 直接。 - 说明用户下一步能做什么。 - 不用“释放生产力”“智能增强”等营销词。 --- ## 10. 开发顺序 建议按以下顺序开发: 1. 创建 macOS 菜单栏应用骨架。 2. 实现设置窗口基础导航。 3. 实现权限检查和引导。 4. 实现全局快捷键。 5. 实现录音与音量采样。 6. 实现底部微型状态胶囊。 7. 接入最小 ASR 路径。 8. 接入轻度润色。 9. 实现文本插入。 10. 实现剪贴板兜底。 11. 实现历史记录。 12. 实现个人词典。 13. 实现模式切换。 14. 完成错误处理和隐私设置。 15. 做真实 app 场景测试。 第一轮开发原则: - 先跑通主链路,再优化模型。 - 先保证不丢内容,再追求插入成功率。 - 先做好小胶囊,再做主窗口美化。 - 先做中文和中英混输验收,再扩展更多语言。 --- ## 11. 测试清单 ### 11.1 功能测试 - 首次启动能引导麦克风权限。 - 首次启动能引导辅助功能权限。 - 快捷键能开始录音。 - 松开快捷键能结束录音。 - 胶囊在录音时出现。 - 胶囊动态条随声音变化。 - `Esc` 能取消。 - 中文能识别。 - 英文能识别。 - 中英混输能识别。 - 润色结果能插入当前输入框。 - 插入失败时能自动复制。 - 历史能看到原文和最终文本。 - 个人词典能影响后续输出。 ### 11.2 场景测试 至少测试这些 app: - TextEdit。 - Notes。 - Safari/Chrome 网页输入框。 - ChatGPT 或 Claude 输入框。 - Cursor 输入框。 - 微信/飞书/Slack 类 IM。 - 邮件客户端。 ### 11.3 质量测试 准备样例库,覆盖: - 去口癖和重复。 - 中途改口。 - 中文标点。 - 中英混输。 - 技术词。 - 邮件表达。 - Prompt 结构化。 - 插入失败兜底。 --- ## 12. 第一轮完成定义 满足以下条件才算第一轮完成: - 主链路可以连续稳定使用。 - 常用 app 中可以完成输入。 - 失败不会丢内容。 - 胶囊 UI 足够小,不干扰工作流。 - 默认润色自然,不过度 AI 化。 - 历史和剪贴板可以恢复最近输入。 - 隐私路径清楚,本地/云端/BYOK 不混淆。 - 用户可以在 5 分钟内完成第一次成功输入。 --- ## 13. 关联文档 - [OpenLess 产品需求文档]() - [语音输入产品第一轮需求文档]() - [OpenLess 竞品评论、产品基调与 UI 方向调研]() - [OpenLess 概念版诊断与落地规格]() - [OpenLess 整体逻辑梳理]()