KiteNote: 结合MicrosoftToDo和Obsidian优势的日常笔记软件

用 Electron + TipTap 写一个兼容 Obsidian 的 Markdown 工作记录工具:无损往返、复制到钉钉不丢图、同步盘不丢稿

项目开源:github.com/qianx77/Kit... (AGPL-3.0)

先说做了个什么

我是做算法的,任务记在 To Do,实验记录写在 Obsidian。用久了有三个痛点:表格不好做 (增删一列都要手改 |)、笔记复制到钉钉图片全丢 、没有周报(任务和记录分在两个软件,周五要两边来回翻)。

于是写了 KiteNote:To Do 式的日 / 周任务,每个任务一键生成一篇所见即所得的 Markdown 笔记,周五一键复制周报。数据就是文件夹里的普通 .md,可以直接用 Obsidian 打开。

技术栈:Electron + React + TypeScript + TipTap(ProseMirror)+ markdown-it。

下面聊聊做的过程中最费劲的四个问题。

一、Markdown 无损往返:打开、编辑、保存,不能把 Obsidian 的写法改坏

所见即所得编辑器的内部是一棵 ProseMirror 文档树,保存时要再序列化回 Markdown。只要「解析 → 序列化」不是恒等的,用户打开一篇笔记、改一个字、保存,文件里别处的写法就可能被悄悄改掉。对一个「直接打开你现有 Obsidian 库」的工具来说,这是不能接受的。

Obsidian 有不少标准 Markdown 之外的语法,每一种都要单独处理:

语法 例子 处理方式
双链 `[[实验/exp-001 基线]]`
嵌入 ![[loss.png]] 解析为图片节点,用 data-embed 记住原始写法,保存时不改成 ![](...)
公式 $\alpha$、$$...$$ 自定义 inline / block 规则,KaTeX 渲染
高亮 ==重点== 自定义 mark
Callout > [!note] 标题 仍按普通引用块存,只加显示样式(ProseMirror Decoration),不改动文档内容
Tasks 插件 - [ ] 任务 ⏫ ✅ 2026-10-27 任务行本身是普通 Markdown,emoji 原样保留

做法是给 markdown-it 加自定义规则,同时为每种节点写对应的序列化逻辑。验证方式也很朴素:拿自己真实库里的 60 多篇笔记做往返测试,「解析 → 序列化」后逐字比对。

完全恒等做不到。有些写法渲染效果相同,序列化时只能输出一种形式,比如多余的 \_ 转义、|a|b| 和 | a | b |、列表缩进。我的选择是不偷偷改:打开时预先跑一遍往返,如果保存后文本会变,就在笔记顶部提示「保存时会规范化格式」,需要保留原样的可以切到源码模式编辑。

二、复制到钉钉不丢图

Obsidian 的图片是本地相对路径,复制到钉钉文档,钉钉拿不到本机文件,图片就丢了。

KiteNote 里图片通过自定义协议 vault:// 加载。复制时,在主进程把 HTML 里所有 vault:// 图片读出来,换成 base64 的 data: URL,再写进剪贴板:

ts 复制代码
for (const src of new Set(srcs)) {
  const rel = relFromVaultUrl(src)
  const buf = await vault.readBinary(rel)
  const mime = MIME[path.extname(rel).toLowerCase()] ?? 'image/png'
  html = html.split(src).join(`data:${mime};base64,${buf.toString('base64')}`)
}
clipboard.write({ html: fullHtml, text })

两个细节:

  • 只选中一张图时,额外写一份位图。 有些应用只认剪贴板里的图片格式、不认 HTML,这样粘到哪里都能得到图片。
  • 在 HTML 里夹带原始 Markdown。 富文本一来一回会丢信息,双链会变成普通文字、公式会变成渲染结果。所以复制时在 HTML 末尾加一段注释,钉钉等外部应用会忽略它;粘贴回 KiteNote 时检测到这段注释,就直接用原始 Markdown,双链、公式、待办都能原样还原:
