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

12 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 三合一)
│   ├── 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:录音(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 模式(智能体)

  • 流程:语音/文字输入 → Node.js CLI 子进程 → 工具调用 → 流式输出
  • 通过 AgentBridgeAgent/src/bridge.js 通信stdin/stdout JSON 行协议)
  • 结果在 AgentNotchView(刘海弹窗)展示,支持复制/停止/新对话
  • 朗读开关(Config.shared.agentSpeakEnabled):任务完成时朗读最终输出
  • 配置通过 Agent/models.json 管理(菜单栏 🤖 Agent LLM → Settings

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}
error 错误 {"message":"错误描述"}

语音朗读

AppDelegate 使用 NSSpeechSynthesizermacOS 原生)进行语音合成。朗读行为:

模式 触发时机 弹窗关闭逻辑
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 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。

注意事项

  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} 等占位符,由 context.jsbuildSystemPrompt() 在运行时替换。

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