先拆清楚,再决定怎么重构(一)

30 阅读8分钟

重构一个项目,最危险的时刻不是开始删代码,而是刚打开项目、心里冒出一句“这不挺简单吗”的时候。

这句话一出现,基本就要坏事。

你会觉得:不就是个桌面小工具吗?UI 改一改,逻辑挪一挪,依赖升一升,今晚就能收工。结果两小时后,你发现自己正在和一堆历史包袱对坐:旧的 Electron 语境还在,UI 审计记录没完全消化,前后端边界也不够干净。项目不大,但有些地方已经开始有味儿了。

这次我换了个玩法:我不写代码。

我的工作只剩三件事:

  1. 先看清楚旧项目到底乱在哪。
  2. 决定新架构应该怎么分层。
  3. 让 AI 按这个边界去实现。

代码交给 AI,架构决策我来做。听着轻松,其实更像坐在副驾上指挥一辆很快的车:你不踩油门,但你得知道什么时候该喊刹车。

项目地址:github.com/Wolfares526…

先承认它有历史包袱

原项目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 层。

01-internal-tauri-vue-architecture.png

不要让前端去碰这些。前端很擅长做界面和交互,但你让它处理 Windows 路径、CLI 协议和系统权限,它可能会先沉默三秒,然后给你造出一个更大的问题。

Vue 层负责状态、交互、液态 UI

Vue 层只做用户看得到、点得到、感知得到的部分:

  • 展示当前额度状态
  • 在 5 小时和 7 天额度窗口之间切换
  • 渲染液态 UI
  • 根据额度切换绿色、黄色、红色、错误、读取中状态
  • 处理刷新、隐藏、置顶、中英文切换
  • 接收 Rust 层通过事件发来的状态变化

关键点是:Vue 层不需要知道 Codex CLI 原始响应长什么样。

它只接收一个整理好的 QuotaSnapshot。前端关心的是 remainingPercentresetsAtprimarysecondary 这些稳定字段,而不是上游 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,也没有“先埋个扩展点”。实时数据就实时读取。等真的需要历史统计,再为那个需求设计。现在先把复杂度请进来,只会让一个小组件提前中年发福。

01-internal-architecture-restraint.png

架构师的克制二:前端不解析原始响应

第二个取舍是:前端不解析原始响应。

这点很关键。

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 不再自由发挥到天边。