OneBook

用树状结构来组织所有笔记

OneBook 把一切都存成区块。笔记、清单、智能体的定义、你发出的提问、模型的回复,都是同一张 blocks 表里的记录,靠 parent_id 串起来,用 fractional position 排序。

Electron 桌面应用,数据落在本机文件里。

  • agent文献综述 agent
    • prompt整理我们关于 RAG 评测的记录
      • completion三类基准,各自的取舍
        • prompt这些在中文语料上哪些会失效?
          • context出处:Lewis et al., 2020
        • noteagent 归档的检索笔记
一个 agent 区块下挂了两次运行,第二次提问旁边还挂着一个上下文区块。图里每一行都是表里的一条记录。

七种区块类型,一张表。

没有按类型分表。七种里只有少数会得到引擎的特殊处理。

没有按类型分表。七种里只有少数会得到引擎的特殊处理。
类型界面名称行为
note笔记Markdown 预览支持 GFM、公式、Mermaid 和代码高亮,编辑用 CodeMirror。
prompt提问你发出的一个问题,映射成对话里的 user。
completion回复模型的答复,就地流式写入,对应 assistant。不能手工新建。
agent智能体区块正文就是系统提示词,它的子区块是这个智能体跑过的记录。
craft工序反复使用的清单、步骤、模板,渲染方式和笔记一样。
script脚本当代码编辑和着色,能读不能跑——执行还没接上。
context上下文把另一个区块挂进来当参考资料,运行时以 XML 注入。

把分支折叠起来,再看某个区块到根的路径

上面那张图的内容都在这里,另外多三行它没画的。按一行,它下面的内容就收进去;选中一行,旁边的面板会顺着 parent_id 一路往上找到根。

  • agent文献综述 agent
    • prompt整理我们关于 RAG 评测的记录
      • completion三类基准,各自的取舍
        • prompt这些在中文语料上哪些会失效?
    • craft下一篇论文的审稿清单
一个区块只有一个父区块,子区块可以有多个,所以这样一条路径就是一条递归查询,不需要跨表 join。

对话是笔记树的一根分支。

每一次追问都把自己的提问区块挂在上一轮回复下面,所以祖先链就是消息历史。拼一次请求用的是一条递归 CTE,不是一份会话日志。

在同一个回复下面问两次,就得到两根分支,两条历史都留在树里,不会互相覆盖。

WITH RECURSIVE strict_path AS (
  SELECT id, parent_id, position, 0 AS depth
  FROM blocks
  WHERE id = ? AND type = 'prompt'
    AND is_deleted = 0

  UNION ALL

  SELECT b.id, b.parent_id, b.position, sp.depth + 1
  FROM blocks b
  JOIN strict_path sp ON b.id = sp.parent_id
  WHERE b.type = 'prompt' AND b.is_deleted = 0
)
SELECT b.*, sp.depth
FROM strict_path sp
JOIN blocks b
  ON (b.parent_id = sp.parent_id
      OR (b.parent_id IS NULL
          AND sp.parent_id IS NULL))
 AND b.position <= sp.position
ORDER BY sp.depth DESC, b.position ASC

提问 → 回复 → 提问 → 回复,落成嵌套的行。 · block.repository.ts

智能体在你已有的笔记里干活。

智能体本身就是一个区块,正文就是系统提示词。它有七个读写区块的工具,可以先搜一遍笔记本,再把找到的东西归到你正在看的那个区块下面。

这些工具跑在主进程里,读写的就是搜索面板用的那个数据库,所以 agent 存下来的东西你马上能翻到。如果这次调用要改某个区块的正文,OneBook 会先把旧内容写成一个快照子区块。

  • getBlock读一个区块
  • getChildBlocks按顺序列子区块
  • getBlockAncestors向上回溯到根
  • searchBlocks和搜索面板读同一份索引,可按 ref_id 收窄
  • createBlock把内容归到某个父区块下
  • updateBlock替换正文或元数据
  • deleteBlock软删除,可恢复

