[0514]AI EDITOR VIBE_LOG
Vibe Coding 过程复盘日志
项目:AI Editor — 本地 Markdown WYSIWYG 桌面编辑器
日期:2026-05-14
耗时:约 6 小时(从空脚手架到打包出 .exe)
灵感与初衷 (The Spark)
起因很简单:想要一个本地的、Typora 风格的 Markdown 编辑器。
不要云同步、不要订阅、不要臃肿的插件系统。就是打开、写、保存——干净利落。当前市面上要么是 Typora 开始收费,要么是 Obsidian 太重,要么就是纯源码编辑器看不出渲染效果。
另外有一个很有意思的伏笔:现在不需要 AI 能力,但架构上要留口子。将来某天想加 AI 功能的时候,不需要把整个项目推翻重来。
所以核心需求就三句话:
- 所见即所得的 Markdown 编辑(像 Typora)
- 多标签页管理(不是单文件,也不是重型文件树)
- AI 扩展槽位(预留接口,不实现)
头脑风暴与方案定型 (Brainstorming)
编辑器引擎:三个方案,一个选择
这是整个项目最关键的技术决策。三个备选方案:
| 方案 | 引擎 | 优点 | 缺点 |
|---|---|---|---|
| A | Milkdown (ProseMirror) | 开箱即用的 WYSIWYG、插件生态好 | 深度定制需要了解 ProseMirror 内部 |
| B | mdxeditor (Lexical) | Meta 出品、React 友好 | Markdown 支持不如 Milkdown 成熟 |
| C | Monaco + 自研 WYSIWYG | 完全可控 | 工作量巨大,大概率造出烂轮子 |
选了 A — Milkdown。原因很实在:Milkdown 是为「Typora 式 Markdown 编辑器」这个场景设计的,而不是通用富文本编辑器缝了个 Markdown 插件。
后来在实现阶段发现 Milkdown v7 的 API 和文档里的 v6 有很大差异(useEditor vs useInstance、listenerCtx 的配置方式、getMarkdown 要用 serializerCtx),但整体方向是对的。
架构定型
Electron Main Process (文件 I/O + 图片处理)
│ IPC (contextBridge, contextIsolation)
▼
React Renderer (Context + useReducer + Milkdown)
关键设计决策:
- 文件 I/O 放主进程,不开放
nodeIntegration,安全第一 - Context + useReducer 而不是 Redux——这个项目状态不复杂,Redux 属于杀鸡用牛刀
- 5 分钟自动保存——用户明确要求(最初讨论是 3 秒 debounce,用户改成 5 分钟)
- 图片存
images/同级目录——像 Typora 默认行为,相对路径引用,搬家不丢图 - AI 扩展槽——定义
AIExtension接口(transform+transformStream),AIPanel组件占位
核心功能实现路径 (The Build)
实现用的是 Subagent-Driven Development 模式——把 16 个 task 逐个派发独立子代理,每个 task 走"实现 → spec review → code quality review"三轮。
实现时间线
Phase 1:基础设施 (Task 1-3)
先搭骨架。vitest + testing-library 测试环境、TypeScript 类型定义(Tab、EditorState、EditorAction、AIExtension)、EditorContext 状态管理。
这里有个小插曲:EditorContext 的 code review 发现 context value 没包 useMemo,每次渲染都创建新对象。虽然当时测试都过了,但确实是个性能隐患。加上后 clean。
Phase 2:组件搭建 (Task 4-10)
一口气搞了 7 个组件:ErrorBoundary、TitleBar、TabBar、StatusBar、MilkdownEditor、EditorArea、AIPanel。
Milkdown 集成是这里最难的部分。代理在实现时发现 v7 的 API 和 task 描述不一样——listenerCtx 要用 ListenerManager 实例而不是回调数组,getMarkdown() 要经过 serializerCtx。代理自己消化了这些差异,没有上来就问,这点很赞。
Phase 3:Electron 层 (Task 11-13)
preload.ts(IPC 桥接)、fileHandlers.ts(打开/保存/另存为 + 大文件 2MB 警告)、imageHandlers.ts(图片写入 images/ 目录 + MD5 命名)。
Phase 4:总装 (Task 14)
把 App.tsx 彻底重写,接上所有组件 + 键盘快捷键 + IPC 调用。代理还发现了 vite-env.d.ts 里有一个旧版的 electronAPI 类型声明,和新的 electron.d.ts 冲突,导致 TypeScript 报错。删掉后 clean。
Phase 5:用户测试 → 5 个 bug 修复
全部测试通过后启动 app,用户上手试,立刻发现了 5 个问题。这是今天最有价值的环节——
踩坑与调试实录 (Debug & Fixes)
Bug 1:奇怪的边距和滚动条
现象: 编辑器周围有莫名边距和滚动条。
根因: 两个问题叠加——① index.html 没有 CSS reset(body 默认 margin);② EditorArea 的 overflow: auto 让容器本身出现滚动条。
修复: 加 CSS reset(* { margin:0; padding:0; box-sizing:border-box });overflow: auto → overflow: hidden。后来用户反馈长内容无法滚动时又改成 overflow-y: auto + min-height: 100%。
Bug 2:没有按钮,只能快捷键
现象: 用户不知道 Ctrl+N/O/S,界面上没有任何可点击的东西。
根因: 设计 spec 里根本没提工具栏。我只关注了键盘操作,忘了 GUI 的基本可用性。
修复: 新增 Toolbar 组件,放 New / Open / Save 三个按钮。TabBar 里的 + 按钮也移到了工具栏。同时需要更新 TabBar 测试(原本测试 "+" 按钮的功能)。
Bug 3:新文件保存报错 "fail to save to..."
现象: Ctrl+N 新建标签页后 Ctrl+S 保存,弹窗报错。
根因: 新建标签页的 path 是空字符串 '',saveFile('', content) 直接把空字符串当路径传给了 fs.writeFileSync。
修复: handleSave 里加判断——如果 path 为空,调 saveFileAs 弹出另存为对话框,成功后更新 tab 的 title 和 path。这个修复的细节比较微妙:需要 closeTab + openTab 来更新已有 tab 的 path(因为 EditorAction 没有 UPDATE_PATH)。
Bug 4:编辑器不自动聚焦
现象: 新建标签页后一片空白,没有光标闪烁,必须鼠标点击一个很小的区域才能激活编辑。
根因: Milkdown 的 ProseMirror 编辑器不会自动聚焦。我经历了三次迭代才搞定:
| 尝试 | 方式 | 结果 |
|---|---|---|
| 1 | requestAnimationFrame(() => view.focus()) |
失败,DOM 还没就绪 |
| 2 | setTimeout(() => view.focus(), 100) |
不稳定,取决于机器速度 |
| 3 | useInstance() 监听 loading 状态 |
成功 |
第三次用了 useInstance() 返回的 loading——只有当 Milkdown 报告 editor 已就绪时才聚焦。这才是正确的时机。同时在 EditorArea 加了 useEffect 监听 activeTab.id 变化,tab 切换时也主动聚焦。
Bug 5:Typora 风格
现象: 整个界面是 VS Code 暗色风格,和 Typora 的清新浅色完全不同。
修复: 6 个组件全面换肤——#1e1e1e → #ffffff,#cccccc → #333333,蓝色 #007acc 状态栏 → 浅灰 #f5f5f5,Tab 指示条从无到蓝色底部边框。Toolbar 加浅灰背景和圆角按钮。
Bug 6 & 7:光标不到位 + 黄边框
用户反馈两个细节问题:光标还是不自动出现(需要进一步调试) + 编辑器聚焦时 ProseMirror 有黄色 outline。
光标问题的根因是 useEditor 的时机——需要用 useInstance() 而不是固定延迟。黄边框修起来简单:.ProseMirror:focus { outline: none !important }。
高光时刻
"一次性写对"的模块: EditorContext 的 reducer 逻辑。6 个 action 的状态转换、CLOSE_TAB 的邻居选择算法、UPDATE_DOC 的自动 dirty 标记——代理第一次就完全写对了,12 个测试一举通过。这可能是因为 discriminated union 的类型约束足够强,让 reducer 实现几乎没有犯错空间。
"反复调整"的模块: 自动聚焦。从 requestAnimationFrame 到 setTimeout(100) 到 useInstance().loading,前前后后改了 4 次代码。这个教训是:不要猜测异步组件的就绪时机,要找框架提供的就绪信号。
当前成果与遗憾 (Outcome & Todo)
今天交付的成果
- Electron 桌面应用 — 可安装的 .exe 文件(80MB NSIS installer)
- 27 项单元测试 — 全部通过,覆盖状态管理 + 4 个组件
- TypeScript 零错误 — 渲染进程 + 主进程双配置
- 完整文档 — 设计规格、实现计划、PROJECT_README、知识图谱、本日志
功能清单
| 已实现 | 状态 |
|---|---|
| Milkdown WYSIWYG Markdown 编辑 | ✅ |
| 多标签页管理(打开/关闭/切换) | ✅ |
| 文件保存/另存为/打开 | ✅ |
图片粘贴与拖拽 → images/ 目录 |
✅ |
| 5 分钟自动保存 | ✅ |
| 标签页切换前自动保存 | ✅ |
| 大文件警告(>2MB) | ✅ |
| 崩溃降级(ErrorBoundary → raw Markdown) | ✅ |
| 键盘快捷键(Ctrl+N/O/S) | ✅ |
| 工具栏按钮(New/Open/Save) | ✅ |
| AI 扩展接口 + 占位面板 | ✅(接口就绪,未实现) |
| Electron 打包(Windows NSIS) | ✅ |
留给明天的 TODO
- 右键菜单 — 标签页上右键"关闭其他"/"关闭右侧"是刚需
- 拖拽排序标签页 — 目前标签页顺序固定
- 最近打开的文件列表 — 无文件树的情况下,这个很实用
- Markdown 语法工具栏 — 加粗/斜体/标题/列表的格式化按钮(目前只靠 Milkdown 内置 markdown 快捷键)
- macOS 打包测试 —
electron-builder配置了 dmg 目标但没验证 - 崩溃恢复 — spec 里写了"检测 5 分钟自动保存的临时副本,下次打开时恢复",还没实现
- AI 功能原型 — 既然接口已经有了,可以先接一个简单的 LLM 调用试试
- 图标 — electron-builder 提示用的是默认图标,缺一个应用 icon
开发体验收获
整个流程走下来,最大的感受是 "Subagent-Driven Development + 用户测试反馈" 这个组合拳的高效。16 个 task 被逐个派发独立子代理,每个 task 都有 spec compliance review + code quality review 两道关卡——这意味着大部分低级错误在提交前就被拦下了。
但用户测试发现的 5 个问题,没有一个是测试能查出来的:界面风格、工具栏缺失、光标位置、滚动条行为——这些都是体验问题,不是逻辑 bug。这再次验证了:自动化测试保证代码正确性,但只有人真正上手用,才能知道体验对不对。
以上就是 2026-05-14 的 Vibe Coding 日志。一个空脚手架出发,经历 16 个 task、7 个 bug、多次样式调整,最终产出了一个可以发给朋友试用的桌面 Markdown 编辑器。充实的一天。
posted on 2026-05-18 15:57 fox_charon 阅读(8) 评论(0) 收藏 举报
浙公网安备 33010602011771号