flomo to obsidian importer 2.0 开发记录
- Published on
- ...
- Authors

- Name
- Huashan
- @herohuashan
背景
作为一个长期使用 Flomo 和 Obsidian 的用户,我一直在用 jia6y/flomo-to-obsidian 这个插件来同步我的 Flomo 笔记到 Obsidian。之前虽然插件基本功能可用,但是随着 flomo的更新,导出逻辑也变了,插件不能用了,需要debug 之后才能使用。 原作者已经很久没有更新了,所以我决定 fork 这个项目,自己动手解决这些问题。
关于 AI 辅助开发
坦白说,我并不是一个专业的软件开发者。 这个插件的开发过程,完全是在 AI 的帮助下完成的 —— 一种我称之为 "Vibe Coding" 的方式。
什么是 Vibe Coding?就是:
- 🤔 我有明确的需求和想法("我想要这个功能")
- 💬 我用自然语言描述给 AI(Claude、Cursor 等)
- 🤖 AI 帮我写代码、调试、优化
- ✅ 我测试、验证,提出改进意见
- 🔄 循环往复,直到功能完善
这是一个非常美好的时代。有了 AI 之后:
- ✨ 想法可以快速变成现实 - 不需要花几年时间学习 TypeScript、Playwright、Obsidian API
- 🚀 专注于"我要什么"而不是"怎么实现" - 把精力放在产品思考和用户体验上
- 🎯 迭代速度极快 - 发现问题立即优化,而不是被技术细节卡住
- 📚 在实践中学习 - AI 会解释代码,帮你理解技术原理
我的开发流程
- 明确痛点 → "每次同步浏览器都弹出来,太烦了"
- 问 AI → "怎么让 Playwright 后台运行?"
- AI 给方案 → "设置
headless: true" - 我来测试 → 运行插件,验证效果
- 发现新问题 → "附件路径有 4 层,太复杂了"
- 继续问 AI → 循环上述流程
为什么要分享这个
我希望通过这个项目告诉大家:
- 🌟 不要被技术门槛吓退 - 你不需要成为专家才能开发有用的工具
- 🤝 AI 是最好的学习伙伴 - 它既是老师,又是搭档
- 🎨 创意比技能更重要 - 知道"要什么"比知道"怎么做"更有价值
- 🔧 Fork 和改造是最好的实践 - 基于现有项目,加入自己的需求
给你的建议
如果你也在用 Flomo 和 Obsidian,欢迎 fork 这个项目,根据你自己的需求来定制:
- 想要不同的文件夹结构?改!
- 想要特殊的标签处理?加!
- 想要定时自动同步?写!
有 AI 帮忙,这些都不是问题。这是一个普通人也能开发软件的时代。
开发过程
第一步:静默同步 - 消除打扰
问题:每次点击同步,可能会有浏览器窗口干扰工作流程。
解决方案:
- 修改 lib/flomo/exporter.ts,确保 Playwright 使用
headless: true - 保留认证时显示浏览器(因为需要处理验证码)
// 确保后台运行
browser = await playwright.chromium.launch(** headless: true **);
效果:自动同步完全在后台进行,不会打断工作流。
第二步:简化附件结构 - 从 4 层到 2 层
问题:Flomo 导出的附件路径是这样的:
flomo picture/file/2025-11-03/[USER_ID]/filename.m4a
这个路径有 4 层目录:
flomo picture/- 附件根目录file/- 没必要的中间层2025-11-03/- 日期目录[USER_ID]/- 用户 ID(每个人不同)
我想要的是:
flomo attachment/2025-11-03/filename.m4a
解决方案:
修改目录名称:从
flomo picture改为flomo attachment(更准确,因为包含音频、视频等)创建专门的复制方法 lib/flomo/importer.ts:146-197:
private async copyAttachmentsSkipUserIdDir(sourceDir: string, targetDir: string) **
// 第一层:遍历日期目录 (2025-11-03)
// 第二层:跳过用户ID目录 ([USER_ID])
// 第三层:直接复制文件到日期目录下
**
- 更新文件引用的正则表达式 lib/flomo/core.ts:72:
// 支持  和  两种格式
.replace(/!\[([^\]]*)\]\(file\/([^\/]+)\/[^\/]+\/([^)]+)\)/gi,
``)
遇到的问题:
- 第一次写正则时只匹配了
![]()空括号,导致带 alt 文字的附件引用没有被更新 - 通过
([^\]]*)捕获 alt 文字,用$1保留原文
第三步:动态路径配置 - 尊重用户设置
问题:我的 Flomo 主目录设置的是 "10 flomo",但附件路径写死在代码里是 "flomo"。
解决方案:
- 给
FlomoCore构造函数添加flomoTarget参数 lib/flomo/core.ts:14 - 在
importer.ts中从配置读取flomoTarget并传递给FlomoCore - 使用模板字符串动态生成路径:
const attachmentPath = `$**this.flomoTarget**/flomo attachment/`;
收获:学会了在类之间传递配置参数,保持灵活性。
第四步:智能内容更新检测 - 不再重复导入
问题:如果在 Flomo 网页上编辑了某条笔记,同步时会被跳过,无法得到最新内容。
原因分析: 原来的逻辑只检查时间戳是否存在于 syncedMemoIds 中:
// 旧逻辑
if (syncedMemoIds.includes(dateTime)) **
return; // 跳过
**
但如果内容变了,时间戳不变,就检测不到更新。
解决方案 lib/flomo/core.ts:129-163:
改进 memo ID 生成算法:
const memoId = `$**dateTime**_$**contentHash**_$**occurrence**_$**total**`;
更新检测逻辑:
const isAlreadySynced = this.syncedMemoIds.some(syncedId => **
const parts = syncedId.split('_');
const syncedDateTime = parts[0];
const syncedHash = parts[1];
// 时间戳和哈希都要匹配
return syncedDateTime === dateTime &&
syncedHash === Math.abs(contentHash).toString();
**);
// 如果时间戳相同但哈希不同,说明内容更新了
if (existingMemoWithSameTime && differentHash) **
// 删除旧ID,重新导入
this.syncedMemoIds.splice(oldIndex, 1);
**
效果:现在可以检测到笔记的编辑,并自动重新导入最新版本。
第五步:重置同步历史 - 给用户更多控制
需求:路径改变后,需要清空同步记录重新导入所有笔记。
- 添加 "Reset Sync History" 按钮
- 显示同步统计(上次同步时间、已同步备忘录数量)
- 清空前给出明确警告,提醒用户先删除旧文件夹
const confirmed = confirm(
`⚠️ IMPORTANT: Before syncing again, you should:\n` +
`1. Delete the old memos folder: $**flomoTarget**/$**memoTarget**/\n` +
`2. Delete the old attachments folder if path changed\n\n` +
`Otherwise, existing files will be OVERWRITTEN!`
);
设计思考:
- 不自动删除文件,让用户手动确认,避免误操作丢失数据
- 显示统计信息,让用户了解当前状态
第六步:完善文档 - 让别人也能用
开发完成后,我意识到如果要分享给其他人使用,需要完善的文档。
创建的文档:
CHANGELOG.md - 详细的版本更新记录
- 所有新功能的说明
- Bug 修复列表
- 升级指南(Option A / Option B)
- 技术改进说明
更新 README.md - 用户使用指南
- "What's New in Version 2.0" 部分,突出新特性
- 升级指南,提供两种方案供选择
- 安装说明(特别强调 Playwright 依赖)
更新版本号:
manifest.json:1.4.0→2.0.0package.json:1.1.2→2.0.0versions.json: 添加"2.0.0": "1.5.0"条目
发布流程
1. 构建项目
npm run build
生成 main.js (3.8mb,包含 Playwright)
2. 提交代码
git add .
git commit -m "Release version 2.0.0 - Major improvements"
git push origin main
3. 创建 GitHub Release
brew install gh
gh auth login
gh release create v2.0.0 \
main.js manifest.json styles.css \
--title "Version 2.0.0 - Major Feature Release" \
--notes "详细的 Release Notes..."
Release 地址:https://github.com/geekhuashan/flomo-to-obsidian/releases/tag/v2.0.0
技术要点总结
1. 正则表达式的陷阱
最初写的正则:
/!\[\]\(file\/([^\/]+)\/[^\/]+\/([^)]+)\)/gi
只能匹配 ,不能匹配 
改进后:
/!\[([^\]]*)\]\(file\/([^\/]+)\/[^\/]+\/([^)]+)\)/gi
使用 ([^\]]*) 捕获方括号内的任意内容(包括空)。
2. 文件系统操作的异步处理
处理 Flomo 的嵌套目录结构时,需要递归遍历:
private async copyAttachmentsSkipUserIdDir(sourceDir: string, targetDir: string) **
const dateItems = await fs.readdir(sourceDir, ** withFileTypes: true **);
for (const dateItem of dateItems) **
if (!dateItem.isDirectory()) continue;
// 检查目录是否包含文件(递归)
const hasFiles = await this.directoryHasFiles(dateDirPath);
if (!hasFiles) continue;
// 跳过用户ID层,直接复制文件
// ...
**
**
学到的:
- 使用
fs.readdir(..., ** withFileTypes: true **)获取文件类型 - 递归检查空目录,避免创建无用文件夹
- 使用
try-catch处理文件操作异常
3. 增量同步的 ID 设计
ID 格式:$**timestamp**_$**contentHash**_$**occurrence**_$**total**
timestamp: 精确到秒的时间戳contentHash: 标题+正文+附件的哈希值occurrence: 同一时间戳的第 N 条(防止同时创建多条)total: 总序号(最后的保险)
兼容性考虑:
// 支持旧格式
const parts = syncedId.split('_');
if (parts.length >= 2) **
// 新格式:时间_哈希_...
return syncedDateTime === dateTime &&
syncedHash === Math.abs(contentHash).toString();
** else **
// 非常旧的格式:只有时间戳
return syncedId === dateTime;
**
向后兼容让老用户升级时不会丢失同步记录。
4. Playwright 的 headless 模式
// 认证时显示浏览器(需要人工处理验证码)
await playwright.chromium.launch(** headless: false **);
// 导出时隐藏浏览器(自动化任务)
await playwright.chromium.launch(** headless: true **);
分离交互场景和自动化场景,提升用户体验。
遇到的坑
1. 变量作用域问题
// ❌ 错误
} else if (item.isFile()) {
try {
const targetPath = `$**targetDir**${item.name}`;
// ...
} catch (copyError) **
console.warn(`失败: $**targetPath**`); // targetPath 不在作用域内
**
}
// ✅ 正确
} else if (item.isFile()) {
const targetPath = `$**targetDir**${item.name}`;
try **
// ...
** catch (copyError) **
console.warn(`失败: $**targetPath**`); // OK
**
}
2. 正则表达式的贪婪匹配
需要明确 [^\/]+ 来匹配"不是斜杠的字符",而不是用 .+ 贪婪匹配。
3. 文件覆盖的风险
重置同步历史后,如果不删除旧文件就同步,会直接覆盖。所以:
- UI 上给出明确警告
- 不自动删除,让用户手动操作
- 在 README 中反复强调
收获与感悟
技术层面
- TypeScript 的类型系统很有用,帮我在编译阶段发现了很多潜在问题
- 正则表达式需要反复测试,特别是涉及复杂路径时
- 异步操作要注意错误处理和资源清理
- 向后兼容很重要,升级不能让老用户受影响
产品层面
- 用户体验细节决定产品质量(比如静默同步)
- 清晰的文档和详细的提示能避免很多支持问题
- 提供选项比强制行为更友好(升级时的 Option A/B)
开发流程
- 先解决自己的痛点,这是最好的动力来源
- 渐进式开发,一个功能一个功能地实现和测试
- 完善的文档和 GitHub Release 让分享变得容易
- AI 辅助开发让非专业开发者也能实现复杂功能
AI 时代的启示
- 技术不再是唯一门槛 - 想法、需求理解和产品思维同样重要
- 学会提问比学会写代码更关键 - 如何清晰地向 AI 描述需求是新的核心能力
- Fork 文化更有价值 - 站在巨人的肩膀上,用 AI 快速定制
- 分享你的改进 - 每个人的需求都可能启发其他人
适用人群
这个插件适合:
- ✅ 同时使用 Flomo 和 Obsidian 的用户
- ✅ 需要定期同步 Flomo 笔记的用户
- ✅ 希望在 Obsidian 中管理所有笔记的用户
- ✅ 重视笔记系统稳定性的用户(增量同步,不丢失数据)
需要注意:
- ⚠️ 仅支持桌面版 Obsidian(依赖 Playwright)
- ⚠️ 需要安装 Playwright:
npx [email protected] install - ⚠️ 从 1.x 升级需要重置同步历史(见升级指南)
下一步计划
可能的改进方向:
- 支持选择性同步(按标签、日期筛选)
- 支持双向同步(Obsidian → Flomo)
- 优化大量笔记的同步性能
- 支持更多自定义模板
但更重要的是:我会根据自己的实际使用情况持续更新和优化。同时,我真心希望你也能 fork 这个项目,根据你自己的需求去开发和定制。
在 AI 的帮助下,这些改进都不是难事。每个人都可以成为自己工具的开发者。
如果你也在用 Flomo 和 Obsidian,欢迎试试这个插件,更欢迎 fork 后改造成你想要的样子!
相关链接
- GitHub 仓库: https://github.com/geekhuashan/flomo-to-obsidian
- Release 下载: https://github.com/geekhuashan/flomo-to-obsidian/releases/tag/v2.0.0
- 原作者项目: https://github.com/jia6y/flomo-to-obsidian
写于 2025-11-03,一个非专业开发者在 AI 帮助下的开发实践记录。
这不是一个完美的项目,但它解决了我的实际问题。希望它也能帮到你,或者启发你去创造自己的工具。
如果这篇文章对你有帮助,欢迎 Star ⭐️ 或 Fork 🍴