NoteGen 里一条记录如何变成 Markdown

NoteGen 是一个开源、跨平台的 AI Markdown 笔记应用,仓库在 codexu/note-gen。这篇文章面向想理解或改造记录系统的开发者,讨论问题:截图、图片、网页、文件和录音,为什么先以记录对象保存,直到写入编辑器或整理成文时才转换成 Markdown。

问题:记录不是一种数据

很多笔记工具会把「收藏」理解成文本。NoteGen 的记录入口更杂:文字、截图、图片、网页、文件和录音都可能变成一篇 Markdown 笔记的材料。

如果在采集时就统一成 Markdown,短期看简单,长期会丢掉上下文。截图要保留本地图片路径,图片可能有 OCR 文本和 AI 描述,网页既有 URL 也有正文摘要,录音既有音频文件也有转写内容。本地优先应用还要考虑资源清理和同步。源码里的重点,是先保存「可追溯的记录」,再在消费边界转换成 Markdown。

背景:先接住,再进入写作

记录的核心模型在 src/db/marks.tsMark 不是富文本节点,而是一条结构化记录:type 标记来源类型,content 保存可检索或可生成的正文,desc 保存摘要,url 保存外部链接、本地文件路径或资源文件名。类型覆盖 scantextimagelinkfilerecordingtodo

这层模型把采集和写作拆开了。采集入口只负责把来源信息落库,不决定最终标题、段落和引用结构。写作侧则按场景消费同一条记录:拖拽插入需要简短 Markdown,AI 整理需要完整上下文,资源清理需要知道背后是否还有本地文件。

源码路径:从入口到 Markdown

文字入口在 src/app/core/main/mark/control-text.tsx,清洗输入后写成 type: 'text'。网页入口在 control-link.tsx,抓取页面 HTML,解析标题、meta 描述和正文,再分别写入 desccontenturl。文件入口在 control-file.tsx,文本类文件直接读取内容,PDF 通过文本层或 OCR 得到内容,最后仍以 type: 'file' 写入同一张表。

图片和截图入口更能说明这个模型的价值。control-scan.tsx 把截图存到 screenshot 子目录;control-image.tsx 把图片缓存到 image 子目录。它们可以走 VLM 描述,也可以走 OCR,再可选地用 AI 生成描述。落库时,content 承载识别文字,desc 承载描述,url 承载资源文件名或上传后的远程地址。

录音入口在 control-recording.tsx。它先保存音频文件,再调用语音转写,成功时把转写文本放进 content,用前一段文本生成 desc,并把音频路径写入 url。录音没有被简化成一段文字,原始音频仍能被打开或引用。

真正的 Markdown 转换集中在 src/lib/mark-to-markdown.ts。它按 type 做边界转换:文本返回 content,图片和截图生成图片语法,网页生成链接,文件生成文件链接并附带内容,录音优先输出转写文本,再附带音频链接。数据库里的记录保持结构化,只有在插入编辑器这样的消费动作发生时才变成 Markdown。

消费侧有两条典型路径。第一条是拖拽插入。src/app/core/main/mark/mark-item.tsx 调用 markToMarkdown(mark),把结果放进 dataTransfertext/plain,同时也把完整记录对象放进 application/json。编辑器侧的 src/app/core/main/editor/markdown/tiptap-editor.tsx 在 drop 时读取 JSON,再调用同一个转换函数,并写进 Tiptap。

第二条是 AI 整理。src/app/core/main/mark/organize-notes.tsx 会按时间范围筛选记录,再按截图、文本、图片、链接、文件分组,拼出包含 OCR 文本、图片描述、链接正文、文件内容和格式要求的请求。它不是把记录拼成一长串 Markdown,而是把结构化上下文交给模型生成新文件。

关键设计:转换放在边界上

第一,marks 表承接输入事实,不承接最终排版。initMarksDb() 的字段保存来源类型、文本内容、展示描述、资源位置和删除状态。insertMark() 还会写入活动事件,让记录既能进入写作流,也能进入后续活动统计。

第二,contentdescurl 有明确分工。content 用于检索、整理和生成;desc 用于列表展示;url 用于回到原始资源。同一条记录因此可以服务多个场景,而不是只作为编辑器里的一段静态 Markdown。

第三,本地资源的生命周期由记录类型决定。getMarkLocalAssetPath()scan 映射到 screenshot,把 image 映射到 image,把 recording 映射到录音路径;远程 HTTP 资源不会被当成本地文件删除。delMarkForever()clearTrash() 在真正删除记录前会查出类型和 URL,再清理本地资源。Markdown 只是呈现层,文件生命周期必须回到结构化数据里处理。

第四,同步没有把记录混进普通文章文件。src/stores/mark.ts 会把所有记录序列化到 .data/marks.json,再按不同后端上传或下载。文章文件和记录集合分开同步,降低格式演进成本:以后给 Mark 增加字段,影响的是记录数据,而不是已生成的 Markdown 正文。

可复用经验

这个实现给类似工具一个经验:不要急着把所有输入统一成最终格式。最终格式适合展示和编辑,但不一定适合保存来源事实。结构化记录可保留更多可恢复的信息,也更容易接入 OCR、VLM、语音转写和 AI 整理。

另一个经验是把转换函数做小、做集中。markToMarkdown() 不承担采集、同步或模型调用,只负责把一条记录变成可插入编辑器的 Markdown。新增记录类型时,入口负责采集,数据库负责保存,资源工具负责生命周期,转换函数负责写作边界。

工程启发

NoteGen 这条路径的核心不是「把收藏变成 Markdown」这么简单,而是把采集、存储、资源、同步和写作边界分开。记录系统保存的是可追溯的素材,Markdown 是其中一个消费结果。工程上的启发是:当输入来源复杂、未来用途不确定时,先建模事实,再在明确边界上转换格式,比一开始追求统一文本更稳。

相关推荐
澄旭1 小时前
拆解一个成熟 Skill,看懂 Skill 到底该怎么写
人工智能
王木风1 小时前
拆解一个 LLM 工程化项目:16 个 Service + Agent 对话循环怎么协同跑流水线
人工智能·agent
沪漂阿龙1 小时前
《LangChain 系列》Human-in-the-loop:什么时候必须让人工介入?
人工智能·架构·langchain
冬哥聊AI1 小时前
Loop Engineering 来了:从写 Prompt 到设计 Loop,AI 编程的第四次范式跃迁
人工智能
柒星栈1 小时前
Codex 不只是更强的代码助手,它开始像代理一样推进开发任务了
人工智能
o_insist2 小时前
04-从零手写 ReAct 循环:Agent 的心跳是怎么转起来的
人工智能·agent
DayByDay2 小时前
从“单专家”到“多专家辩论”:多大脑对话实现复盘
人工智能
狗哥哥2 小时前
知乎回答二次创作转AI 漫画/视频思路分享
人工智能
极速蜗牛2 小时前
我在 Taro 小程序项目里实践的 API First + AI 编程方式
前端·人工智能·后端