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

15 KiB
Raw Blame History

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 ManagerPackage.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.appRelease.build/debug/VoiceInput.appDebug

.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:录音(onFnDownonFnUp
  • 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 使用 NSSpeechSynthesizermacOS 原生)进行语音合成。朗读行为:

模式 触发时机 弹窗关闭逻辑
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.jsonmemory.json(存储在系统目录,不在 bundle 内)无需额外处理。

代码约定

约定 说明
final class 所有业务类默认使用,除非设计上需要继承
weak self 闭包/异步回调中必须使用,防止循环引用
@MainActor 所有 UI 操作在主线程,异步任务用 Task { @MainActor in }
// MARK: - 按功能分区,保持 AppDelegate 等大文件可读性
Print 日志 使用 print("[模块名] 内容") 格式,如 [KeyMonitor][App]
权限处理 所有权限失败必须给用户明确提示NSAlert并引导至系统设置
异步 语音识别和 LLM 调用使用 Swift Concurrencyasync/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/nodearm64构建时自动进入 .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 signmake 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.jsbuildSystemPrompt() 在运行时替换。

  7. NSSpeechSynthesizer 弃用警告macOS 14 标记 NSSpeechSynthesizer 为 deprecated建议用 AVSpeechSynthesizer),但功能正常。编译时仅有 warning无 error。

  8. 消息加载顺序bridge.js 中必须先加载旧对话再 push 用户消息,否则新消息会被覆盖。

  9. 弹窗状态覆盖showAgentNotchInternal 在弹窗已存在时只执行 show(),不覆盖 isTextInputMode 等状态,避免语音输入时输入框被错误隐藏。

  10. 连续 assistant 消息bridge.js 在 done 前自动去重/合并末尾连续 assistant 消息,防止 API 偶发重复返回导致上下文污染。