工具就这些。没有一个会走网络。

数据库就是一个文件。

一个项目是一个目录。复制它就是备份,换台机器就带走笔记、智能体和聊天记录。

没有遥测,也不用登录。真要出网的请求只发给你自己配的那家服务商。想确认里面存了什么,用 sqlite3 打开数据库就行。

~/.onebook/settings.json应用偏好
~/.onebook/models.json你配置的模型密钥
~/.onebook/projects.json项目清单
~/.onebook/projects/<name>/db/data.dbSQLite,WAL 模式
~/.onebook/projects/<name>/export/你往外导出的东西

OneBook 往磁盘上写的全部东西。

中文搜索和英文一样。

标题和正文进 FTS5 表,按 MATCH 排序。中文在入索引之前先用 nodejieba 分词,所以不用建 trigram 表,也不用前缀匹配的折中办法;再拿停用词表把索引压小。

搜索面板和 agent 的 searchBlocks 工具读同一份索引,你搜得到的东西它也搜得到。

blocks标题和正文,一个区块一条记录。
block_ftsFTS5,porter 分词器;中文在入索引前先用 nodejieba 切好。
block_links保存时从 [[slug]] 和 ((id)) 里抽出来的边。
两个读的人搜索面板和 agent 的 searchBlocks 查的是同一张表。

一份索引,两个读法。

[[页面名]] 点开就是真实区块。

remark 插件解析 [[slug]] 链接和 ((block-id)) 嵌入,把它们渲染成可以点开的区块卡片。slug 在写入和读取时走同一个规范化函数,所以你写 “My Page”,照样能找到 my-page。

保存时这些引用会被抽进 block_links,分成 block_link 或 block_embed 两种边,反链查询于是就是一个 SELECT。展示反链的面板还没做。

[[My Page]]my-page
[[CRO 是什么?]]cro-是什么
[[ RAG Notes ]]rag-notes
((4f2c9a1e-…))一条嵌入边

写入路径和读取路径共用一个规范化函数。

人们拿它装什么

树就是全部界面,所以下面这些做法本质相同:把一个区块放到某个位置,再往里放区块。

跨很多材料的调研
一份材料一个父区块,摘录作为子区块,材料之间用 [[slug]] 互链。
比较不同回答
同一个提问跑三遍,或者交给不同 agent,让回复并排留着。
长文和报告
用区块搭大纲,收起暂时不写的部分,整个章节连子区块一起拖走。
agent 工作台
把 agent 写成区块,附上它该看的笔记,让它把结论发回树里。
会议记录
先平着记,再把待办重新挂到负责它的项目下面。
喂给 AI 的语料
内容本来就被切成区块大小并建好索引,检索是一条查询,不是一次入库。

模型自己带。

模型访问走 Vercel AI SDK,注册了 21 个 provider adapter。

  • OpenAI
  • Anthropic
  • Google
  • xAI
  • Mistral
  • Groq
  • DeepSeek
  • MoonshotAI
  • Alibaba
  • ByteDance
  • Cohere
  • Perplexity
  • Cerebras
  • Fireworks
  • Together
  • DeepInfra
  • Baseten
  • Hugging Face
  • OpenRouter
  • Ollama
  • OpenAI-compatible

密钥存在 ~/.onebook/models.json,只在主进程里读。

还没做的。

大致按离得近的排。

  • 反链面板和链接总览。边已经在 block_links 里了。
  • Markdown 或 JSON 的导入导出,一个区块对应一个文件,好让已有的笔记搬进来。
  • 关键词搜索旁边加向量检索。sqlite-vec 在 optionalDependencies 里,没有代码调用它。
  • 在沙箱里运行 script 区块。
  • 把一次运行发布成作品,以及跨设备同步——两者都建立在按项目分库的文件上。
  • 自动更新、打包流水线、CI。

把树打开。

文档从一个项目文件夹和一个笔记区块开始,后面所有东西都挂在它下面。