ARTICLE · 1098791
文档不只是字符串
文档不只是字符串:FastDocs 块编辑器
Frappe Writer 的 FastHTML 移植:块是文档的原子,类型是块的语法

FastDocs:三份堆叠的文档(合成示意图)
contenteditable 是浏览器里最难做好的组件。光标、选区、粘贴进来的格式、撤销栈,任何一个环节都会出现分歧,最终存下来的是谁都不想要的 HTML 碎片。FastDocs 绕开了这条老路:文档不是一段连续的字符串,而是一列有序的类型块。块是原子,类型是语法,编辑就是替换块。
定位与功能全景
FastDocs 是 Frappe Writer 核心的 FastHTML 移植。上游是 Vue 3 加 TipTap,这里换成服务端渲染的 Python 加 SQLite。定位一句话:服务端渲染的、HTMX 驱动的文档编辑器。运行在 5016 端口,数据全部由 seed.py 合成。
块编辑器
文档是一列有序的类型块:标题、段落、项目符号、编号列表、引用、代码、分隔线;每块是 Markdown,可移动、增删改,全部 HTMX 片段替换。
文件夹与库
首页按文件夹分组展示文档。
模板
从可复用结构新建文档:会议纪要、项目简报、博客文章。
版本历史
快照文档并可恢复任意历史版本;恢复前先自动快照当前状态,可逆。
公开分享
发布到无需登录的只读 /p/
AI 助手
右栏聊天可起草、改写、摘要,基于工作区内容;Generate with AI 从一句话生成整篇文档;斜杠命令无密钥可用。
架构与数据
数据层 db.py 管 SQLite:documents、blocks、templates、template_blocks、doc_versions、folders。块按行存储,每行一块,文档是有序的块集合。
web_app.py 提供 fast_app 装配、会话登录与路由;web/views.py 渲染块编辑器,用 doc_main 与 doc_detail 两个 HTMX 片段实现整页不刷新的编辑;web/ai.py 承担生成与对话。
块是文档的原子
块的类型是受控词汇表,db.BLOCK_TYPES 只有九种:heading1、heading2、heading3、paragraph、bullet、numbered、quote、code、divider。
每种块都有确定的渲染方式,段落是段落,代码是代码,分隔线就是一条线。类型决定了块的外观,也决定了它能被 AI 怎么生成。

九种块类型构成文档的语法,编辑动作都是 HTMX 片段替换(合成示意图)
Generate with AI:协议钉死结构
AI 生成不是自由发挥。DOC_SYSTEM 指令把输出约束成 JSON 块数组:6 到 14 个块、首块必须是 heading1、不写散文、不用 Markdown 代码围栏。generate_doc 拿到模型回复后先抽取 JSON,再过两道防线:
for b in arr:
if not isinstance(b, dict):
continue
t = b.get("type", "paragraph")
if t not in db.BLOCK_TYPES:
t = "paragraph"
blocks.append({"type": t, "content": str(b.get("content", ""))})
if not blocks:
raise RuntimeError("The generated document was empty. Try again.")
if blocks[0]["type"] != "heading1":
blocks.insert(0, {"type": "heading1", "content": topic[:80]})
两道防线对应两种失败模式。类型越界:模型产出了不在白名单里的类型,降级成 paragraph,保证每个块都渲染得出来。结构缺失:首块不是 heading1,把主题截断 80 字插到最前,保证文档有一个像样的标题。生成永远不直接落库,先过校验,产物再进入可编辑的编辑器。

生成流水线:一句话 prompt → LLM → JSON 块数组 → 白名单校验 → 落库成文(合成示意图)
版本历史与公开分享
版本历史的数据结构是快照序列:每次保存当前版本写进 doc_versions,页面回滚就是恢复某一版快照。恢复操作本身先对当前状态自动快照,所以回滚永远是双向可逆的——误恢复也能回到恢复前的状态。
公开分享走 publish 生成的 token,只读页面 /p/{token} 不需要登录;取消发布后 token 立即失效。
边界与延伸
README 把边界写得坦白:这是一个演示单条业务纵深的移植,实时协同编辑(Yjs、OT)刻意排除在范围之外,单用户是设计决定,不是疏漏。ROADMAP 记录了与上游 Frappe Writer 的差距。三栏布局、确定性合成数据、多供应商 AI 助手、原生与 Docker 两种部署,与姊妹应用完全同构。
项目链接
GitHub 仓库:https://github.com/predictivelabsai/FastDocs
在线服务:https://docs.fastsme.com/