# VoiceInput — AI Agent 项目指南 ## 项目概述 VoiceInput 是一款 **macOS 菜单栏语音输入应用**。按住 Fn / Globe 键录音,松开后通过剪贴板 + Cmd+V 自动将识别文本注入光标位置。支持 Apple 原生语音识别实时转录、LLM 纠错优化、AI 对话、Agent 智能体调用、多语言切换。 **三种模式**(双击 Control 循环切换): | 模式 | 图标 | 功能 | |------|------|------| | Input(语音输入) | 🎤 | 语音转文字 + LLM 纠错 → 注入光标 | | Assist(AI 助手) | 💬 | 语音提问 → AI 回复(注入光标或刘海弹窗) | | Agent(智能体) | 🤖 | 语音/文字 → 多轮对话 + 工具调用 → 刘海弹窗 | - **最低系统**:macOS 14+ - **语言**:Swift 5.9 - **构建系统**:Swift Package Manager(`Package.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 分发包(**自动清空 API Key**) | 构建产物路径:`.build/release/VoiceInput.app`(Release)或 `.build/debug/VoiceInput.app`(Debug)。 `.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 # LLM API:语音纠错(refine) / AI 对话(chat) │ ├── AgentBridge.swift # Agent CLI 进程桥接:Node.js stdin/stdout JSON 通信 │ └── NotchDisplayManager.swift # AI 回复刘海弹窗管理(Assist 模式) ├── UI/ │ ├── RecordingPanel.swift # 胶囊浮动窗:无边框、底部居中、弹簧动画 │ ├── WaveformView.swift # 5 条球形波动画:RMS 驱动实时跳动 │ ├── SettingsWindow.swift # LLM 设置面板(Input/Assist/Agent 三合一 + 助手个性化) │ ├── MemoryWindow.swift # 记忆管理窗口(NSTableView 查看/搜索/编辑记忆) │ ├── AgentNotchView.swift # Agent 刘海弹窗视图(SwiftUI) │ ├── AgentProgressTracker.swift # Agent 任务进度追踪(工具调用/流式输出/自动新建) │ ├── AgentModelEditor.swift # Agent 模型添加/编辑弹窗 │ ├── AppKitTextField.swift # 支持快捷键的 NSTextField 包装 │ ├── KeyEquivalentSecureTextField.swift # 支持快捷键的安全文本框 │ └── KeyEquivalentPanel.swift # 支持快捷键的 NSPanel 基类 ├── Utils/ │ └── Config.swift # UserDefaults 持久化:语言、LLM、触发模式、朗读、助手个性化等 └── Resources/ ├── AppIcon.icns # 应用图标 ├── AppIcon-macOS.png # 图标源文件 └── Info.plist # Bundle 配置 Agent/ ├── models.json # Agent 模型配置(API URL/Key/Model) ├── models.example.json # 模型配置模板(分发包用) ├── prompts/ │ └── system.txt # Agent system prompt(含 {assistant_name}/{user_name} 等占位符) ├── doc/ │ └── tools.json # 工具声明(remember/recall/set_identity 等) ├── src/ │ ├── bridge.js # Node.js 桥接入口(stdin/stdout JSON 通信 + 对话持久化) │ ├── config.js # 配置加载 │ ├── cli/ # CLI 命令处理 │ ├── core/ # 上下文构建(含 user_profile/identity 注入)、状态管理 │ ├── model/ # LLM 客户端、模型配置 │ ├── storage/ # 对话历史存储(conversation_store) │ ├── tools/ # 工具实现 │ │ ├── remember.js # 记忆记录(add/update/delete) │ │ ├── recall.js # 记忆搜索(按类型/关键词/时间过滤) │ │ ├── set_identity.js # 身份设置(助手名字/用户称呼/语气) │ │ └── ... # 文件读写、命令执行、搜索等 │ └── ui/ # 工具友好展示(记住/回忆/设置身份) └── node_modules/ # npm 依赖 ``` ## 架构模式 ### 应用生命周期 `main.swift` 创建 `NSApplication` 并设置为 `.accessory`(无 Dock 图标,仅菜单栏)。`AppDelegate` 通过 `static retainedInstance` 保持强引用(因为 `NSApplication.delegate` 是 weak)。 ### 权限引导流程(顺序执行) ``` 辅助功能权限 → 输入监控权限 → 启动 KeyMonitor ↓ 运行时按需请求: 麦克风权限 + 语音识别权限 ``` `beginPermissionFlow()` 使用 `CGPreflightListenEventAccess()` 和 `CGEvent.tapCreate` 做权限预检。未授权时弹出对话框 + 轮询 Timer。 ### 录音与调度流程 ``` Fn Down(长按 > 200ms) → AppDelegate.startRecording() → 停止正在进行的朗读(避免冲突) → Agent 模式 → 统一显示刘海弹窗 → AudioLevelMonitor.start() // 音频电平 → SpeechRecognizer.start() // 语音识别 → RecordingPanel.showAnimated() // 显示胶囊窗 Fn Up → AppDelegate.stopRecording() → 根据 triggerMode 分发: ├── input → handleInputMode() → LLMRefiner.refine() → TextInjector.inject() ├── assist → handleAssistMode() → LLMRefiner.chat() → inject 或 Notch弹窗 └── agent → handleTextInput() → AgentBridge 子进程 → AgentNotch 弹窗 ``` ### Fn 键交互 `KeyMonitor` 通过 `CGEvent.tapCreate(.cghidEventTap)` 监听 `flagsChanged` 事件。 - Fn 键 mask `0x00800000`,**长按 > 200ms**:录音(`onFnDown` → `onFnUp`) - Control 键 mask `0x00040000`,**双击(间隔 < 400ms)**:切换模式(`onControlDoubleClick` → Input → Assist → Agent 循环) - Fn 短按(< 200ms):无操作 ### 三种模式详解 #### Input 模式(语音输入) - 流程:语音识别 → LLM 纠错(可选)→ 注入光标 - LLM 配置:`Config.shared.inputLLM`(菜单栏 `🎤 Input LLM` → Settings) #### Assist 模式(AI 助手) - 流程:语音识别 → LLM 对话 → 结果展示 - 输出方式(`Config.shared.assistOutputMode`): - `inject`:直接注入光标 - `notch`:刘海弹窗展示(`NotchDisplayManager`) - 朗读开关(`Config.shared.assistSpeakEnabled`):弹窗模式下朗读 AI 回复 - LLM 配置:`Config.shared.assistLLM`(菜单栏 `💬 Assist LLM` → Settings) #### Agent 模式(Better Siri 个人助手) Agent 被设计为个人 Mac 助手,而非纯代码开发工具: - **身份定位**:友好自然的个人助手,充分利用 macOS 原生能力(osascript、系统命令) - **记忆系统**:`remember`(记录)+ `recall`(搜索),自动记住用户偏好/习惯/待办; 记忆存储在 `~/Library/Application Support/VoiceInput/memory/memory.json`,跨对话持久化 - **身份设置**:`set_identity` 工具独立管理助手名字、用户称呼、语气,与记忆分开 - **对话持久化**:对话自动保存到 `~/.easyagent/conversations/`,启动时恢复最新对话; token 计数(prompt/completion 累加,total 覆盖) - **新建对话**:手动/自动两种模式;自动模式下 total > 阈值时下次输入自动新建 - **弹窗统一**:语音和打字输入使用同一刘海弹窗,按 Fn 即弹出; 语音识别内容自动填入发送(可关闭自动发送开关) - **工作状态**:thinking/running 期间隐藏输入框,完成/空闲时显示 - **工具显示**:三种模式(全部显示 / 最后隐藏 / 一直不显示),在设置中配置 流程: - 语音输入:Fn Down → 显示弹窗 + 录音 → Fn Up → 自动填入文本 → 发送 → 工具调用 → 流式输出 - 打字输入:Fn → 弹窗显示 → 用户输入 → 手动发送 → 工具调用 → 流式输出 ### Agent JSON 通信协议 `AgentBridge` 通过 `bridge.js` 子进程的 stdin/stdout 通信: **发送(stdin)**: ```json {"action":"chat","message":"用户输入","workspace":"/path","allowMode":"full_access","modelKey":"deepseek-v4-flash","thinkingMode":false} ``` **接收(stdout,逐行 JSON)**: | type | 说明 | |------|------| | `status` | 状态变更 `{"mode":"thinking"}` | | `tool_start` | 工具开始 `{"tool":"run_command","display":"ls -la"}` | | `tool_end` | 工具结束 `{"tool":"run_command","result":["line1","line2"]}` | | `assistant_chunk` | 流式文本 `{"content":"部分回复..."}` | | `done` | 任务完成 `{"content":"完整回复","elapsed_ms":1234,"total_tokens":5000}` | | `error` | 错误 `{"message":"错误描述"}` | ### 语音朗读 `AppDelegate` 使用 `NSSpeechSynthesizer`(macOS 原生)进行语音合成。朗读行为: | 模式 | 触发时机 | 弹窗关闭逻辑 | |------|---------|-------------| | Assist | AI 回复弹窗展示时(需开启开关 + notch 模式) | 朗读完成 + 5s 带动画关闭 | | Agent | 任务完成 `.done` 事件时(需开启开关) | 朗读完成 + 5s 带动画关闭 | 朗读开关关闭时,弹窗按固定时间关闭(Assist 15s / Agent 20s)。 用户按 Fn 开始新录音时立即停止朗读。 ### DMG 安全 `make dmg` 打包时会自动将 `models.json` 替换为空配置 `{"models":[],"default_model":"","tavily_api_key":""}`,防止 API Key 泄露。`user_profile.json` 和 `memory.json`(存储在系统目录,不在 bundle 内)无需额外处理。 ## 代码约定 | 约定 | 说明 | |------|------| | `final class` | 所有业务类默认使用,除非设计上需要继承 | | `weak self` | 闭包/异步回调中必须使用,防止循环引用 | | `@MainActor` | 所有 UI 操作在主线程,异步任务用 `Task { @MainActor in }` | | `// MARK: -` | 按功能分区,保持 `AppDelegate` 等大文件可读性 | | Print 日志 | 使用 `print("[模块名] 内容")` 格式,如 `[KeyMonitor]`、`[App]` | | 权限处理 | 所有权限失败必须给用户明确提示(NSAlert)并引导至系统设置 | | 异步 | 语音识别和 LLM 调用使用 Swift Concurrency(`async/await`) | ## 关键依赖 ### Swift 端 所有依赖通过系统框架链接: ``` Speech → 语音识别 AVFAudio → 音频采集与电平 Carbon → 全局热键(已逐步迁移至 CoreGraphics) CoreGraphics → CGEvent API(热键监听、文本注入) AppKit → UI 组件、NSSpeechSynthesizer(语音合成) Foundation → 基础类型与网络 DynamicNotchKit → 刘海弹窗(Swift Package,纯 SwiftUI) ``` Entitlements 文件 `Sources/Resources/VoiceInput.entitlements`: - `com.apple.security.device.audio-input` — 麦克风 - `com.apple.security.cs.disable-library-validation` — 允许加载系统插件 ### Agent 端(Node.js) Agent CLI 使用 Node.js,入口为 `Agent/src/bridge.js`。Node.js 运行时已打包在 `Agent/bin/node`(arm64),构建时自动进入 `.app` bundle。用户无需额外安装 Node.js。 ## 配置项速查 ### Agent 设置窗口可配置项 | 配置项 | 说明 | 默认值 | |--------|------|--------| | 默认模型 | Agent 使用的 LLM | - | | 运行模式 | Read Only / Full Access | Full Access | | 输入方式 | 语音 / 打字 | 语音 | | 思考模式 | 快速 / 思考 | 快速 | | 工具显示 | 全部显示 / 最后隐藏 / 一直不显示 | 全部显示 | | 新建对话 | 手动 / 自动 | 手动 | | 自动新建阈值 | total tokens 超过此值时自动新建对话 | 40000 | | 朗读 | 任务完成时朗读最终输出 | 关 | | 语音自动发送 | 语音识别后自动发送还是等待手动 | 开 | | 助手名字 | 出现在 prompt 中 | 空 | | 我的称呼 | 出现在 prompt 中 | 空 | | 助手语气 | 友好/专业/幽默/简洁/温暖/正式 | 友好 | ### 用户数据存储位置 | 数据 | 路径 | |------|------| | Agent 模型配置 | `~/Library/Application Support/VoiceInput/models.json` | | 用户身份配置 | `~/Library/Application Support/VoiceInput/user_profile.json` | | 记忆数据 | `~/Library/Application Support/VoiceInput/memory/memory.json` | | 对话历史 | `~/.easyagent/conversations/` | ## 注意事项 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 sign` → `make release`,需要 `hdiutil`(系统自带)。打包时自动清空 `models.json` 中的 API Key。 6. **Agent prompt 维护**:Agent 的 system prompt 在 `Agent/prompts/system.txt`,包含 `{current_time}`、`{model_id}`、`{assistant_name}`、`{assistant_tone}`、`{user_name}` 等占位符,由 `context.js` 的 `buildSystemPrompt()` 在运行时替换。 7. **NSSpeechSynthesizer 弃用警告**:macOS 14 标记 `NSSpeechSynthesizer` 为 deprecated(建议用 `AVSpeechSynthesizer`),但功能正常。编译时仅有 warning,无 error。 8. **消息加载顺序**:bridge.js 中必须先加载旧对话再 push 用户消息,否则新消息会被覆盖。 9. **弹窗状态覆盖**:`showAgentNotchInternal` 在弹窗已存在时只执行 `show()`,不覆盖 `isTextInputMode` 等状态,避免语音输入时输入框被错误隐藏。 10. **连续 assistant 消息**:bridge.js 在 done 前自动去重/合并末尾连续 assistant 消息,防止 API 偶发重复返回导致上下文污染。