AGENTS.md: - 新增记忆系统、对话持久化、新建对话、弹窗统一等架构说明 - 更新项目结构(MemoryWindow.swift、recall/remember/set_identity.js) - 新增配置项速查表(助手名字/语气/阈值/自动发送等) - 新增注意事项 8-10(消息顺序、弹窗状态、去重) - 更新录音调度流程(Agent 走 handleTextInput) README.md: - Agent 重新定位为 Better Siri 个人助手 - 新增记忆系统、统一弹窗、Mac 原生集成等功能亮点 - 新增三种模式详解中的 Agent 记忆系统描述
15 KiB
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):
{"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/ |
注意事项
-
ARC 陷阱:
NSApplication.delegate是 weak 引用,main.swift中的AppDelegate会被 ARC 释放。解决方案:AppDelegate内部使用static retainedInstance自持 +main.swift使用withExtendedLifetime双重保险。 -
HID Event Tap 回退:
KeyMonitor优先创建.cghidEventTap,失败时回退到.cgSessionEventTap。前者需要输入监控权限,后者权限要求较低但可能丢失部分事件。 -
语音识别间歇性报错:
SFSpeechRecognizer在环境安静时可能报kAFAssistantErrorDomain 1110(无语音输入),AppDelegate中已忽略此错误。 -
输入法切换:注入文本前
TextInjector会通过InputSourceManager检测并切换到合适的 CJK 输入法,确保粘贴中文时不出现乱码。 -
DMG 构建:
make dmg依赖make sign→make release,需要hdiutil(系统自带)。打包时自动清空models.json中的 API Key。 -
Agent prompt 维护:Agent 的 system prompt 在
Agent/prompts/system.txt,包含{current_time}、{model_id}、{assistant_name}、{assistant_tone}、{user_name}等占位符,由context.js的buildSystemPrompt()在运行时替换。 -
NSSpeechSynthesizer 弃用警告:macOS 14 标记
NSSpeechSynthesizer为 deprecated(建议用AVSpeechSynthesizer),但功能正常。编译时仅有 warning,无 error。 -
消息加载顺序:bridge.js 中必须先加载旧对话再 push 用户消息,否则新消息会被覆盖。
-
弹窗状态覆盖:
showAgentNotchInternal在弹窗已存在时只执行show(),不覆盖isTextInputMode等状态,避免语音输入时输入框被错误隐藏。 -
连续 assistant 消息:bridge.js 在 done 前自动去重/合并末尾连续 assistant 消息,防止 API 偶发重复返回导致上下文污染。