ts 复制代码
const MARK_RE = /<!--kitenote-md:([A-Za-z0-9+/=]+)-->/

export function withMarkdown(html: string, md: string): string {
  return `${html}<!--kitenote-md:${toBase64(md)}-->`
}

三、表格:所见即所得,但存下来还是 Markdown

表格是花时间最多的部分。鼠标悬停时,列上方、行左侧出现手柄,点一下插入、删除、对齐;拖动边线调列宽;单元格里也能放图片。

麻烦在于 Markdown 表格存不了列宽,也不支持合并单元格。我的处理是:

  • 普通表格存成标准 Markdown 表格;
  • 调过列宽或合并过单元格的,存成 HTML <table>(Obsidian 能正常显示);
  • 右键「恢复平均列宽」,又变回 Markdown 表格。

宁可显式换一种格式,也不丢用户调好的布局。

四、两台电脑用同步盘:保存前先比对,绝不覆盖

库就是普通文件夹,放进百度网盘、坚果云、OneDrive 都行。我自己是公司一台、家里一台交替写,这就带来一个问题:A 电脑正在编辑某篇笔记,同步盘这时把 B 电脑改过的版本拉了下来。如果编辑器按原来的逻辑自动保存,B 的改动就被 A 覆盖了,同步盘再把 A 的版本传回去,B 白天写的内容就没了。

解决办法:每次保存前,先比对磁盘上的内容和打开时读到的版本。

ts 复制代码
async writeChecked(rel: string, content: string, expected: string | null, device = '') {
  let current: string | null
  try { current = await this.read(rel) } catch { current = null }
  if (current === content) return null
  if (current === null || current === expected) {
    await this.write(rel, content)          // 没被外部改过:正常保存
    return null
  }
  const copy = await this.conflictPath(rel, device)
  await this.write(copy, content, true)     // 被改过:原文件不动,本机内容另存
  return copy                               // 界面据此提示用户合并
}
  • 磁盘内容和打开时一样:正常保存;
  • 被外部改过:原文件保留同步来的版本,本机改动另存为 xxx (冲突副本 电脑名 时间).md,编辑器重新加载并弹出提示;
  • 文件被外部删了:照常写回,正在编辑的内容不丢。

另外两个配套细节:

  • 写入用「临时文件 + 改名」,避免同步盘读到写了一半的文件;
  • 文件监听忽略自己 1.5 秒内的写入,否则自己保存一次,就会被当成「外部修改」又重新加载一次。

最后

KiteNote 目前只有 Windows 版,界面只有中文,代码在 GitHub 上:

如果你也在用 Obsidian,欢迎拿自己的库试试,哪种写法被改坏了,麻烦在 Issues 里贴个例子。觉得有用的话点个 Star,谢谢。

相关推荐
用户466969256971 小时前
AI编码账单砍60%:这个84K星的小工具,一行命令装好
github
简创AIGC陶先生1 小时前
【直播切片工作台】第22章:素材下载与 URL 重写
github
KevinAshen671 小时前
用 Go 修复旧下载工具:从接口 403 到凭证截断的完整复盘
go·github
miofly2 小时前
GitHub 今日推荐|Edge0:让大号 MoE 模型在一台 Mac 上流式跑起来
python·开源·github
shandianchengzi3 小时前
【记录】LLM|ChatGPT连接Github并管理
chatgpt·github
徐小夕3 小时前
JitOffice:把办公、AI 与审批装进同一个平台
前端·vue.js·github
徐小夕4 小时前
万字分享:为什么我们要自研Word解析引擎?JitWord 解析引擎 4.0 深度复盘
前端·vue.js·github
miofly5 小时前
GitHub 日榜趋势速报 | 2026-10-30
开源·github
wflynn14 小时前
GitHub 日榜趋势速报 | 2026-10-28
开源·github