# 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 偶发重复返回导致上下文污染。
