28 KiB
Note
本文档由 WeHub 基于上游 README 翻译整理,属于社区翻译,非官方中文文档。
English · 原始项目 · 上游 README
原作者、版权与许可证归属以原始项目及本仓库 LICENSE 文件为准。
Files.md
一处私密、安静的思考空间。一款只为你 .md 文件而生的简单应用。
English · 简体中文
你的整段人生,都可以放在这里:
- 📌 笔记
- 📝 文档、项目
- 💚 日记、习惯
- ✅ 清单、任务
全部以纯 .md 文件保存,本地优先,对 LLM 友好。隐私无虞——任何数据都不会上传到服务器。
让你的数据,以纯粹的本地文件形式属于你。 让打开这些文件的软件,也真正属于你。 用文件和你自己的大脑,去培育知识。 借助 LLM,去打磨文件周围的软件。 属于自己的纯文件与软件,才能穿越时代。
线上体验:app.files.md(Beta)。主站:files.md。
你可以把它用作第二大脑、卡片盒笔记(Zettelkasten)、笔记、日记、任务、清单等等。
这个项目我已经做了 5 年。 欢迎 在 GitHub 上支持作者 💚。
随手记下想法 · 如何深入思考 · 第二大脑? · 日记、任务、清单
又一个笔记应用?
也算是。但这一次:
- 只保留必要功能,约束孕育创造力
- 无需安装,一个浏览器就够了
- 离线可用
- 本地优先,文件不会离开你的设备
- 自由开源
- 代码极其简洁。一个人或一个 LLM 能把整个项目装进脑子里
- 换句话说,这个项目具有低认知负荷
- 代码库为 LLM 留好了扩展空间,随你按需添砖加瓦
- 便携,没有任何构建系统,直接打开
web/index.html即可 - 同步开箱即用,但完全可选
- 服务器就是一个二进制文件(也可以直接用 iCloud / Dropbox / Google Drive 同步)
- Telegram 机器人,方便你在路上随时访问文件
如何使用
- 打开 app.files.md(推荐 Chrome)
- 点击地址栏右侧的 "Install files.md":
- 打开一个本地文件夹,让改动持久化保存
- 偶尔按
Cmd+Shift+R强制刷新一下,拿到最新更新
如何同步
| 方案 | 文件存放在哪儿 | 多设备同步 | 是否需要服务器 | 适合的人 |
|---|---|---|---|---|
本地优先,app.files.md 完全不发送任何数据 |
你设备上的某个文件夹 | 否 | 不需要 | 追求极致隐私,数据完全留在本地 |
| 云盘文件夹同步(iCloud / Dropbox / Google Drive) | 你现有的云盘文件夹 | 是 | 不需要(由云服务商负责) | 不想自己跑服务器,也想多设备同步 |
| 自托管同步服务器 | 你自己(或局域网内)的服务器 | 是 | 一个 Go 二进制 | 想在自家或公司网络里多设备同步 |
| 托管同步服务器 | 我们托管的服务器 | 是 | api.files.md |
想立刻上手,不折腾任何配置 |
随手记下想法
你可以打开聊天框,随手把脑子里冒出来的想法记下来。
打开聊天框,发一条消息:
挑一个去处(也可以稍后再决定):
这条流程足够让你顺手收下笔记、任务、日记和清单。
在 Telegram 机器人里保存
打开聊天,输入点东西,按 Enter:
就这样,你的笔记已经保存到一个 .md 文件里了。
机器人的完整功能:
别担心——默认情况下它比看上去简单得多。
Telegram 机器人入口。其他即时通讯工具也会陆续支持。
如何深入思考
让想法相连,让它们彼此叠加,并真正想透。
- 我用 app.files.md 来沉淀关于大脑和软件开发的认识
- 我把新笔记放进
brain或dev文件夹,一条笔记只承载一个想法 - 在 Web 应用里,把彼此相关的笔记连接起来(按
[即可) - 所有东西最终都连成一片,正如我们的大脑
- 我会花时间在这些笔记之间穿行、反复琢磨
- 久而久之,有些
brain里的笔记和dev里的笔记开始看起来意外地相关 - 跨领域的这种连接,会激发出洞察
- 我据此写了一篇文章:软件开发中的认知负荷
这个过程帮助我:
- 深入思考(在 AI 时代尤其重要)
- 系统性地思考,看见更完整的图景
- 写出更有洞见的文字
要做到以上几点,你必须用自己的脑子去想,而不是依赖什么花哨模板或者 AI 工作流。
- 一开始什么结构都别建,0 个文件夹
- 一条笔记只承载一个想法
- 每条笔记要在脱离上下文时也能被读懂
- 学到新东西就立刻用上,别留给"未来的自己"
- 把相关的笔记彼此链接起来
- 常回去翻翻,再想想
我和我的朋友用这套朴素的方法已经五年了,效果一直很好。
第二大脑?
我想引用 I Deleted My Second Brain 里的一段话:
Obsidian 是一款出色的软件,我深爱它。但任何东西失去节制都会变成陷阱。嵌套文件夹里的 Markdown 文件、追踪你生产力的插件、暗示某种全知感的关系图谱——看着自己的笔记编织成星座般的网络,会让人产生一种掌控的幻觉。但星座只是投影。它们讲故事,却并不等于理解。
刚开始使用知识管理工具时,我以为自己是在解决"遗忘"这件事。后来,我以为自己解决的是"整合"。
最终,我意识到我创造了一个新问题:拖延。系统越是壮大,我就越是把真正的思考工作推给某个未来的自己——那个会整理、贴标签、提炼、淘金的自己。
而那个自己从未到来。
第二大脑是一个迷人的概念。 那些进阶模板、插件、AI 工作流…… 让人忍不住想把整个互联网的智慧都打包收下。 这个整洁的系统看起来真美。每多一条笔记,多巴胺就涌出来一些。 第二大脑越来越好。
可是,第一大脑从来没有真正变得更聪明。 这才是问题——在 AI 时代,你的第一大脑前所未有地宝贵。
用你的脑子去把笔记想透。 工具并不重要,思考才是。
新增一条笔记之前,先试着回答这两个问题:
- 这则新知识能不能磨利我的判断、拓宽我的分类框架?
- 拥有这份知识后,我能用不同的视角看世界吗?
笔记反而会阻碍体验
- 一边读一边记,很容易让我们误以为自己理解了文本
- 我们以为自己理解了,但其实只是知道
- 而当"知道"变得足够多,我们甚至会觉得自己已经"做过了"(或者起码尝试过了)
最糟糕的是,我们因此不再让新的体验发生——因为我们"已经知道了"。这就是一道知识的高墙。生活给了我们去亲身经历的机会,我们却拒绝,理由是"我已经知道了"。
通过阅读和做笔记自我疗愈?🧘
在情绪层面受到的伤害,必须在情绪层面治愈。
不是靠脑力劳动和做笔记。 没有行动的阅读,是娱乐,是一种拖延的形式。 再多自助类书籍也治不好情感的伤口。 真正有帮助的,是心理治疗、改写练习与椅子工作。是冥想。 疗愈来自感受。
什么时候应该做笔记
如果你的目标是:
- 对某件事建立更深入、更有结构的理解
- 做研究
- 写一篇文章或一本书
那么记笔记完全合适。
日记
对什么事情感觉良好?发一条消息。
然后点 "To Journal"。
或者直接在消息末尾加上 jj 或 жж。
你的记录会被妥帖地存进 journal/YYYY.MM Month.md。
任务 ✅
你正心流满满地工作着。 同事突然让你发一份报告。 把这件事一直挂在脑子里很耗能。 丢一条消息,继续保持心流。
只添加小而可执行的事项。 不要写"规划一次假期"那种。 写下"反正要做的事情"的第一小步即可。 写你愿意去做的,不是你只是想象会去做但还没动力的。 你的任务列表不该带来负罪感。
要稍后再做的事情,按 "To Later"。
清单 🛒
朋友推荐了一本书给你。 家里的黄油和面包没了。 这些事一直挂在脑子里,会让人心累。
丢进聊天框,再转到对应的清单里。
文件结构
你不必费心想结构,它是预定义的。 当然,你也完全可以按自己的喜好来。
- 聊天:
Chat.md - 笔记:
brain/Note.md、<category>/*.md - 清单:
Read.md、Watch.md、Shop.md、MyChecklist_.md - 日记:
journal/2024.08 August.md - 任务:
Later.md - 习惯:
habits/Ate consciously.md、habits/*.md - 图片:
media/*(png、jpg、webp、gif) - 归档:
archive/*.md - 配置:
config.json
这份结构也可以在 files.md/AGENTS.md 取到。
把它作为 AGENTS.md 放进你的知识库——你的文件就成了 AI 可以参与的鲜活个人空间。
快捷键
| 快捷键 | 操作 |
|---|---|
[ |
插入到某个文件的链接 |
Cmd+K / Ctrl+K |
打开文件搜索框 |
Cmd+N / Ctrl+N |
新建文件 |
Cmd+M / Ctrl+M |
移动文件 |
Cmd+D / Ctrl+D |
删除文件 |
Cmd+Enter / Ctrl+Enter |
打开聊天 |
Cmd+Shift+Enter / Ctrl+Shift+Enter |
切换聊天弹窗 |
Cmd+[ / Ctrl+[ |
跳到上一个文件 |
Cmd+] / Ctrl+] |
跳到下一个文件 |
Cmd+~ / Ctrl+~ |
切换侧边栏 |
Cmd+B / Ctrl+B |
切换粗体 |
Cmd+I / Ctrl+I |
切换斜体 |
Cmd+Y / Ctrl+Y |
插入复选框 |
Cmd/Ctrl + Click |
复制纯文本 / 打开链接 |
Ctrl+Cmd+Space |
插入 emoji(macOS) |
一些好用的脚本
所有脚本都在 cmd 目录下,请在你的笔记文件夹里运行。请先安装 Go。
把 Whoop 数据写进日记
go run /abs/path/to/files.md/cmd/whoop/whoop.go
把 wikilinks 转成 Markdown 链接
把 [[wikilinks]] 转成标准的 [Name](/path.md)(支持 --dry-run):
go run /abs/path/to/files.md/cmd/tomdlinks/tomdlinks.go .
插入反向链接
在被引用的文件里加上回链(支持 --dry-run):
go run /abs/path/to/files.md/cmd/backlink/backlink.go
调整日记时间戳
把日记文件中的时间戳整体平移 N 小时(换时区后很实用):
go run /abs/path/to/files.md/cmd/shifttime/shifttime.go
文档
在自己的服务器上部署 Telegram 机器人 同步流程 集成测试
仓库结构
web—— Web 应用(PWA),入口是index.htmlweb/lib—— 前端依赖库cmd/server—— 服务端入口cmd/*/—— 处理.md文件的脚本server/bot.go—— 机器人server/sync/—— 同步 API 服务端vendor—— 后端依赖tests—— 端到端测试,会同时覆盖 Web 应用和服务端
如何贡献
- 初级开发者也应能读懂代码
- 理想情况下,每个 PR 都应该删减或简化代码,而不是新增
- 代码越少,我们就越灵活
- 所有依赖都是我们自己的代码与责任,能不引就不引
- 代码应当自洽,所以
vendor和web/lib都在仓库里 - 我们真的需要这个功能吗?它能帮我们做"真正的事",还是只为了一时的多巴胺?
更完整的原则请参考 这份指南。
后端规范
- 我们写测试
- 我们不用
get*前缀命名方法 - 不要 panic,错误本身就是业务逻辑的一部分
- 如果要忽略某个 error,必须留下解释 WHY 的注释
- 我们一律包装 error,并补上方法层面的上下文
- 客户端代码里不使用迭代器
- 优先使用真实实现或至少 fake,而不是 mock / stub
- import 只在与其他 import 冲突时才重命名
- 为了可移植性,所有数据都存在纯
.md文件里
前端规范
- 如果你就地修改了第三方库,请用
PATCHED关键字标注 - 要是有一天我们能用自己的小实现替换掉
CodeMirror,那就太棒了 - 没有构建系统,十年后我们打开
/web/index.html,它仍然要能直接跑 - 别忘了:在"检查锁"和"获取锁"之间出现的
await会引入竞态 - 别写脆弱的 e2e 测试。先是带来负面情绪,然后我们就再也不跑测试了
- 大多数 bug 都源于异步流程中途被打断时的竞态条件
术语表
filename—— 带扩展名的文件名,例如 "note.md"(用它作为 ID)header—— 去掉扩展名并首字母大写后的文件名,例如 "Note"body—— 文件内容dir—— 用来按类别存放笔记的目录,例如 "happiness"userID—— 即 chatID。大多数场景下我们就直接用 chatID(私聊机器人)- 文件的
ctime—— 数据块或元数据变更时间:所有权、位置、文件类型、权限变更都会触发。父目录改名不会触发,移动文件会触发,重命名会触发。我们需要它来追踪文件的位置变化,比如什么时候被移到归档、追踪任务的"愤怒度"等 - 文件的
mtime—— 文件内容最近一次被修改的时间。和ctime不同,所有权、权限、改名都不会影响它。我们用它来做同步 - 目录的
ctime—— 增删文件或子目录会触发(类似mtime,再加上 inode 的变化,比如改名)
每个文件都可以通过文件名与所在目录唯一定位。我们只支持一层嵌套。
性能
项目跑起来飞快 :) 如果你怕用文件或互斥锁会带来性能开销,看一眼这张对比:
Mutex 加 / 解锁 = 25 ns
从 SSD 随机读 4K = 150,000 ns
1 ms = 1,000,000 ns
ADR(架构决策记录)
06.05.2026从 Today.md 切回 Chat.md。用户访谈发现"chat"这个概念用户更容易理解。而且 "open chat" 这句话在机器人和网页应用里语义都一致。02.05.2026hide-token 现在在每次变更时同步运行;之前的 100ms 防抖会让链接和格式化文本在逐字删除时出现抖动。06.05.2026把 Inbox.md 和 Today.md 合并成了 Today.md。"Inbox"这个名字太抽象、太"生产力"和 GTD 味道。我想要的是从容和简洁。Today 更像"我活在聊天里的那一页"。23.04.2026把 API_HOST、APP_HOST 改成了 API_URL、APP_URL。不同环境下,配置里能携带 schema 等更多信息总归更好。22.04.2026机器人里的 Inbox 条目改用稳定的内容哈希识别(对去掉- [ ]/- [x]标记后的块取fs.Hash),而不是位置序号。这样即便中间有条目被新增/删除/完成,按钮也仍指向正确的那一行。06.05.2026同时存在 "to inbox" 和 "to chat" 两个按钮,心智上挺累。也很难轻松地把任务往 chat 里随手一丢——因为它会先落到 inbox,还要多点一次。这一次多余的点击,正是让"添加新任务"变得令人厌烦的原因。我放弃了两条不同的流程,现在一切都先进 inbox,每一项都是 Markdown 复选框。额外的好处:PWA 默认展示 chat 里的任务,特别顺手。也许 "inbox" 这个词心智负担太重,"chat" 听起来更轻松。再观察一下。11.04.2026虽然我希望以纯 Markdown 链接形式存储链接,但视觉上我希望它们能像极简的[link]一样工作。所以当光标停在该行时,我会隐藏(...)那部分。仅对指向 md 文件的链接生效。11.04.2026又把标准 Markdown 链接拉回来了。我希望这个知识库跨平台可用,它必须在 GitHub 上也能正常显示。05.04.2026试过把web/*移到根目录以求简洁,结果发现是个坏主意——必须有一个显式的目录,作为服务器上的公开 DOCROOT。19.04.2026改成了[link]形式的链接。[link](full%20path)太重也太突兀,我们也不想处理路径变更。21.09.2025移除了 WASM。我当时遇到一个 bug:从 Inbox.txt 删除一条消息后,按"move to file"没有把它写到目标文件里。我没复现出来,但发现了一大堆复杂度:JS -> Go(writeFile)-> Go 等待来自 JS 的 promise -> 中间的 JS Golang 运行时 -> JS(writeFile)-> Go(promise 返回)-> 把结果发回 JS。而且这一切还得跑在独立 goroutine 里,因为 WASM 和 JS 共用一个线程。再加上 Go 的 WASM 仍是实验性的。组件太多、不确定性太大。当时我不愿意用 JS 重写已经能用的逻辑,于是用了 WASM 撑了一段时间。现在该用 JS 把它重新实现,把这一整摞复杂度都甩掉。另外inbox.wasm大约 8MB,我希望整个应用尽可能小。11.07.2025决定先用 OPFS 作为文件系统的初始驱动。浏览器兼容性更好,对用户更省心。应用默认以 OPFS 启动;用户需要时可以打开一个本地目录,切到 Local FileSystem API。DirHandle 会被保存到 IndexedDB,下次复用。08.07.2025根目录现在是/而不是空字符串。Web 应用里所有文件按路径唯一标识,不再用dir + filename,并限制为一层嵌套。11.04.2026Dropbox 会改动新创建文件的某些元数据,因此ctime会被改写。我考虑过把同步切到mtime,但这样就检测不出重命名(虽然我们另有机制检测重命名),而mtime整体上更可靠:权限/所有权变更也不会触发同步。最终切换到了mtime。mtime用于内容级同步,ctime用于追加式同步日志(重命名 / 删除)。另外mtime可以从.git/archive里恢复,ctime不行。30.06.2025决定把所有流程,包括待办列表,都迁移到 Chat.md。补充:这种流程不太方便处理多行任务,可能需要同时支持文件和索引。两种实现思路:参数统一编码,复用同一个命令处理器加 if 分支;或者用不同的命令处理器分别处理 chat / 文件。我选了后者。补充:如果走分流,"move to" 按钮的配置会更复杂。补充:也许在"move to file"时把文件移回 Chat.md,复用现有流程?补充:目前看来还不错。我们的 chat.md 充当一个仅追加日志。额外的好处是:即便某些流程(比如安排时间 / 移动)没走完,内容也保留在日志里,下次可以继续。06.05.2026所有新进消息现在默认进 Chat.md。在此之前它们会被移到/chat并变成任务,这对简单的 to-do 列表很合适,但对其他用例不够灵活。我意识到开会时,我真正需要的就是一个简单的输入框,把脑子里乱七八糟的东西先丢进去,不必立即决定怎么处理,回头再整理就好。它们可能是任务、可能是日记、也可能就是文件。同时保持一条简单到无需理解的默认流程,对用户更友好——所有消息都丢进同一个文件,仅此而已。27.06.2025Chat 现在默认是"一个大文件"模式,也就是把所有消息都堆进同一个文件,不做更多事。还是那句话:从最简单的流程开始,不要一上来就把用户淹没。补充:如果选了完整模式,我们就得提前建好目录,"to habits"、"to read/shop" 之类才能工作。用户用不上的话,把目录删了我们就不再自动重建(不像"按需创建"模式那样)。所以"按需创建"并不适合所有场景。26.06.2025之前是提前把所有目录都建好,现在改成按需创建。这样不会一上来就把用户的知识库塞得乱七八糟。24.06.2025同步时改用微秒级别追踪文件变化。连续创建文件之间的间隔通常在 1000μs–5000μs,足够区分。没用纳秒是因为 JS 处理 int64 精度有问题。补充:Linux 用的是内核缓存时间,按CONFIG_HZ间隔更新(grep CONFIG_HZ /boot/config-$(uname -r)),我这里是 1000(1ms)。实际操作之间的间隔通常远大于 1ms:用户交互、网络延迟、磁盘 I/O 都会拉长间隔。只有在原生紧凑循环里更新文件才可能踩坑。16.06.2025我觉得是时候让知识库真正跨平台了——禁掉文件名里的:?<>*等字符。这些字符在某些环境(Windows、PWA)里本来就不允许。15.09.2025我想在浏览器里有类机器人的能力。又不想把已经被良好测试过的代码用 TypeScript 重写一遍,所以用了 wasm。它确实跑得挺好。12.06.2025我们把 Telegram 机器人当作通往知识库的"无干扰、只写"入口。唯一的问题是它在欧美没那么流行。后来想到:把窗口缩小后,app.files.md 也可以直接呈现成一个聊天界面!这将是移动端的默认形态。04.06.2025引入了用于同步的追加式日志。无状态同步实现起来很费劲——每次请求都要把所有文件发过去。既然服务器端只处理重命名,那我们只追踪重命名就好。04.06.2025仅做内容同步(不含重命名 / 删除)时,服务端不存任何状态,只比较哈希和最近的 ctime。11.11.2024移除了 Wikilinks 支持。只保留纯 Markdown 链接,我们的知识库必须保持互操作性。26.10.2024更新现在按用户串行处理。原因是并发写文件出现过竞态。同时还遇到过转发消息乱序的问题,导致无法合并成一条。06.10.2024移除了 fyne.io。一开始我只是想找一个比 Electron 更轻量的方案,fyne.io 看起来是个理想候选。用了几天,机器人 80% 的功能已经搬过来了,体验也不错。但要把剩下 20% 落地,得花上巨大的工夫——滚动行为、emoji 渲染、文本选择、链接支持等等细枝末节。未来还要做图片上传、Markdown/HTML 渲染,在一个不基于 webview 的工具集里这些会非常痛苦。尽管我个人挺反感用 Web 技术做桌面应用,但似乎没得选。试试 wails.io。09.09.2024我们用 vendoring 管理依赖。我希望所有的少量依赖都在仓库里,这样就不必担心依赖被屏蔽或下线。我们的仓库就是自给自足的"唯一可信源"。09.06.2025我们在 db、journal、userconfig 里使用细粒度锁,而不是给每个用户上一把全局锁,以避免瓶颈。Workers 可能要调外部 API(如 ChatGPT),不希望长期持有用户锁。已 PATCHED——加入了按用户串行更新,bot自身不会导致竞态,但bot和worker之间仍可能竞态,所以继续保留细粒度锁。20.08.2024我们在每次访问时都从配置文件读取 userconfig 的每个字段。不需要在bot.Answer()前后整体加载/保存配置。修改时必须重新读取,避免写回过时数据。设想一下:如果只在bot.Answer()之前加载一次,而bot.Answer()中又有可观的网络延迟(比如对外部接口发请求要 2 秒),那这 2 秒内worker.MoveDueTasks()完全可能改写userconfig.Schedule,引发数据竞争(机器人答完后写回的就成了过期数据),调度信息会丢失。08.08.2024早期清洗(Sanitize Early)。我们放弃了在Path方法里做清洗——那种行为非常违反直觉,会把路径搞坏。东西一收进来就应该清洗。大多数命令都基于 md5 哈希工作,那种场景不需要清洗。13.07.2024用gofumpt做更严格的格式化。gofumpt接受的格式是 gofmt 的子集。在格式选项上越少做选择,越好。13.07.2024FS 的结构里应该带上userFS名字,明确它是按用户命名空间隔离的。09.07.2024"Note" 这个词太空泛。我们改用 "file",不再套用"笔记"这种高层抽象。13.08.2024放弃了 AST 解析 / 渲染。走 AST 时遇到太多边界情况,代码也愈发复杂。Markdown 并不那么难解析,老老实实写直白的代码就好。最终代码量降到原来的 1/3,理解起来也轻松多了。MD->HTML 转换我们做了同样的事。Telegram 并不支持完整的 HTML 标签,自己写一个 md-to-html 转换器反而更容易。08.07.2024遵循 Tolerant Reader 原则。解析时遇到乱码就跳过;但如果看到看起来合法的标记(比如###)后面跟着无效数据,就直接 panic。TODO:在读写循环中保留这些乱码。07.07.2024引入了 https://github.com/rivo/uniseg。在 Go 里,string 是只读的字节切片,可以通过for或[]rune(str)转成 Unicode 码点。但多个码点可能被组合成一个"用户感知"的字符,也就是 Unicode 规范里说的"字素簇"(grapheme cluster)。例如白色圆圈 "⚪" 由两个 rune 组成,却是一个字素簇。07.09.2024Markdown 转 HTML。用户笔记里可能有非法 Markdown,直接发给 TG API 会失败。所以我们先转义 HTML,再把用户的 Markdown 转成 HTML,最后通过 Telegram API 以 HTML 形式发出。13.06.2023文件名哈希化。所有出现用户输入的地方都应该用fs.hash,否则文件名会变得很长,而 Telegram 会返回INVALID_DATA(callbackData 最大 64 字节)。13.06.2023引入了db.go。本来就需要把 Redis 抽象出来(不然测试不好写)。13.03.2023db.go这个包里不存userID(我们经常单独使用userID)……真的吗?也许哪天我们会在与用户无关的场景下用它(比如全局 bot 统计)。补充:之后把userID挪进了类里。未来如果在用户作用域之外还要复用这个类再说,眼下先这样。13.06.2023我们不能在fs.Put里给文件名做首字母大写——万一这是用户在外部自己建的文件(小写)就被改坏了。






