重构一个项目,最危险的时刻不是开始删代码,而是刚打开项目、心里冒出一句“这不挺简单吗”的时候。
这句话一出现,基本就要坏事。
你会觉得:不就是个桌面小工具吗?UI 改一改,逻辑挪一挪,依赖升一升,今晚就能收工。结果两小时后,你发现自己正在和一堆历史包袱对坐:旧的 Electron 语境还在,UI 审计记录没完全消化,前后端边界也不够干净。项目不大,但有些地方已经开始有味儿了。
这次我换了个玩法:我不写代码。
我的工作只剩三件事:
- 先看清楚旧项目到底乱在哪。
- 决定新架构应该怎么分层。
- 让 AI 按这个边界去实现。
代码交给 AI,架构决策我来做。听着轻松,其实更像坐在副驾上指挥一辆很快的车:你不踩油门,但你得知道什么时候该喊刹车。
先承认它有历史包袱
原项目github地址:github.com/xicunwus202…
这个项目叫 Codex LED Widget,本质上是一个 Windows 桌面小组件。它读取本机 Codex 的额度信息,然后用一个常驻桌面的液态状态灯告诉我:
- 5 小时额度还剩多少
- 7 天额度还剩多少
- 什么时候恢复
- 快没额度时别再硬开新任务
功能不复杂,但桌面应用从来不是“网页套个壳”那么简单。尤其项目历史里曾经有 Electron 的影子,事情就更容易变得别扭。
Electron 的优点是快,缺点也是快。它能让你很快做出一个桌面应用,也能让一个小组件背着半个浏览器出门。小工具还没干几件事,运行时已经先开始做深蹲了。
旧项目里主要有三类问题。
第一,历史 Electron 语境还残留在文档和审计材料里。它不一定影响当前代码运行,但会影响后续判断。新来的开发者或者 AI 一读,很容易以为项目仍然沿着旧路线走。
第二,UI 审计遗留没有完全收拢。早期视觉审计里提到过层级、动效、错误状态、控件密度等问题,这些都是有效信息,但需要重新映射到当前实现。
第三,结构需要收敛。哪些逻辑属于系统层,哪些属于界面层,哪些地方应该变成稳定边界,不能靠“先写了再说”。
所以我让 AI 做的第一件事不是写代码,而是读项目、读文档、读目录结构,然后回答一个问题:
这个项目应该被拆成几层?
新架构方案:Tauri 2 + Vue 3 + TS
最后定下来的新架构方案是:Tauri 2 + Vue 3 + TS。
更具体一点:
- 桌面壳和系统能力:Tauri 2 + Rust
- 前端界面:Vue 3 + TS
- 构建工具:Vite
- 通信方式:Tauri Command/Event
- 不引入本地 HTTP 服务
- 不引入数据库
这个组合适合 Codex LED Widget。它不是完整的 Codex 客户端,也不是云端统计平台。它只是一个轻量状态灯,需要稳定地跟本机系统和 Codex CLI 打交道。
Rust 层负责系统能力、CLI、托盘、窗口、单实例
Rust 层我把它定义成系统适配层。
它负责这些脏活累活:
- 查找本机
codex.exe - 调用 Codex CLI
- 通过 stdio 读取额度信息
- 处理 Microsoft Store 版 Codex CLI 的 WindowsApps 路径问题
- 管理窗口显示、隐藏、置顶
- 管理系统托盘
- 做单实例保护
- 写本地诊断日志
- 把 Codex CLI 原始响应整理成前端能稳定使用的数据结构
一句话:凡是和操作系统、外部进程、窗口生命周期、路径兼容有关的东西,都放在 Rust 层。
不要让前端去碰这些。前端很擅长做界面和交互,但你让它处理 Windows 路径、CLI 协议和系统权限,它可能会先沉默三秒,然后给你造出一个更大的问题。
Vue 层负责状态、交互、液态 UI
Vue 层只做用户看得到、点得到、感知得到的部分:
- 展示当前额度状态
- 在 5 小时和 7 天额度窗口之间切换
- 渲染液态 UI
- 根据额度切换绿色、黄色、红色、错误、读取中状态
- 处理刷新、隐藏、置顶、中英文切换
- 接收 Rust 层通过事件发来的状态变化
关键点是:Vue 层不需要知道 Codex CLI 原始响应长什么样。
它只接收一个整理好的 QuotaSnapshot。前端关心的是 remainingPercent、resetsAt、primary、secondary 这些稳定字段,而不是上游 CLI 今天把数据放在 data 里,明天又塞进 payload 里。
协议的不稳定,应该关在 Rust 层。界面的世界,能安静一点就安静一点。
用 Tauri Command/Event 替代本地 HTTP 服务
很多桌面应用喜欢在本地起一个服务:前端请求 localhost:xxxx,后端返回 JSON。熟悉,直观,也很 Web。
但这个项目不需要。
我选择用 Tauri Command/Event 替代本地 HTTP 服务,原因很实际:
- 不占本地端口
- 不处理端口冲突
- 不多套一层 HTTP 协议
- 权限边界更清楚
- 调用模型更贴近桌面应用
- 打包后的心智负担更低
前端要读取额度,就调用 command。后端要通知前端刷新,就发 event。
没有迷你 Express,没有本地端口,没有“为什么我这个小组件还开了个服务”的灵魂追问。少一层,就少一个事故现场。
架构师的克制一:不引入数据库
重构时最容易犯的毛病,是顺手做大。
看到额度数据,很容易冒出一堆想法:要不要存历史记录?要不要画趋势图?要不要做日报?要不要上 SQLite?
听起来都合理。也都暂时不该做。
Codex LED Widget 的核心价值是“一眼看到当前额度”。这里的数据是实时快照,不是账本系统。用户打开它,不是为了研究过去一周的额度消耗曲线,而是想知道现在还能不能继续用 Codex。
所以边界很明确:不引入数据库。
没有 SQLite,没有 IndexedDB,也没有“先埋个扩展点”。实时数据就实时读取。等真的需要历史统计,再为那个需求设计。现在先把复杂度请进来,只会让一个小组件提前中年发福。
架构师的克制二:前端不解析原始响应
第二个取舍是:前端不解析原始响应。
这点很关键。
Codex CLI 是外部依赖。外部依赖的意思就是,它可能会变。字段位置可能变,错误结构可能变,响应层级也可能变。
如果前端直接解析原始响应,UI 组件很快就会被协议细节污染。按钮里藏着兼容判断,状态栏里写着字段兜底,液态球旁边挂着一段“适配旧格式”的逻辑。到那时,前端就不再是前端了,更像一个小型考古现场。
所以原始响应只在 Rust 层处理。Rust 层负责把外部世界的不稳定,压成内部世界的稳定结构。Vue 层只拿 QuotaSnapshot。
这不是偷懒,是分工。
架构师的克制三:无感复用本机 Codex 登录状态
第三个取舍和信任有关:无感复用本机 Codex 登录状态。
既然用户本机已经安装并登录了 Codex CLI,这个小组件就不应该再伸手要 API Key 或 Token。它只是一个状态灯,不是账号系统,也不该把用户带进新的认证流程。
所以架构上选择调用本机 Codex CLI 读取额度,而不是自己维护登录态。应用不保存认证信息,不上传额度数据,也不要求用户输入 Token。
这类工具越小,越要克制。
小工具一旦开始要 Token,用户心里那盏红灯就先亮了。
AI 写代码,人定边界
这次重构最有意思的地方,不是 AI 会不会写 Vue,也不是它能不能写 Rust。它当然能写,而且写得很快。
真正重要的是,你要先告诉它什么不能做。
不能引入数据库。
不能把原始响应解析丢给前端。
不能加本地 HTTP 服务。
不能要求用户输入 Token。
不能把旧 Electron 思路带进新架构。
这些“不做”,才是架构决策里最值钱的部分。
AI 很擅长执行,也很容易热情过度。你给它一个小组件,它可能还你一个平台;你让它加一个状态,它可能顺手安排一套服务端。它不是懒,它是太勤快。
而架构师的工作,就是在它准备修机场的时候提醒它:
朋友,我们只是要装一个门铃。
这一篇只是开始。下一篇,我会讲怎么把“我想要一个 Codex 额度状态灯”这种一句话想法,磨成一份足够清楚的产品规格,让 AI 不再自由发挥到天边。