- Assist/Agent 模式新增朗读开关 + NSSpeechSynthesizer 语音合成 - 朗读完成后弹窗延迟5秒带动画关闭(不再写死15/20秒) - 打字模式下不自动关闭弹窗 - 二次输入时取消旧朗读/旧延迟关闭,避免竞争问题 - Agent 进度计时器第二次输入时正确归零 - DMG 打包自动清空 models.json API Key - Node.js 运行时打包进 Agent/bin/,用户无需安装 - Agent system prompt 更新为 VoiceInput 身份 + 语音容错说明 - AGENTS.md 全面更新,补全 Agent 模式文档
236 lines
12 KiB
Markdown
236 lines
12 KiB
Markdown
# 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 三合一)
|
||
│ ├── 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(含 {current_time} 等占位符)
|
||
├── src/
|
||
│ ├── bridge.js # Node.js 桥接入口(stdin/stdout JSON 通信)
|
||
│ ├── config.js # 配置加载
|
||
│ ├── cli/ # CLI 命令处理
|
||
│ ├── core/ # 上下文构建、状态管理
|
||
│ ├── model/ # LLM 客户端、模型配置
|
||
│ ├── storage/ # 对话历史存储
|
||
│ ├── tools/ # 工具实现(文件读写、命令执行、搜索等)
|
||
│ └── ui/ # CLI 终端 UI(Agent 内部使用)
|
||
└── 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()
|
||
→ AudioLevelMonitor.start() // 音频电平
|
||
→ SpeechRecognizer.start() // 语音识别
|
||
→ RecordingPanel.showAnimated() // 显示胶囊窗
|
||
|
||
Fn Up
|
||
→ AppDelegate.stopRecording()
|
||
→ 根据 triggerMode 分发:
|
||
├── input → handleInputMode() → LLMRefiner.refine() → TextInjector.inject()
|
||
├── assist → handleAssistMode() → LLMRefiner.chat() → inject 或 Notch弹窗
|
||
└── agent → handleAgentMode() → 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 模式(智能体)
|
||
- 流程:语音/文字输入 → Node.js CLI 子进程 → 工具调用 → 流式输出
|
||
- 通过 `AgentBridge` 与 `Agent/src/bridge.js` 通信(stdin/stdout JSON 行协议)
|
||
- 结果在 `AgentNotchView`(刘海弹窗)展示,支持复制/停止/新对话
|
||
- 朗读开关(`Config.shared.agentSpeakEnabled`):任务完成时朗读最终输出
|
||
- 配置通过 `Agent/models.json` 管理(菜单栏 `🤖 Agent LLM` → Settings)
|
||
|
||
### 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}` |
|
||
| `error` | 错误 `{"message":"错误描述"}` |
|
||
|
||
### 语音朗读
|
||
|
||
`AppDelegate` 使用 `NSSpeechSynthesizer`(macOS 原生)进行语音合成。朗读行为:
|
||
|
||
| 模式 | 触发时机 | 弹窗关闭逻辑 |
|
||
|------|---------|-------------|
|
||
| Assist | AI 回复弹窗展示时(需开启开关 + notch 模式) | 朗读完成 + 5s 带动画关闭 |
|
||
| Agent | 任务完成 `.done` 事件时(需开启开关) | 朗读完成 + 5s 带动画关闭 |
|
||
|
||
朗读开关关闭时,弹窗按固定时间关闭(Assist 15s / Agent 20s)。
|
||
用户手动停止/关闭弹窗或开始新对话时立即停止朗读。
|
||
|
||
### DMG 安全
|
||
|
||
`make dmg` 打包时会自动将 `models.json` 替换为空配置 `{"models":[],"default_model":"","tavily_api_key":""}`,防止 API Key 泄露。本地开发构建不受影响。
|
||
|
||
## 代码约定
|
||
|
||
| 约定 | 说明 |
|
||
|------|------|
|
||
| `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。
|
||
|
||
## 注意事项
|
||
|
||
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}` 等占位符,由 `context.js` 的 `buildSystemPrompt()` 在运行时替换。
|
||
|
||
7. **NSSpeechSynthesizer 弃用警告**:macOS 14 标记 `NSSpeechSynthesizer` 为 deprecated(建议用 `AVSpeechSynthesizer`),但功能正常。编译时仅有 warning,无 error。
|