我是如何用 2 条 Codex 提示词构建我的第一个 macOS 原生应用的
🚀 下载应用:已编译好的开箱即用版本现已在 GitHub Releases 发布。
我已经很久没有 100% 依赖键盘打字了。在日常工作中,我更多时候是直接用语音进行听写输入,以获得快速且精准的文本转写。
这主要用于和大语言模型(LLM)对话。我妻子甚至开玩笑说,我现在跟电脑说的话比跟她说的还多。老实说,这玩笑并非毫无根据:我坚信要想让大模型给出高质量的回答,输入的实时上下文、细节和背景信息越丰富,产出的质量就越高。如果单纯靠双手在键盘上敲出长篇大论,既耗时又低效。
正因如此,我需要一款符合我使用习惯的语音输入工具。不是那种通用的复杂软件,而是严格契合我工作流的产品:按住全局快捷键(Fn / 地球仪键 一键对讲 push-to-talk),倾吐想法,松开按键——转写好的文字立刻自动粘贴在当前光标闪烁的位置(无论是代码编辑器、终端、浏览器还是聊天窗口)。
市面上现有方案的局限
macOS 上确实有一些优秀的产品。我深入测试了多种方案,其中有两款给人留下了深刻印象。
Wispr Flow 的体验非常出色:界面精美、响应极其迅速。但免费额度很快就会耗尽,随后便需要按月支付高昂的订阅费用。
随后我体验了 MacWhisper:
MacWhisper 是一款扎实的产品。它在本地免费运行 Whisper 模型,对隐私极其友好。然而,我本身就拥有专属的远程云端大模型 API,对于长段落识别速度更快、准确率更高。而 MacWhisper 却把自定义服务器 URL 和 API Token 的功能锁在了收费的 Pro 版本中。
这些软件各有亮点,但都有一个共同的问题:它们都想要你的钱。
而在 AI 时代,当一款基础封装应用试图收取昂贵费用时,我们完全可以直接自己动手,vibecoding 写一个属于自己的开源分支。
Simple Flow 的工作架构
在这个项目中,我自己没有手写一行 Swift 代码。从架构设计、核心逻辑到最终的二进制打包,全部由 OpenAI Codex 仅凭 2 条提示词完成。
从宏观上看,整个流水线分为四个清晰的步骤:
- 全局快捷键拦截:应用静默常驻在菜单栏,通过系统级底层钩子全局拦截
Fn(地球仪)按键的按下与释放。 - 音频录制:在按键按住期间,系统麦克风音频以极低延迟录入内存缓冲区。
- 云端转写请求:松开按键的瞬间,音频数据被异步推送到自定义远程端点,并附带 Token 鉴权。
- 自动文本粘贴:转写结果返回后,通过模拟合成
⌘V系统按键,即时注入到当前处于焦点的活跃应用窗口中。
用 2 条 Codex 提示词完成开发
提示词 1:核心逻辑与架构设计
在第一条提示词中,我用自然语言描述了整套工作流程:菜单栏常驻后台、按住 Fn 录音、松开请求网络 API、并将文本自动粘贴到焦点处。
Codex 自动挑选了 macOS 原生框架(AVFoundation、ApplicationServices、CoreGraphics),并构建了状态机、全局快捷键监控器与合成事件注入器:
public enum DictationPhase: Equatable, Sendable {
case idle
case recording
case transcribing
case feedback(FeedbackKind)
}
public struct DictationStateMachine: Sendable {
public private(set) var phase: DictationPhase = .idle
public mutating func handle(_ event: DictationEvent) -> [DictationEffect] {
switch (phase, event) {
case (.idle, .hotkeyPressed):
phase = .recording
return [.captureFocus, .startAudio]
case (.recording, .hotkeyReleased):
phase = .transcribing
return [.stopAndTranscribe]
case (.recording, .escapePressed):
phase = .idle
return [.cancelAudio, .returnToIdle]
case (.transcribing, .transcriptionInserted):
phase = .feedback(.inserted)
return []
case (.transcribing, .failed(let message)):
phase = .feedback(.error(message))
return []
default:
return []
}
}
}import ApplicationServices
import CoreGraphics
import Foundation
public final class HotkeyMonitor: @unchecked Sendable {
private var eventTap: CFMachPort?
private var decoder: HotkeyEventDecoder
public init(hotkey: Hotkey = .fnKey) {
self.decoder = HotkeyEventDecoder(hotkey: hotkey)
}
public func start() {
let mask = (1 << CGEventType.flagsChanged.rawValue)
self.eventTap = CGEvent.tapCreate(
tap: .cghidEventTap,
place: .headInsertEventTap,
options: .listenOnly,
eventsOfInterest: CGEventMask(mask),
callback: { (proxy, type, event, refcon) -> Unmanaged<CGEvent>? in
guard let refcon = refcon else { return Unmanaged.passUnretained(event) }
let monitor = Unmanaged<HotkeyMonitor>.fromOpaque(refcon).takeUnretainedValue()
monitor.handleFlagsChanged(event.flags)
return Unmanaged.passUnretained(event)
},
userInfo: Unmanaged.passUnretained(self).toOpaque()
)
let runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventTap, 0)
CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, .commonModes)
CGEvent.tapEnable(tap: eventTap!, enable: true)
}
}import CoreGraphics
import Foundation
public final class SystemEventPoster: EventPosting, @unchecked Sendable {
public func postPasteCommand() -> Bool {
let cmdKeyCode: CGKeyCode = 55 // Command key
let vKeyCode: CGKeyCode = 9 // 'V' key
let source = CGEventSource(stateID: .combinedSessionState)
guard let cmdDown = CGEvent(keyboardEventSource: source, virtualKey: cmdKeyCode, keyDown: true),
let vDown = CGEvent(keyboardEventSource: source, virtualKey: vKeyCode, keyDown: true),
let vUp = CGEvent(keyboardEventSource: source, virtualKey: vKeyCode, keyDown: false),
let cmdUp = CGEvent(keyboardEventSource: source, virtualKey: cmdKeyCode, keyDown: false) else {
return false
}
cmdDown.flags = .maskCommand
vDown.flags = .maskCommand
vUp.flags = .maskCommand
cmdUp.flags = []
// Post Command+V synthetic event tap directly to active focused application
cmdDown.post(tap: .cghidEventTap)
vDown.post(tap: .cghidEventTap)
vUp.post(tap: .cghidEventTap)
cmdUp.post(tap: .cghidEventTap)
return true
}
}提示词 2:设置界面与生产级打包
在第二条提示词中,我要求补齐日常使用所需的一切功能:
- 菜单栏状态图标(
Ready待命 /Recording录音中)与下拉菜单。 - 原生设置窗口:麦克风设备切换、自定义快捷键录制、系统权限检查(辅助功能 Accessibility 与麦克风权限)以及开机自启开关。
- 将服务器 URL 与 API Token 安全存储在 macOS 系统 Keychain 中。
- 自动化构建脚本,直接输出生产环境下的
.app应用包。
Codex 编写了完整的 UI 代码,在终端中直接编译 Swift 代码,并交付了打包签名的 SimpleFlow.app。
实际运行效果
我直接将 SimpleFlow.app 拖入 /Applications 文件夹,在设置中填入 API Token 与服务器地址,应用立刻完美运行。


在写这篇文章时,我还专门使用 top 命令实测了系统资源开销:
- CPU:
0.0%待机占用(进程常驻在RunLoop中处于sleeping状态,仅在触发CGEventTap按键中断时瞬时唤醒)。 - RAM:
~50 MB(标准 AppKit 和 SwiftUI 系统框架的基础内存常驻)。
它几乎不消耗系统资源,安安静静地待在状态栏,毫无延迟地帮我把长篇语音转化为大模型提示词,没有任何收费陷阱或人工限制。
GitHub 开源与下载
我已经将项目的全部源代码开源:
预编译好的应用压缩包(SimpleFlow-v1.0.0.zip)可以直接在 GitHub Releases 页面 下载。解压后将 SimpleFlow.app 拖入 /Applications 即可开始使用。
Bottom Text
在 2026 年,个人专属软件不再是一个神话,而是每个人触手可及的现实——即使是没有任何编程经验的人。打造一款完全贴合自身需求的专属工具,如今比在互联网上到处寻找所谓的“完美替代品”还要简单。
我不知道还能从中得出什么别的结论——别犹豫了,动手用起来吧,朋友们。