VoiceInput/AGENTS.md
JOJO 76741877a8 feat: 朗读功能、弹窗优化、Node.js 打包、Prompt 更新
- Assist/Agent 模式新增朗读开关 + NSSpeechSynthesizer 语音合成
- 朗读完成后弹窗延迟5秒带动画关闭(不再写死15/20秒)
- 打字模式下不自动关闭弹窗
- 二次输入时取消旧朗读/旧延迟关闭,避免竞争问题
- Agent 进度计时器第二次输入时正确归零
- DMG 打包自动清空 models.json API Key
- Node.js 运行时打包进 Agent/bin/,用户无需安装
- Agent system prompt 更新为 VoiceInput 身份 + 语音容错说明
- AGENTS.md 全面更新,补全 Agent 模式文档
2026-04-29 18:22:14 +08:00

236 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# VoiceInput — AI Agent 项目指南
## 项目概述
VoiceInput 是一款 **macOS 菜单栏语音输入应用**。按住 Fn / Globe 键录音,松开后通过剪贴板 + Cmd+V 自动将识别文本注入光标位置。支持 Apple 原生语音识别实时转录、LLM 纠错优化、AI 对话、Agent 智能体调用、多语言切换。
**三种模式**(双击 Control 循环切换):
| 模式 | 图标 | 功能 |
|------|------|------|
| Input语音输入 | 🎤 | 语音转文字 + LLM 纠错 → 注入光标 |
| AssistAI 助手) | 💬 | 语音提问 → 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 终端 UIAgent 内部使用)
└── 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