13 KiB
13 KiB
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 正常输入流程
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 插入失败流程
TextInserter.insert(finalText) fails
-> ClipboardFallback.copy(finalText)
-> HistoryStore.save(session with copied_fallback)
-> CapsuleOverlay.set(copied_fallback)
用户不需要重新录音。
4.3 取消流程
Esc or capsule x clicked
-> Recorder.cancel()
-> discard unsaved audio by default
-> CapsuleOverlay.set(cancelled)
-> CapsuleOverlay.hide()
4.4 重跑润色流程
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 轻度润色模板
你是语音输入文本整理器。请把用户的口语转写整理成可直接发送或继续编辑的文字。
要求:
- 去掉明显口癖、重复和无意义停顿。
- 补充自然标点。
- 保留用户原意和表达习惯。
- 不扩写、不创作、不回答内容。
- 如果包含中英混输、产品名、代码名,请尽量保留。
原始转写:
{{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 |
结构:
[ 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. 开发顺序
建议按以下顺序开发:
- 创建 macOS 菜单栏应用骨架。
- 实现设置窗口基础导航。
- 实现权限检查和引导。
- 实现全局快捷键。
- 实现录音与音量采样。
- 实现底部微型状态胶囊。
- 接入最小 ASR 路径。
- 接入轻度润色。
- 实现文本插入。
- 实现剪贴板兜底。
- 实现历史记录。
- 实现个人词典。
- 实现模式切换。
- 完成错误处理和隐私设置。
- 做真实 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 分钟内完成第一次成功输入。