VoiceInput/AGENTS.md
JOJO deae1fbc06 feat: RecordingPanel 宽度自适应 + 打包 DMG
- updateText 移除文本宽度硬编码下限 160,真正根据内容自适应
- showRefining/showThinking 简化为调用 updateText,复用自适应逻辑
- 面板最小宽度由 minPanelWidth(180pt) 兜底
- 打包 VoiceInput-20260425.dmg
2026-04-25 16:34:33 +08:00

6.7 KiB
Raw Blame History

VoiceInput — AI Agent 项目指南

项目概述

VoiceInput 是一款 macOS 菜单栏语音输入应用。按住 Fn / Globe 键录音,松开后通过剪贴板 + Cmd+V 自动将识别文本注入光标位置。支持 Apple 原生语音识别实时转录、LLMDeepSeek纠错优化、多语言切换以及双击 Control 切换 Input / Assist 模式。

  • 最低系统macOS 14+
  • 语言Swift 5.9
  • 构建系统Swift Package ManagerPackage.swift+ Makefile

常用命令

所有命令在项目根目录执行。

命令 说明
make build / make debug Debug 构建并生成 .app bundle
make release Release 构建(优化)
make run Debug 构建 + 启动应用
make clean 清理构建产物
make install Release 构建 + 安装到 /Applications
make sign Ad-hoc 签名Release 后用)
make dmg 签名 + 创建 DMG 分发包

构建产物路径:.build/release/VoiceInput.appRelease.build/debug/VoiceInput.appDebug

.app bundle 由 scripts/create_app_bundle.sh 生成,负责将二进制 + 资源封装为标准 macOS 应用包。

项目结构

Sources/
├── main.swift                     # 入口:创建 NSApplication(.accessory) + AppDelegate
├── App/
│   ├── AppDelegate.swift          # 主控制器:权限引导、录音/停止、模式切换、LLM 调度
│   └── MenuBarController.swift    # 菜单栏图标与菜单语言、LLM 配置、模式切换)
├── Core/
│   ├── KeyMonitor.swift           # CGEvent 全局热键Fn 键长按录音、Control 键双击切换模式
│   ├── SpeechRecognizer.swift     # SFSpeechRecognizer 封装:流式识别、语言切换
│   ├── AudioLevelMonitor.swift    # AVAudioEngine 音频电平RMS 音量驱动波形
│   ├── TextInjector.swift         # 剪贴板 + Cmd+V 注入;自动切换 CJK 输入法
│   ├── InputSourceManager.swift   # 输入法检测与切换(中文/英文等)
│   └── LLMRefiner.swift           # DeepSeek API 调用:语音纠错(refine) / AI 对话(chat)
├── UI/
│   ├── RecordingPanel.swift       # 胶囊浮动窗:无边框、底部居中、弹簧动画
│   ├── WaveformView.swift         # 5 条球形波动画RMS 驱动实时跳动
│   └── SettingsWindow.swift       # LLM 设置面板API Key / Base URL / Model / Test
├── Utils/
│   └── Config.swift               # UserDefaults 持久化语言、LLM 配置、触发模式
└── Resources/
    ├── AppIcon.icns               # 应用图标
    ├── AppIcon-macOS.png          # 图标源文件
    └── Info.plist                 # Bundle 配置

架构模式

应用生命周期

main.swift 创建 NSApplication 并设置为 .accessory(无 Dock 图标,仅菜单栏)。AppDelegate 通过 static retainedInstance 保持强引用(因为 NSApplication.delegate 是 weak

权限引导流程(顺序执行)

辅助功能权限 → 输入监控权限 → 启动 KeyMonitor
                                      ↓
                              运行时按需请求:
                              麦克风权限 + 语音识别权限

beginPermissionFlow() 使用 CGPreflightListenEventAccess()CGEvent.tapCreate 做权限预检。未授权时弹出对话框 + 轮询 Timer。

录音流程

Fn Down长按 > 200ms
  → AppDelegate.startRecording()
    → AudioLevelMonitor.start()        // 音频电平
    → SpeechRecognizer.start()         // 语音识别
    → RecordingPanel.showAnimated()    // 显示胶囊窗

Fn Up
  → AppDelegate.stopRecording()
    → 如果启用 LLM 纠错 → LLMRefiner.refine()
    → TextInjector.inject()            // 注入文本
    → RecordingPanel.hideAnimated()

Fn 键交互(长按录音)

KeyMonitor 通过 CGEvent.tapCreate(.cghidEventTap) 监听 flagsChanged 事件。

  • Fn 键 mask 为 0x00800000长按 > 200ms:触发录音(onFnDownonFnUp
  • Control 键 mask 为 0x00040000双击(两次按下间隔 < 400ms:切换模式(onControlDoubleClick
  • Fn 短按(< 200ms无操作留作扩展

LLM 集成

LLMRefiner 通过 HTTP 调用 DeepSeek Chat Completions API。两种模式

  • Input 模式refine):纠错优化,修正同音字/拼写/术语
  • Assist 模式chatAI 对话,结果注入光标

配置通过 Config.shared.inputLLM / Config.shared.assistLLM 独立管理。

代码约定

约定 说明
final class 所有业务类默认使用,除非设计上需要继承
weak self 闭包/异步回调中必须使用,防止循环引用
@MainActor 所有 UI 操作在主线程,异步任务用 Task { @MainActor in }
// MARK: - 按功能分区,保持 AppDelegate 等大文件可读性
Print 日志 使用 print("[模块名] 内容") 格式,如 [KeyMonitor][App]
权限处理 所有权限失败必须给用户明确提示NSAlert并引导至系统设置
异步 语音识别和 LLM 调用使用 Swift Concurrencyasync/await

关键依赖

所有依赖通过系统框架链接,无第三方包:

Speech        → 语音识别
AVFAudio      → 音频采集与电平
Carbon        → 全局热键(已逐步迁移至 CoreGraphics
CoreGraphics  → CGEvent API热键监听、文本注入
AppKit        → UI 组件
Foundation    → 基础类型与网络

Entitlements 文件 Sources/Resources/VoiceInput.entitlements 包含:

  • com.apple.security.device.audio-input — 麦克风
  • com.apple.security.cs.disable-library-validation — 允许加载系统插件

注意事项

  1. ARC 陷阱NSApplication.delegate 是 weak 引用,main.swift 中的 AppDelegate 会被 ARC 释放。解决方案:AppDelegate 内部使用 static retainedInstance 自持 + main.swift 使用 withExtendedLifetime 双重保险。

  2. HID Event Tap 回退KeyMonitor 优先创建 .cghidEventTap,失败时回退到 .cgSessionEventTap。前者需要输入监控权限,后者权限要求较低但可能丢失部分事件。

  3. 语音识别间歇性报错SFSpeechRecognizer 在环境安静时可能报 kAFAssistantErrorDomain 1110(无语音输入),AppDelegate 中已忽略此错误。

  4. 输入法切换:注入文本前 TextInjector 会通过 InputSourceManager 检测并切换到合适的 CJK 输入法,确保粘贴中文时不出现乱码。

  5. DMG 构建make dmg 依赖 make signmake release,需要 hdiutil(系统自带)。