2026.07.21Harness#product#agent

一个本地优先工具的设计记录

记录一个本地优先写作工具的设计过程:为什么本地优先、数据怎么存、同步怎么取舍,以及目前仍未解决的问题。

目录 · Contents

这是一个正在构建的小工具的设计记录——一个本地优先的写作环境,附带一个能在本地语料上工作的 AI 助手。写下来是为了强迫自己把决策讲清楚。

为什么是本地优先

动机很朴素:写下来的东西应该比任何一家公司活得久。云服务提供的便利是真实的,但它的代价是你的语料以别人的存亡为前提

本地优先在这个工具里意味着三条硬约束:

  • 断网时功能完整可用(AI 功能除外,但要有明确降级);
  • 数据是纯文件,离开这个工具依然可读、可搜索、可迁移;
  • 任何「同步」都只是复制,不是迁移——本地副本永远是权威版本。

数据怎么存

内容用 Markdown 纯文件,元信息放在 frontmatter。目录即结构,不引入数据库。唯一允许的索引是一个可以随时重建的缓存:

notes/
├── 2026/
│   ├── 07-28-agent-boundaries.md
│   └── 07-21-local-first-design.md
└── .index/cache.json   # 可随时删除重建

检索用倒排索引,构建一次几百篇笔记在毫秒级,不需要更重的方案:

function buildIndex(docs: Doc[]): Map<string, Set<string>> {
  const index = new Map<string, Set<string>>();
  for (const doc of docs) {
    for (const term of tokenize(doc.body)) {
      if (!index.has(term)) index.set(term, new Set());
      index.get(term)!.add(doc.id);
    }
  }
  return index;
}

工具应该像水:装满什么形状的容器都可以,倒出来还是水。

同步的取舍

多设备同步用「文件级最后写入胜出 + 冲突副本」。这意味着偶尔会产生 note (conflict 2026-07-21).md 这样的文件。我接受这个代价,因为:

  1. 写作场景的冲突本来就极少;
  2. 冲突以可见的副本呈现,比静默合并出错好得多;
  3. 它不需要一个中心服务器,任何文件夹同步工具都能胜任。

AI 助手放在哪一层

AI 助手不碰原始文件,只读写一个 drafts/ 目录。它生成的任何东西,都要经过人显式「采纳」才进入正式笔记。这是上一篇关于行动边界的思考在自家工具上的直接应用。

还没解决的问题

  • 移动端查看体验目前很粗糙;
  • 全文检索对中文分词的处理还很幼稚;
  • 「采纳」这一步的交互还不够轻,用多了会烦。

先这样。工具是给自己用的,能在真实使用里长出来的设计才是活的设计。