VoiceInput/AGENTS.md
JOJO 76fcda7210 docs: 更新 AGENTS.md 和 README.md,反映 Better Siri 改造
AGENTS.md:
- 新增记忆系统、对话持久化、新建对话、弹窗统一等架构说明
- 更新项目结构(MemoryWindow.swift、recall/remember/set_identity.js)
- 新增配置项速查表(助手名字/语气/阈值/自动发送等)
- 新增注意事项 8-10(消息顺序、弹窗状态、去重)
- 更新录音调度流程(Agent 走 handleTextInput)

README.md:
- Agent 重新定位为 Better Siri 个人助手
- 新增记忆系统、统一弹窗、Mac 原生集成等功能亮点
- 新增三种模式详解中的 Agent 记忆系统描述
2026-04-30 18:21:39 +08:00

292 lines
15 KiB
Markdown
Raw Permalink 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 三合一 + 助手个性化)
│ ├── 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 偶发重复返回导致上下文污染。