氛围编程(Vibe Coding)实战:最难的不是界面,是和本机环境打交道
用 AI 做项目,很容易产生一种错觉:它好像什么都会。
写 Vue 组件,几分钟。调 CSS,也像那么回事。让它拆文件、改状态、补一点交互,通常都能跑起来。于是很多人会觉得 Vibe Coding 已经可以横着走了。
但这只是幸存者偏差。
界面层是 AI 最舒服的地方:输入明确、反馈直观、错了也好修。真正难的是项目开始碰操作系统、外部进程、权限路径和本机环境。到这里,AI 经常会开始猜:猜路径、猜协议、猜 Windows 行为,甚至认真写出一个根本不存在的调用方式。
Codex LED Widget 的第三场硬仗,就是把 Codex CLI 和 Windows 本机环境打通。界面可以让 AI 写,系统边界必须由人盯住。
IPC 调用链:先画清楚,再让 AI 写
这个项目的核心不是液态球,而是稳定读到 Codex 额度。
我先给 AI 画出调用链:
Vue UI
-> Tauri command: get_quota
-> Rust quota adapter
-> locate codex.exe
-> codex app-server --listen stdio://
-> stdin: initialize
-> stdin: account/rateLimits/read
-> stdout: quota response
-> normalize QuotaSnapshot
-> Vue UI
前端只调用 get_quota,后面的脏活全部交给 Rust。
第一步是找 codex.exe。不能假设用户装在固定目录,也不能只赌 PATH。查找顺序必须写死:
1. CODEX_CLI_PATH
2. %LOCALAPPDATA%\OpenAI\Codex\bin\codex.exe
3. %LOCALAPPDATA%\codex-led-widget\bin
4. PATH
第二步是启动 stdio app-server:
codex app-server --listen stdio://
这不是抓一段命令行输出,而是和子进程做标准流通信。Rust 要启动进程,接管 stdin/stdout,然后按协议发 JSON。
let mut child = Command::new(codex_path)
.args(["app-server", "--listen", "stdio://"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()?;
send_json(&mut child.stdin, initialize_message)?;
send_json(&mut child.stdin, rate_limits_read_message)?;
let response = read_json(&mut child.stdout)?;
let snapshot = normalize_quota(response)?;
顺序也不能靠感觉:先 initialize,再 account/rateLimits/read。协议型调用最怕“差不多”。
CLI 原始响应会有多层结构,字段也不适合前端直接用。所以最后一步必须在 Rust 层清洗,输出稳定的 QuotaSnapshot。
前端绝不能直接碰 CLI 原始结构。
这条线一破,Vue 里很快就会长出一堆 response?.data?.payload?.limits,后面谁维护谁头疼。
WindowsApps:看得到,不代表能用
真正的坑出在 Microsoft Store 版 Codex CLI。
如果用户装的是 Store 版本,codex.exe 可能会在 WindowsApps 受保护路径里。这个目录很别扭:路径可能能看到,但不代表能稳定读取;文件可能存在,但不代表能直接执行。
AI 一开始很容易给出朴素方案:找到路径,直接跑。
普通路径当然没问题,到了 WindowsApps 就翻车。这不是语法问题,是 Windows 权限问题。AI 没在当前机器上真正摔过,很容易把 Windows 想得过于讲理。
最后的策略是白名单搬运。
检测到 CLI 位于受保护路径时,不硬刚权限,而是复制到当前用户可控目录:
%LOCALAPPDATA%\codex-led-widget\bin
再从缓存目录执行。
流程变成:
发现 codex.exe
-> 判断是否位于 WindowsApps
-> 是:复制到 %LOCALAPPDATA%\codex-led-widget\bin
-> 否:直接使用
-> 启动 app-server
这里的重点是“必要时复制”。不乱搬、不提权、不碰系统目录。缓存目录放在当前用户空间里,问题小很多。
Diagnostic Logs:能排错,但不能泄密
系统级功能越多,日志越重要。否则用户说“连接异常”,你根本不知道是找不到 CLI、权限失败、协议失败,还是 Codex 没登录。
但日志不能贪心。
我的规则很死:
Diagnostic Logs 只记录路径、状态和错误,不记录认证信息、Token、额度响应正文。
可以记:
codex path found
using cached executable
rate limit read failed: exit code ...
quota read success
不能记:
access token
session payload
account raw response
完整 JSON 响应正文
AI 调试时很喜欢“多打点日志”。这话听着合理,但很多泄密问题就是这么来的。安全红线不能交给热情。
桌面应用不是网页套壳
打通 CLI 只是第一步。一个能常驻的桌面小组件,还要处理窗口生命周期。
单实例互斥锁(Mutex)
用户双击多次,不能开出一排状态灯。Windows 下用系统级 Mutex 做单实例保护:
let mutex = create_global_mutex("Global\CodexLedWidget.SingleInstance")?;
if already_exists(mutex) {
show_message_box("Codex LED Widget 已在运行。");
exit(0);
}
这类判断应该放在 Rust 层。前端不该关心自己是不是第几个实例。
原生托盘菜单
状态灯要常驻,但不该一直占任务栏。托盘菜单负责把它安稳地放到后台:
托盘右键:
- 显示/隐藏
- 刷新额度
- 切换置顶
- 退出
这不是装饰。用户隐藏窗口后,必须知道从哪里把它找回来。
置顶状态同步
状态灯的价值是“扫一眼就知道”。如果它被 IDE、浏览器、终端盖住,就没意义了。
所以置顶状态要和 Tauri 窗口状态同步:
用户点击置顶
-> Tauri command
-> window.set_always_on_top(...)
-> emit event
-> Vue 更新按钮状态
不能只在前端把按钮点亮,系统层也必须真的置顶。
隐藏与恢复
关闭不等于退出。这个小组件更适合 hide/show,而不是每次销毁进程。
window.hide()?;
// 托盘点击或菜单触发
window.show()?;
window.set_focus()?;
这样体验更轻,也避免反复启动 CLI 和重建窗口。
AI 能写模块,人要守边界
这次最明显的感受是:AI 很适合写边界清楚的模块,比如 Vue 组件、Tauri command、菜单项、状态封装。规格写清楚,它能很快交付。
但碰到本机黑盒环境,就不能全信它。
Windows 权限、Store 应用路径、stdio 协议、Diagnostic Logs、单实例行为,这些都不是“补全代码”能解决的。人必须当守门人,把系统限制、安全边界和复杂度集中到 Rust 后端。
前端保持干净。
Rust 负责脏活。
AI 负责执行。
人负责别让它乱执行。
下一篇进入终章:第四篇:项目发版以后,我重新看了一遍这次 AI 重构。