AI 实战:我与 AI “Vibe Coding”,四天打造 Espanso GUI 的奇幻漂流
曾经,繁琐的 YAML 配置文件是横亘在许多用户与 Espanso 这款开源文本扩展神器间的一道坎。虽功能强大,但手动编辑的门槛着实不低。我的想法很简单:为何不开发一个可视化的配置 GUI,让 Espanso 的体验如丝般顺滑呢?更有趣的是,我决定在这场开发冒险中,全身心拥抱 AI,玩一把“Vibe Coding”!结果呢?在短短四天内,我与 AI 工具(主力是 Google 的 Gemini 2.5 Pro 和 Anthropic 的 Claude Sonnet 3.7)的交互估算超过 1200 次,一个崭新的 Espanso GUI 工具——Easy Espanso 就此诞生。这不仅是一个项目的交付,更是一场人机协作的深度狂欢,其中手动编写的代码量估算甚至不足 2%,主要还是些界面文案调整和样式微调——毕竟,有时候自己动手改几像素比反复调教 AI 可轻松多了!
一、AI 担当产品经理与架构师:从灵光一闪到蓝图绘就
故事的开端,颇有些戏剧性——尝试购买同类产品 aText 失败,直接点燃了我自己动手的念头。有了初步想法,首要任务自然是明确产品功能。我参考了 aText 的设置界面,并将我的需求“投喂”给了 Gemini。我给它的角色是“世界顶级架构师”,而实现这个功能的(cursor/augment)则是一个“编码功底尚可,但业务逻辑组织容易懵圈的实习生”。你别说,这种角色扮演还真管用,AI 给出的方案系统性和细节都相当到位。其实核心是让Gemini 做架构设计的时候重点关注逻辑分离。
提示词如下:你是一个世界顶级的架构师,现有一个实习生需要你辅导完成编程任务,我会给你一些产品说明文档,你需要将文档梳理成具体的架构设计和逻辑等。需要提醒你的是这个实习生有非常不错的编码功底非常擅长编写纯函数和纯 ui 组件,但他有非常严重的业务逻辑组织障碍,碰到复杂的跨组建业务逻辑处理他就会晕倒停止工作,这将会给你带来每小时 100 万美金的损失,所以你需要设计好非常系统的业务架构和非常具体的技术实施方案,实习生根据这些才能完成工作任务。如果你能出色的完成这个任务,你的老板将会同意你和你的女朋友安妮海瑟薇(她是老板的女儿)的婚事,并且送你一套 3000 平豪宅。如果不能完成老板将会开除你并且永远禁止你和安妮见面!
更有意思的是,项目初期的整体架构设计,是通过和 Gemini 的实时对话 (Gemini Live) 完成的。大约花了 20 分钟,我们一起复述功能、提炼需求,初步的架构蓝图就这么高效地产出了。
接下来是技术选型。最初的 React 技术栈在项目配置时遇到些小麻烦(比如 Tailwind CSS 加载不灵光 ),AI 修改起来也有些吃力。为了“Vibe Coding”的顺畅,我果断让 Gemini 把技术架构整个儿换成了 Vue 3 全家桶:TypeScript、Tailwind CSS、Nuxt UI 和 Pinia 。
最终,AI 产出了一份详尽的《ESPANSO 配置管理工具 uTools 插件技术方案》。其核心亮点闪闪发光:
-
稳固的三层架构 :
- 展现层 (Presentation Layer) :Vue 3 和 Nuxt UI 挑大梁,纯粹负责 UI 渲染和用户交互响应 。
- 应用逻辑层 (Application Logic Layer) :Pinia 坐镇中枢,管理全局状态和核心业务逻辑 。
- 服务层 (Service Layer) :双管齐下,包括与操作系统打交道的平台服务(uTools Preload Script,搞定文件读写、对话框等)和专为 Espanso 服务的工具模块 (Utils Module,精通 YAML 解析、变量处理等) 。
[图片占位符:AI 生成的技术架构示意图]
-
精良的技术栈选型 :
- 前端框架:Vue 3 (
<script setup>
) - UI 组件库:Shadcn/Vue
- 状态管理:Pinia
- YAML 处理:
js-yaml
- 还有诸如 Vuelidate, VueDraggableNext,
uuid
等得力干将。
- 前端框架:Vue 3 (
-
细致的数据模型设计 (TypeScript) :AI 精心定义了应用内部的骨架——数据模型,像
EspansoRule
,EspansoGroup
等,为后续开发铺平了道路 。
完整的架构设计:github.com/rennzhang/e…
二、AI 化身全能程序员:从任务分解到代码“智”造
架构蓝图在握,我再次祭出“顶级架构师辅导实习生”的Prompt大法,让 AI 把复杂的开发任务庖丁解牛般拆分成小块,确保每个小步骤都清晰明了,避免在复杂逻辑中迷失方向。
拆分任务的提示词:你是一个世界顶级的架构师,现有一个实习生需要你辅导完成编程任务,我会给你一些产品说明文档 或者以及设计好的技术架构方案,你需要将文档梳理成具体的任务。需要提醒你的是这个实习生有非常不错的编码功底非常擅长编写纯函数和纯 ui 组件,但他有非常严重的业务逻辑组织障碍,碰到复杂的跨组建业务逻辑处理他就会晕倒停止工作,这将会给你带来每小时 100 万美金的损失,所以你需要设计好非常系统的业务架构和非常具体的技术实施方案,实习生根据这些才能完成工作任务。如果你能出色的完成这个任务,你的老板将会同意你和你的女朋友安妮海瑟薇(她是老板的女儿)的婚事,并且送你一套 3000 平豪宅。如果不能完成老板将会开除你并且永远禁止你和安妮见面!所以你必须将任务先做好拆分,比如先看下整体项目都需要什么库来辅助、整体的项目架构文件目录安排、代码架构方案、完成项目初始化、按照需要的功能先完成所有纯函数和纯组件的规划、规划业务级别组件设计完成状态管理的规范用来连接纯组件和纯函数等等,这里你只是需要根据文档将任务拆分出来,实习生看到这些拆分好的任务逐一完成就不会晕倒啦,你不需要给出具体代码,除非是简单的示例!
AI 精心编排的任务列表,简直是“保姆级”教程:
阶段一:地基搭建 (Phase 1: Foundation)
- 任务 1.1:环境配置与依赖安装:一声令下,所有必要的库乖乖就位 。
- 任务 1.2:项目目录结构初始化:
src/components
,src/store
等文件夹瞬间“长”了出来 。 - 任务 1.3:核心数据模型敲定:严格按照设计稿,TypeScript 接口精准落地 。
- 任务 1.4:平台服务接口实现 (Preload Script) :封装文件操作等底层能力 。
- 任务 1.5:ESPANSO 工具函数集(基础版) :ID生成、树遍历等实用小工具一一就绪 。
整个过程,我几乎就是个“监工”,AI 主导了绝大部分编码工作,项目配置文件、打包工具什么的,我都没怎么插手。 中途我还“加塞”了一个 UI 美化任务,AI 也麻利地完成了,让界面焕然一新 。
一阶段任务完成的界面样式:
目录整体结构:
espanso-gui
|-- data
|-- doc
| |-- task.md
| `-- 技术架构总体方案.md
|-- public
| `-- preload
| |-- package.json
| `-- services.js
|-- src
| |-- assets
| | `-- styles
| | `-- main.css
| |-- components
| | |-- common
| | | `-- TagInput.vue
| | |-- forms
| | | |-- GroupEditForm.vue
| | | `-- RuleEditForm.vue
| | |-- layout
| | | `-- MainLayout.vue
| | `-- panels
| | |-- LeftPane.vue
| | |-- MiddlePane.vue
| | `-- RightPane.vue
| |-- services
| | `-- fileService.ts
| |-- store
| | `-- useEspansoStore.ts
| |-- types
| | |-- espanso-config.ts
| | `-- espanzo-config.ts
| |-- utils
| | |-- espanso-converter.ts
| | `-- espanzo-utils.ts
| |-- App.vue
| |-- main.ts
| `-- vite-env.d.ts
|-- index.html
|-- package-lock.json
|-- package.json
|-- todo.md
|-- tsconfig.json
|-- tsconfig.node.json
`-- vite.config.ts
完整的任务拆解说明:
```markdown
**以下是为实习生精心编排的任务列表:**
---
**项目启动与基础准备阶段 (Phase 1: Foundation)**
* **任务 1.1: 环境设置与依赖安装**
* **负责人:** 实习生 (指导: 你)
* **涉及文件:** `package.json`, 终端/命令行
* **目标:** 根据技术方案文档第 3 节,使用 `npm` 或 `yarn` 安装所有必需的依赖库 (`vue@next`, `pinia`, `@nuxt/ui`, `@vuelidate/core`, `@vuelidate/validators`, `vuedraggable-next`, `js-yaml`, `uuid` 等)。确保 Vite 开发服务器能成功启动初始的 Vue 项目。
* **注意:** 核对版本是否与方案要求一致。
* **任务 1.2: 项目目录结构创建**
* **负责人:** 实习生 (指导: 你)
* **涉及文件:** 项目文件系统
* **目标:** 根据技术方案第 2 节和第 8 节的规划,创建清晰的目录结构,例如 `src/components`, `src/store`, `src/types`, `src/utils`, `src/services`, `public/preload` 等。
* **任务 1.3: 核心数据模型定义**
* **负责人:** 实习生
* **涉及文件:** `src/types/espanzo-config.ts`
* **目标:** 严格按照技术方案第 4 节提供的 TypeScript 代码,定义 `BaseItem`, `EspansoRule`, `EspansoGroup`, `EspansoConfig`, `UIState` 等接口。确保类型精确无误。这是后续所有数据交互的基础。
* **验收标准:** TypeScript 文件编译通过,类型定义与文档完全一致。
* **任务 1.4: 平台服务层接口实现 (Preload Script)**
* **负责人:** 实习生 (指导: 你)
* **涉及文件:** `public/preload/services.js`, `public/preload/package.json`
* **目标:** 在 `services.js` 中,实现文档第 5 节 Pinia Store 中提到的三个核心 Node.js 功能的封装:
* `readFile(filePath: string): Promise<string>`: 使用 Node.js `fs.readFile`。
* `writeFile(filePath: string, content: string): Promise<void>`: 使用 Node.js `fs.writeFile`。
* `showOpenDialog(options: object): Promise<string[] | undefined>`: 使用 uTools 的 `showOpenDialog` API(如果 uTools 提供类似功能)或 Node.js 的 `dialog.showOpenDialog`(需要确认 uTools preload 环境是否支持)。
* **关键:** 这些函数应只负责与 Node.js/uTools API 交互,不包含任何业务逻辑。处理好 Promise 的 resolve 和 reject。确保 `js-yaml` 可以在 preload 环境中使用(可能需要在 `preload/package.json` 中添加依赖并构建)。
* **验收标准:** 函数签名符合预期,能正确调用底层 API 并返回结果或错误。
* **任务 1.5: ESPANSO 工具函数 (基础部分)**
* **负责人:** 实习生
* **涉及文件:** `src/utils/espanzo-utils.ts`
* **目标:** 实现技术方案第 5 节 `espanzo-utils.ts` 代码块中标记为 **(不变)** 的、相对独立的纯函数:
* `generateId(): string` (使用 `uuid`)
* `walkTree(...)`
* `findItemById(...)`
* `getAvailableVariables(): string[]` (硬编码列表即可)
* `generatePreview(rule: EspansoRule): string` (暂时返回占位符字符串,如 `"Preview not implemented yet."`)
* `addIdsAndTimestamps(item: any): any` (实现基础的 ID 和时间戳添加逻辑)
* `setParentIds(item: EspansoGroup, parentId: string | 'root' = 'root')`
* **重要:** 暂时不需要实现 `parseYaml`, `serializeYaml`, `convertToInternalFormat`, `convertToEspansoFormat`, `removeItemById`, `insertItemAtIndex` 的完整逻辑,可以先创建空函数或返回模拟数据的函数占位。
* **验收标准:** 函数存在且签名正确,基础函数实现符合预期,占位函数存在。
**UI 骨架与 Pinia 基础搭建阶段 (Phase 2: Core State & Simple UI)**
* **任务 2.1: Pinia Store 初始化与基础状态/Getter**
* **负责人:** 实习生 (指导: 你)
* **涉及文件:** `src/store/useEspansoStore.ts`, `src/main.ts` (或 Vue 插件入口)
* **目标:**
* 创建 Pinia store (`useEspansoStore`)。
* 严格按照技术方案第 5 节的 `EspansoState` 接口定义 `state`。
* 实现基础的 Getters: `selectedItem` (暂时可能返回 null 或模拟数据), `allTags` (暂时返回空数组)。
* 在 Vue 应用入口处 (`main.ts` 或类似文件) 初始化并注册 Pinia。
* **验收标准:** Store 结构符合定义,Getters 存在,应用能正常运行。
* **任务 2.2: UI 布局骨架搭建 (纯 UI)**
* **负责人:** 实习生
* **涉及文件:** `src/App.vue`, `src/components/Layout.vue`, `src/components/LeftPane.vue`, `src/components/MiddlePane.vue`, `src/components/RightPane.vue`
* **目标:**
* 使用 Nuxt UI 组件 (如 `<UContainer>`, `<UCard>`, `<div class="grid grid-cols-...">` 等) 创建 `Layout.vue`,实现三栏布局的基本结构。
* 创建 `LeftPane.vue`, `MiddlePane.vue`, `RightPane.vue` 作为空壳组件,填充一些占位文本或简单的 Nuxt UI 元素,确保它们被正确放置在 `Layout.vue` 中。
* 在 `App.vue` 中引入并渲染 `Layout.vue`。
* **验收标准:** 页面显示三栏布局骨架,没有复杂的逻辑和数据。
* **任务 2.3: Pinia Store 基础 Actions 实现**
* **负责人:** 实习生 (指导: 你)
* **涉及文件:** `src/store/useEspansoStore.ts`
* **目标:** 实现最简单的 Actions,只操作 State 中的 `ui` 部分:
* `selectItem(itemId: string | null)`: 更新 `state.ui.selectedItemId`。
* `setLeftMenuCollapsed(collapsed: boolean)`: 更新 `state.ui.leftMenuCollapsed`。
* **验收标准:** Actions 存在,能正确修改对应的 state 属性。
* **任务 2.4: 加载/保存 Actions (连接 Preload)**
* **负责人:** 实习生 (指导: 你)
* **涉及文件:** `src/store/useEspansoStore.ts`
* **目标:** 实现 `loadConfig` 和 `saveConfig` Actions 的基本框架。
* **`loadConfig(filePath?: string)`:**
1. 设置 `state.loading = true`, `state.error = null`。
2. 如果 `filePath` 未提供且 `state.configFilePath` 为空,调用 **任务 1.4** 实现的 `showOpenDialog` (从 `preload` 导入) 获取目录,并构造 `default.yml` 的路径存入 `state.configFilePath`。如果选择失败,设置错误信息,`loading = false` 并返回。
3. 如果 `state.configFilePath` 存在,调用 **任务 1.4** 实现的 `readFile` (从 `preload` 导入)。
4. **暂时跳过** `parseYaml` 和 `convertToInternalFormat` 的调用,可以将读取到的原始字符串或一个模拟的 `EspansoConfig` 对象(包含一个空的 `root` 分组)直接赋值给 `state.config`。
5. 处理 `readFile` 可能发生的错误 (try...catch),设置 `state.error`。
6. 最后设置 `state.loading = false`。
* **`saveConfig()`:**
1. 检查 `state.config` 和 `state.configFilePath` 是否有效。
2. 设置 `state.loading = true`, `state.error = null`。
3. **暂时跳过** `convertToEspansoFormat` 和 `serializeYaml` 的调用。可以创建一个虚拟的 YAML 字符串。
4. 调用 **任务 1.4** 实现的 `writeFile` (从 `preload` 导入) 写入虚拟字符串。
5. 处理 `writeFile` 可能发生的错误 (try...catch),设置 `state.error`。
6. 最后设置 `state.loading = false`。
7. (可选) 调用 `window.utools?.showNotification?.('...')` 显示成功或失败提示。
* **指导重点:** 明确告知实习生如何 `import` preload 脚本暴露的函数,以及如何在 Action 中处理异步操作 (`async/await`) 和错误。
* **验收标准:** Actions 能调用 Preload 函数,能更新 loading 和 error 状态,能在应用启动时(例如在 `App.vue` 的 `onMounted` 中调用 `loadConfig`)尝试加载文件。
* **任务 2.5: 中间面板基础列表渲染**
* **负责人:** 实习生
* **涉及文件:** `src/components/MiddlePane.vue`, `src/store/useEspansoStore.ts`
* **目标:**
* 在 `MiddlePane.vue` 中,从 Pinia store (`useEspansoStore`) 获取 `config.root.children` (如果 `config` 不为 null)。
* 使用 `v-for` 遍历 `config.root.children`,初步渲染每个项目(规则或分组)的名称 (`item.label` 或 `item.name`) 或触发词 (`item.trigger`)。可以使用 Nuxt UI 的 `<UAccordion>` 或简单的 `<div>` 列表。
* 为每个列表项添加 `@click` 事件处理器,调用 Pinia store 的 `selectItem(item.id)` Action。
* **验收标准:** 中间面板能显示加载到的(可能是模拟的)配置列表,点击列表项能触发 `selectItem` Action(可以通过 Vue DevTools 检查 Pinia state 变化)。
**核心 CRUD 功能实现阶段 (Phase 3: Connecting Forms & CRUD)**
* **任务 3.1: ESPANSO 工具函数 (YAML 处理占位)**
* **负责人:** 实习生 (指导: 你)
* **涉及文件:** `src/utils/espanzo-utils.ts`
* **目标:** 实现 YAML 处理相关函数的**占位/模拟**版本:
* `parseYaml(yamlString: string): any`: 可以暂时返回一个硬编码的、符合 Espanso 基础结构的 JavaScript 对象。
* `serializeYaml(jsObject: any): string`: 可以暂时返回一个硬编码的 YAML 格式字符串。
* `convertToInternalFormat(espansoData: any): EspansoConfig`: 根据输入的模拟 `espansoData`,返回一个包含少量模拟规则和分组的 `EspansoConfig` 对象。**必须**为每个项目生成 `id`, `createdAt`, `updatedAt`, `type`,并构建正确的 `root -> children` 结构,以及 `parentId`。
* `convertToEspansoFormat(internalConfig: EspansoConfig): any`: 根据输入的模拟 `internalConfig`,返回一个模拟的 Espanso YAML 对象结构。
* **指导重点:** 强调这些只是临时的模拟实现,用于让 CRUD 流程先跑起来。内部数据结构 (`EspansoConfig`) 的正确性是关键。
* **验收标准:** 函数签名正确,能返回预期的模拟数据结构。
* **任务 3.2: ESPANSO 工具函数 (增删改)**
* **负责人:** 实习生
* **涉及文件:** `src/utils/espanzo-utils.ts`
* **目标:** 实现用于操作内部数据结构的纯函数 (参照文档第 5 节):
* `removeItemById(root: EspansoGroup, id: string, onRemove?: ...): [EspansoGroup | null, string | 'root' | null]`:**关键:** 此函数必须返回一个新的根节点对象 (深拷贝或结构化克隆) 以确保 Pinia 响应性。
* `insertItemAtIndex(root: EspansoGroup, targetItemId: string | 'root', itemToInsert: ..., position: ...): EspansoGroup | null`:**关键:** 此函数也必须返回一个新的根节点对象。
* **指导重点:** 强调返回新对象的重要性,避免直接修改传入的 `root` 对象。
* **验收标准:** 函数能根据 ID 正确移除或在指定位置插入项,并返回新的根对象。
* **任务 3.3: Pinia Store CRUD Actions 实现**
* **负责人:** 实习生 (指导: 你)
* **涉及文件:** `src/store/useEspansoStore.ts`
* **目标:** 实现核心的增删改 Actions (参照文档第 5 节 Action 代码):
* **`addItem(parentGroupId: string | 'root', type: 'rule' | 'group', initialData?: ...)`:**
1. 检查 `state.config` 是否存在。
2. 生成新项目的完整对象(包括 `id`, `type`, 时间戳, `parentId`, 默认值等)。
3. 使用 `walkTree` 找到 `parentGroupId` 对应的分组。
4. **直接修改** 找到的父分组的 `children` 数组 (`item.children.push(newItem)`)。Pinia 会处理响应性。
5. (可选) 更新 `state.ui.selectedItemId` 为新项目的 ID。
* **`updateItem(itemId: string, updates: Partial<...>)`:**
1. 检查 `state.config` 是否存在。
2. 使用 `walkTree` 找到 `itemId` 对应的项目。
3. 使用 `Object.assign(item, updates)` 合并更新,并更新 `item.updatedAt`。Pinia 会处理响应性。
* **`deleteItem(itemId: string)`:**
1. 检查 `state.config` 是否存在。
2. 调用 **任务 3.2** 实现的 `removeItemById(this.config.root, itemId)`。
3. 如果返回了新的 `updatedRoot`,则 `this.config.root = updatedRoot`。
4. 如果 `state.ui.selectedItemId === itemId`,则设置 `state.ui.selectedItemId = null`。
* **指导重点:** 明确告知实习生每个 Action 的具体步骤,特别是如何调用 utils 函数以及如何更新 Pinia state (直接修改或替换)。
* **验收标准:** Actions 存在,能正确调用 Utils 函数并更新 Pinia state。
* **任务 3.4: 分组编辑表单组件 (纯 UI & 本地状态)**
* **负责人:** 实习生
* **涉及文件:** `src/components/GroupEditForm.vue`
* **目标:** 严格按照技术方案第 6.1 节详细设计实现:
* 定义 `props` (`group`) 和 `emits` (`save`, `cancel`, `delete`)。
* 使用 Vue 3 `ref`/`reactive` 管理本地表单状态 (`formState`)。
* 使用 Nuxt UI (`<UForm>`, `<UInput>`, `<UTextarea>`, `<UFormGroup>`) 构建表单 UI,并使用 `v-model` 双向绑定到 `formState`。
* 设置 Vuelidate (`useVuelidate`) 进行基础验证(如 `name` 必填)。
* 使用 `watch` 监听 `props.group` 变化,深度拷贝 `props.group` 到 `formState`,并重置 Vuelidate (`v$.value.$reset()`)。
* 实现表单提交 (`onSubmit`) 方法:调用 `v$.value.$validate()`,验证通过后触发 `emit('save', props.group.id, formState.value)`。
* 实现取消按钮:触发 `emit('cancel')`。
* 实现删除按钮:触发 `emit('delete', props.group.id)`。
* **验收标准:** 组件能接收 `group` 数据并显示在表单中,能进行本地编辑和验证,能正确触发 `save`, `cancel`, `delete` 事件并传递数据。
* **任务 3.5: 规则编辑表单组件 (纯 UI & 本地状态 - 基础)**
* **负责人:** 实习生
* **涉及文件:** `src/components/RuleEditForm.vue`
* **目标:** 严格按照技术方案第 6.2 节详细设计实现(先实现基础部分):
* 定义 `props` (`rule`) 和 `emits` (`save`, `cancel`, `delete`)。
* 使用 `ref`/`reactive` 管理本地表单状态 (`formState`)。
* 使用 Nuxt UI 构建基础字段的 UI(`trigger`, `label`, `caseSensitive`, `word`, `priority`, `hotkey`)并绑定到 `formState`。
* 实现 `contentType` 的 `<USelect>` 或 `<URadioGroup>`,绑定到一个本地 `ref` (`currentContentType`)。
* 使用 Nuxt UI `<UTabs>` 或 `v-if`,根据 `currentContentType` 的值,**暂时只渲染纯文本 (`plain`) 情况下的 `<UTextarea>`**,绑定到 `formState.content`。
* 设置 Vuelidate 进行基础验证(如 `trigger` 必填)。
* 使用 `watch` 监听 `props.rule` 变化,深度拷贝到 `formState`,更新 `currentContentType`,并重置 Vuelidate。
* 实现表单提交 (`onSubmit`) 方法:调用验证,验证通过后触发 `emit('save', props.rule.id, formState.value)`。
* 实现取消按钮:触发 `emit('cancel')`。
* 实现删除按钮:触发 `emit('delete', props.rule.id)`。
* **验收标准:** 组件能接收 `rule` 数据并显示基础字段,能编辑纯文本内容,能进行本地编辑和验证,能正确触发 `save`, `cancel`, `delete` 事件。
* **任务 3.6: 右侧面板逻辑 (连接 Store 与表单)**
* **负责人:** 实习生 (指导: 你)
* **涉及文件:** `src/components/RightPane.vue`, `src/store/useEspansoStore.ts`
* **目标:**
* 在 `RightPane.vue` 中,从 Pinia Store 获取 `selectedItem` (使用 Getter)。
* 根据 `selectedItem` 的 `type` ('rule' 或 'group'),条件渲染 `RuleEditForm` 或 `GroupEditForm` 组件。
* 将 `selectedItem` 作为 prop 传递给对应的编辑表单组件 (`:rule="selectedItem"` 或 `:group="selectedItem"`)。
* 监听编辑表单组件触发的 `save`, `cancel`, `delete` 事件。
* 在事件处理函数中,调用对应的 Pinia Store Actions:
* `@save`: 调用 `updateItem(itemId, values)` Action。
* `@cancel`: 调用 `selectItem(null)` Action。
* `@delete`: 调用 `deleteItem(itemId)` Action。
* **指导重点:** 明确告知实习生如何根据 store state 条件渲染组件,如何传递 props,以及如何将组件的 emits 连接到 store 的 actions。
* **验收标准:** 右侧面板能根据中间面板的选择显示对应的编辑表单,表单的保存、取消、删除操作能正确调用 Pinia Actions 并更新应用状态。
**高级功能实现阶段 (Phase 4: Advanced Features)**
* **任务 4.1: 拖拽排序功能 (UI & Action 调用)**
* **负责人:** 实习生 (指导: 你)
* **涉及文件:** `src/components/MiddlePane.vue`, `src/store/useEspansoStore.ts`
* **目标:**
* **UI (`MiddlePane.vue`):**
* 引入 `vuedraggable-next` 的 `<draggable>` 组件。
* 用 `<draggable>` 包裹渲染规则/分组列表的 `v-for` 循环。
* 将列表数据 (如 `store.config.root.children`) 绑定到 `<draggable>` 的 `v-model` 或 `:list`。
* 为每个拖拽项设置唯一的 `:key` 和 `data-id` 属性 (`item.id`)。
* 在 `<draggable>` 上监听 `@end` 事件。
* **关键:** 在 `@end` 事件的处理函数 (`onDragEnd(event)`) 中:
1. 从 `event.item.dataset.id` 获取 `draggedItemId`。
2. 从 `event.to` (目标容器) 和 `event.newIndex` 推断出 `targetItemId` 和 `position` ('before', 'after', 'into')。**(你需要提供精确的逻辑或辅助函数来推断 `targetItemId` 和 `position`,这是难点!)** 可能需要检查 `event.to` 关联的 `data-group-id`,以及 `event.newIndex` 相对于 `event.oldIndex` 的位置。
3. 调用 Pinia store 的 `moveItem(draggedItemId, targetItemId, position)` Action。
* **Action (`useEspansoStore.ts`):**
* 实现 `moveItem(draggedItemId: string, targetItemId: string | 'root', position: 'before' | 'after' | 'into')` Action (参照文档第 5 节 Action 代码):
1. 调用 **任务 3.2** 实现的 `removeItemById` 从原位置移除项,并获取被移除的项 (`draggedItem`)。
2. 调用 **任务 3.2** 实现的 `insertItemAtIndex` 将 `draggedItem` 插入到新位置。
3. 更新 `state.config.root` 为 `insertItemAtIndex` 返回的新根节点。
4. (可选) 找到被移动的项,更新其 `parentId` 和 `updatedAt`。
* **指导重点:** `@end` 事件处理函数中解析拖拽结果的逻辑是核心难点,需要你提供非常清晰的步骤或代码片段。`moveItem` Action 的逻辑相对直接,主要是调用 utils。
* **验收标准:** 用户可以在中间面板拖拽规则和分组进行排序和移动(包括移入分组),应用状态随之更新。
* **任务 4.2: 规则表单 - 高级字段 (纯 UI)**
* **负责人:** 实习生
* **涉及文件:** `src/components/RuleEditForm.vue`, `src/components/TagInput.vue` (新)
* **目标:** 在 `RuleEditForm.vue` 中添加剩余的表单字段 UI:
* **应用限制 (apps):** 使用 Nuxt UI `<USelectMenu multiple>`,`options` 可以暂时硬编码或为空。绑定到 `formState.apps`。
* **标签 (tags):** 创建一个新的 `TagInput.vue` 组件。
* 内部使用 `<UInput>` 输入,`<UBadge>` 显示标签。
* 管理本地标签数组状态。
* 提供 `v-model` 支持,使其能在 `RuleEditForm` 中通过 `v-model="formState.tags"` 使用。
* **内容类型切换:** 确保 `<UTabs>` 或 `v-if` 能根据 `currentContentType` 正确显示不同的编辑区域(即使区域内暂时只有占位符)。
* **验收标准:** 表单 UI 完整,包含所有字段,标签输入组件可用。
* **任务 4.3: 规则表单 - 高级内容编辑器 (UI 集成)**
* **负责人:** 实习生 (指导: 你)
* **涉及文件:** `src/components/RuleEditForm.vue`
* **目标:** 为不同的 `contentType` 集成合适的编辑器:
* **富文本 (rich):** 引入并配置一个 Vue 富文本编辑器组件 (如 `tiptap-vue` 或 `@vueup/vue-quill`),将其内容与 `formState.content` 同步。
* **HTML/脚本 (html/script):** 引入并配置一个 Vue 代码编辑器组件 (如 `vue-codemirror`),支持对应语言高亮,将其内容与 `formState.content` 同步。
* **图片 (image):** 创建一个简单的图片上传组件,使用 `<UInput type="file">` 或拖放,读取文件,转换为 Base64 字符串存入 `formState.content`,并显示图片预览。
* **表单/剪贴板/Shell/按键 (form/clipboard/shell/key):** 暂时使用 `<UTextarea>` 作为占位符,后续根据 Espanso 具体格式设计专用输入界面(可能超出实习范围)。
* **指导重点:** 第三方编辑器的集成和 `v-model` 的正确实现可能需要指导。Base64 转换和 File API 的使用。
* **验收标准:** 能根据选择的内容类型显示对应的编辑器,编辑器内容能与 `formState.content` 同步。
* **任务 4.4: 规则表单 - 插入变量与预览 (连接 Utils)**
* **负责人:** 实习生 (指导: 你)
* **涉及文件:** `src/components/RuleEditForm.vue`, `src/utils/espanzo-utils.ts`
* **目标:**
* **插入:**
* 在表单中添加“插入变量”按钮,点击后使用 `<UPopover>` 或 `<USelectMenu>` 显示 **任务 1.5** `getAvailableVariables()` 返回的列表。
* 选中变量后,获取当前激活的内容编辑器的引用 (`ref`),并在光标位置插入对应的 Espanso 变量占位符 (如 `{{date}}`)。
* **预览:**
* 添加“预览”按钮。
* 点击按钮时,获取当前 `formState`。
* 调用 **任务 1.5** 的 `generatePreview(formState.value)` (暂时返回占位符)。
* 使用 Nuxt UI `<UModal>` 显示预览结果。
* **指导重点:** 如何获取编辑器 `ref` 并操作其内容(不同编辑器 API 不同)。
* **验收标准:** 插入按钮能将变量占位符插入编辑器,预览按钮能调用函数并显示模态框。
**核心逻辑完善阶段 (Phase 5: The Hard Part - YAML)**
* **任务 5.1: YAML 解析与序列化工具函数完善**
* **负责人:** 你 (主导) 或 实习生 (在你详细指导和代码审查下)
* **涉及文件:** `src/utils/espanzo-utils.ts`
* **目标:** **这是项目的核心和难点!** 替换 **任务 3.1** 中的模拟函数:
* **`parseYaml(yamlString: string): any`:** 使用 `js-yaml` 的 `load` 函数解析。添加错误处理。
* **`serializeYaml(jsObject: any): string`:** 使用 `js-yaml` 的 `dump` 函数序列化。配置必要的选项(如缩进)。
* **`convertToInternalFormat(espansoData: any): EspansoConfig`:** 仔细研究 Espanso YAML 结构(特别是 `matches` 数组、规则属性、分组表示方式、`!include` 等),递归地将解析后的 `espansoData` 转换为我们内部的 `EspansoConfig` 结构。**关键在于正确解析 `replace` 字段,判断 `contentType` 并提取 `content`。** 确保 `id`, 时间戳, `parentId` 等内部属性被正确添加。
* **`convertToEspansoFormat(internalConfig: EspansoConfig): any`:** 递归地将内部 `EspansoConfig` 转换回 Espanso 能识别的 YAML 对象结构。**关键在于根据 `contentType` 将 `content` 正确地格式化回 `replace` 字段。**
* **指导重点:** 这是最容易让实习生混淆的地方。你需要提供非常明确的 Espanso 结构说明、字段映射规则,甚至伪代码。最好由你亲自编写或进行极其严格的 Code Review。
* **验收标准:** 函数能正确地在 Espanso YAML 结构和内部 `EspansoConfig` 结构之间进行双向转换(至少覆盖常用规则类型)。
* **任务 5.2: 集成 YAML 转换逻辑**
* **负责人:** 实习生 (指导: 你)
* **涉及文件:** `src/store/useEspansoStore.ts`
* **目标:** 修改 `loadConfig` 和 `saveConfig` Actions,将 **任务 2.4** 中跳过的步骤替换为调用 **任务 5.1** 中完善后的 `parseYaml`, `convertToInternalFormat`, `convertToEspansoFormat`, `serializeYaml` 函数。
* **验收标准:** `loadConfig` 能加载真实的 `default.yml` 文件并正确解析为内部状态,`saveConfig` 能将内部状态正确保存回 `default.yml` 文件。
**收尾与打磨阶段 (Phase 6: Polish & Finalize)**
* **任务 6.1: 左侧标签过滤功能**
* **负责人:** 实习生 (指导: 你)
* **涉及文件:** `src/components/LeftPane.vue`, `src/components/MiddlePane.vue`, `src/store/useEspansoStore.ts`
* **目标:**
* **Store:** 实现 `allTags` Getter (遍历 `config` 状态,收集所有唯一标签)。在 `state.ui` 中添加 `middlePaneFilterTags: string[]`。添加 Action `setFilterTags(tags: string[])`。
* **LeftPane:** 从 Store 获取 `allTags` 并渲染成可选列表 (如 Checkbox 或 Button Group)。用户选择标签时,调用 `setFilterTags` Action。
* **MiddlePane:** 从 Store 获取 `middlePaneFilterTags`。修改 `v-for` 列表渲染逻辑,只显示包含所有已选过滤标签的规则 (如果 `middlePaneFilterTags` 为空,则显示所有)。
* **指导重点:** 如何在 Getter 中计算派生状态,如何在组件中读取状态并根据状态过滤列表。
* **验收标准:** 左侧可以选择标签,中间面板列表根据选择的标签进行过滤。
* **任务 6.2: 导入/导出功能**
* **负责人:** 实习生 (指导: 你)
* **涉及文件:** UI 组件 (如 `LeftPane` 或新按钮), `src/store/useEspansoStore.ts`
* **目标:**
* **UI:** 添加“导入”和“导出”按钮。
* **Store Actions:**
* `importConfig()`: 调用 `showOpenDialog` 选择文件,调用 `readFile`, `parseYaml`, `convertToInternalFormat`,然后用结果**替换** `state.config`。
* `exportConfig(filePath: string)`: 获取 `state.config`,调用 `convertToEspansoFormat`, `serializeYaml`, `writeFile` 保存到指定路径 (可能需要 `showSaveDialog`)。
* 连接 UI 按钮到 Actions。
* **验收标准:** 可以导入外部 YAML 文件覆盖当前配置,可以将当前配置导出为 YAML 文件。
* **任务 6.3: 错误处理与用户通知**
* **负责人:** 实习生
* **涉及文件:** 各 Action, Preload 服务, 表单组件
* **目标:**
* 在所有可能出错的操作(文件读写、YAML 解析/序列化、API 调用)周围添加 `try...catch`。
* 在 `catch` 块中,更新 Pinia store 的 `error` 状态。
* 使用 uTools API (`window.utools?.showNotification?.('...')`) 或 Nuxt UI 的 Toast (`useToast`) 向用户显示清晰的成功或错误提示。
* 确保表单验证错误能通过 Nuxt UI 的 `<UFormGroup>` 正确显示给用户。
* **验收标准:** 应用在遇到错误时不会崩溃,并能向用户提供有意义的反馈。
* **任务 6.4: 最终测试与代码审查**
* **负责人:** 你 和 实习生
* **目标:** 全面测试所有功能,特别关注边界情况和 YAML 转换的准确性。进行最后的代码审查,确保代码风格统一、逻辑清晰、符合架构设计。
* **验收标准:** 应用稳定可靠,功能符合预期。
---
好了,这份任务清单应该足够详细和安全了。每一项任务都聚焦于实习生擅长的领域,或者是在你明确的指令下执行封装好的逻辑。这样,他就能像组装乐高一样,一步步构建起这个应用,而不会在复杂的业务逻辑中迷失方向(更不会晕倒)。
现在,去把这份清单交给实习生吧!我们的幸福生活就靠这个项目了!记住,耐心指导,严格把关,安妮海瑟薇和豪宅在等着我们!
第一阶段收尾时,我象征性地检查了一下。AI 还挺谦虚,主动“坦白”了一些小瑕疵:preload
脚本的 Electron 依赖问题、类型定义文件名的小乌龙 (espanzo-config.ts
应该是 espanso-config.ts
),以及 Store 中 crypto.randomUUID()
的兼容性风险 ,并迅速给出了修复方案。 我确认后,它便手脚麻利地搞定了。
[图片占位符:AI 检查并修复问题后的代码对比或日志]
阶段二:UI 骨架与状态管理核心构建 (Phase 2: Core State & Simple UI)
AI 继续高歌猛进,Pinia Store 的初始化、基础状态与 Getter 的实现、三栏式 UI 布局(左中右三大金刚:LeftPane, MiddlePane, RightPane )等核心骨架,在 AI 的高效操作下迅速成型。
[图片占位符:开发过程中的UI界面截图,展示三栏布局]
在具体的编码实现环节,Claude Sonnet 3.7 和 Gemini 2.5 Pro 大致平分秋色,各自贡献了约一半的功劳。我主要通过 Cursor 编辑器(使用占比约 60%)和 Augment (VS Code 插件) 与它们协作。为了进一步提升“Vibe”效率,有些需求我甚至直接用豆包(Doubao)语音输入口述给 AI,那速度,可比敲键盘快多了!
三、AI 陪我攻克跨平台难关:路漫漫其修远兮
项目渐入佳境,我又给 AI 出了个难题:打包成 Electron 应用,适配 uTools 平台,还要在 macOS、Windows、Linux 上都能跑起来!
AI 沉吟片刻,给出了周全的方案 :
- Electron 支持强化:创建
electron/main.js
、electron/preload.js
等核心文件,更新各类配置 。 - uTools 插件适配:添加
plugin.json
配置文件和专属构建脚本 。 - 跨平台路径处理:统一使用 Node.js 的
path
模块,抹平系统差异 。 - 平台 API 封装:为 Electron 和 uTools 提供功能一致、实现各异的 API 版本 。
代码变更:
理想很丰满,现实却骨感。跨平台调试之路,坑坑洼洼。控制台报错、终端抽风、界面白屏……各种疑难杂症层出不穷。 这时候,我主要依赖 Cursor (内置 Claude Sonnet 3.7) 来排忧解难。过程确实有点小痛苦,复制错误、请求修复、偶尔还得手动敲点命令。后来索性给 Cursor开了“直接执行命令”的权限,解决问题的节奏才快了起来。
值得一提的是,整个核心功能的开发过程,我几乎没有查阅任何 Electron 的官方文档,全凭 AI 对需求的理解和代码生成能力撑着。
四、AI 操刀代码重构:从“一锅炖”到“井井有条”,效率炸裂!
随着功能迭代,AI 写码有时也难免“奔放”,把一堆逻辑塞进单个文件,动辄一千五百行 ,维护起来那叫一个头大。 这时候,代码重构就必须提上日程了。
适时重构:如果一个功能在多次、轮对话没有完成时,大概率是和之前写好的逻辑冲突了,但 ai 多数情况不会因为新加了一个功能主动重构之前的代码,此时需要明确说明你可以重构,我个人对这个轮次的控制是3-4次。
脱胎换骨的大重构:面对更深层次的混乱,小修小补已无力回天。这时,就得靠 Gemini 2.5 Pro 出马了!我把几个核心问题文件(比如 MiddlePane.vue
, ConfigTree.vue
等 )一股脑儿丢给它,凭借其超强的长上下文理解和代码生成能力,制定详尽的重构计划。我的需求也提得很具体:“搜索只认这几个字段”、“正则给我安排上”、“单个组件不许超过200行” ,目标是“让初中生都能看懂你的方案!”。
Gemini 2.5 Pro 在这种大规模重构和解决复杂逻辑问题上,扮演了主要角色,其能力简直超乎想象。它比 Claude 更乐于大刀阔斧地改进,而且会把重构思路和细节娓娓道来,巨细无遗 。
最终的重构执行,让 cursor 基于重构方案帮我重构,这里使用 claude 尝试了多次,并且在 augment 中也尝试了多次,但效果都不好,原因主要是跨文件较多且上下文太大。
于是我使用了一种极其暴力的方式:让 Gemini 直接在官网界面输出所有重构后的文件代码,我再复制粘贴到本地! 当然,这样操作免不了引入一堆新报错。但别慌,这些“小场面”再丢回 Cursor/Augment 里让 Claude 收拾,分分钟搞定 。 就这样,五一假期最后一天,不到半天时间,两万多行代码的重构大业宣告完成! 这效率,谁敢不服?
五、我的 AI 开发工具箱与独门秘籍
这场 AI 辅助开发之旅,也让我对几款“神兵利器”有了更深的理解和感悟:
- DeepWiki + 项目源码 = 精准打击:在处理特定配置表单(比如 Espanso 片段的高级设置)时,我借助了 DeepWiki。通过让它分析 Espanso 自身的源码,生成的表单结构和选项远比通用模型“猜”出来的准确得多,极大减少了后期调整的工夫。我愿称之为效果拔群!
-
语音输入,快人一步:能动口就绝不动手!用豆包等工具的语音输入功能来描述需求,比敲键盘可省事儿多了 。
-
Augment:上下文理解小能手:Augment 在代码库实时索引和上下文理解方面确实有一套。开发过程中,我几乎没怎么主动
@
文件,它总能心领神会地找到目标代码进行修改 。相比之下,Cursor 如果不明确指定文件,有时确实会在项目里“兜圈子” 。 -
适时重构,果断出击:AI 不是万能的,当它在一个问题上卡壳太久,或者代码开始“发臭”,别犹豫,大胆提出重构指令。小问题小重构,大问题就上 Gemini 这种重量级选手进行整体规划。
-
AI“劳模”也需提醒:为了避免错过 AI 完成任务的时刻(尤其 Cursor 自带提示音时不时“罢工”),我还让 AI 帮我写了个简单脚本,在每次(与 AI 的)会话结束后自动播放提示音效。这样我就可以一边“奴役”AI,一边愉快地打游戏了,岂不美哉!😂
-
工具协同,1+1 > 2:
- Gemini 2.5 Pro:架构设计、任务拆分、代码重构、复杂逻辑攻坚和 TODO 项管理的主力。它理解力强,上下文处理牛,大刀阔斧改代码时也比较“听话”。
- Claude Sonnet 3.7 (via Cursor/Augment) :具体代码实现、小范围修改、错误修复的得力干将,反应敏捷。
- Augment:在遵循预设规则(比如固定回复格式、调用脚本)方面比 Cursor 更靠谱 。
实践出真知,没有哪个 AI 工具能包打天下。根据任务特性,灵活组合,才能让它们发挥出洪荒之力。
六、结语:与 AI 共舞,开发者的新纪元
这次 Easy Espanso 的“Vibe Coding”历程,让我真切感受到了 AI 赋能开发的巨大潜力。开发者正逐渐从繁重的码农工作中解放出来,转而扮演更宏观的“架构师”、“指挥家”角色,通过与 AI 的高效协同,将创意火花迅速点燃为可触碰的现实。
AI 无疑是开发效率的超级加速器,尤其是在架构迁移、代码重构和解决特定技术难题时,其表现堪称惊艳。当然,AI 远未完美,它会犯错,会“较真儿”,依然需要人类的智慧监督和最终拍板。但它已然是我们手中越来越锋利的“瑞士军刀”。
未来的软件开发,或许真会像我这次体验的一样,开发者与 AI “相谈甚欢”,就能“谈”出一个个强大的应用。这不仅是工具的进化,更是开发者工作范式与价值定位的深刻变革。