把 AI 生成的 HTML 当 Markdown 来管:Web-Doc 自托管文档站实践
一个开源项目的诞生故事:当 AI 越来越会写 HTML,我们却没有一个像样的地方"存放"它们。
项目地址:https://github.com/IcedSoul/web-doc

一、为什么会有这个项目
最近一年,我用大模型生成 HTML 单页的频率越来越高:
- 给同事讲一个技术方案,让 AI 直接生成一份带交互的 HTML 总结页;
- 做产品 demo,让 AI 写一个静态原型;
- 整理学习笔记,让 AI 把一段视频/论文翻译成可交互的可视化页面;
- 做活动方案,让 AI 出一份带样式的、能直接发给老板看的"网页版 PPT"。
但很快我就遇到一个尴尬的问题:这些 HTML 文档没地方放。
- 丢在 ChatGPT 对话里 → 找不到、不能再生成;
- 存成本地
.html文件 → 多文件(带图片、CSS)就乱了,分享要发压缩包; - 推到 GitHub Pages → 重,每次还要 commit;
- 用 Notion / 飞书 → 它们不渲染 HTML,最多放个截图。
我想要的其实是:
像管 Markdown 笔记一样管 HTML,有树形目录 、所见即所得 、改完即刷新 、一键分享 ,最好还能让 AI 直接在里面写。
于是有了这个开源项目 ------ Web-Doc:一个自托管、单二进制的 HTML 文档站。
仓库结构很轻:
web-doc/
├── apps/api/ # Go (Gin + GORM + Postgres) 后端
├── apps/web/ # React 19 + Vite + TS 前端
├── deploy/nginx/
└── docker-compose.full.yml
二、核心设计:一个文档 = 磁盘上的一个目录
很多类似工具会把 HTML 存进数据库的 text 字段,看起来简单,但一旦涉及多文件(index.html + assets/ + 图片)就立刻笨重。
Web-Doc 的选择非常"老派",但也非常顺手:
storage/docs/
├── 1a2b3c4d/ # 文档 A:单文件
│ └── index.html
├── 5e6f7a8b/ # 文档 B:多文件(zip 上传)
│ ├── index.html
│ ├── style.css
│ ├── app.js
│ └── assets/cover.png
└── ...
数据库(PostgreSQL via GORM)只存元数据:节点(文件夹/文档)、归属用户、分享 Token、AI 设置、Prompt 模板、MCP Token。真正的内容永远在磁盘上 ,可以直接 cd 进去用 vim 改,也可以 tar 一下搬家。
这个选择带来的好处会在后面 AI 流式写入、热更新两个特性中体现得淋漓尽致。
三、三个让我自己天天用的特性
特性 1:沙箱化预览 + 文件变更热更新
预览不是把 HTML "塞进" 一个 iframe,而是直接挂到一条独立路径上:
GET /d/{docId}/index.html
GET /d/{docId}/assets/cover.png
前端 iframe 加了 sandbox 属性,相当于让用户写的 JS 跑在一个隔离的"小宇宙"里 ------ 它出不来,污染不了主站。
特性 2:AI 流式生成,边写边落盘
很多 AI 写 HTML 的工具是"等生成完,整段渲染"。Web-Doc 走得更激进 ------ AI 输出的 token 直接落到磁盘上:
用户点击「AI 生成」
│
▼
POST /api/ai/generate (流式 SSE)
│
▼
Go 服务调用 OpenAI 兼容协议(DeepSeek / Kimi / GLM / Qwen / OpenRouter ...)
│
▼
每收到一段 chunk → 追加写入 storage/docs/{id}/index.html
│
▼
fsnotify 触发 → WebSocket 推送 → iframe 重载(节流 ~300ms)
实际效果:你看着模型一行一行把 <div> 写出来,预览框里 UI 一块一块地长出来,像在看一段延时摄影。
技术上的关键点:
- 配置完全 per-user:
Base URL/API Key/Model/System Prompt/Temperature/MaxTokens全部独立存在数据库; - 兼容 OpenAI Chat Completions 协议,所以国内外几乎所有模型都能直接接;
- 两种模式:生成新文档 或 改写当前文档;
- 节流 ~300ms 刷新预览,避免模型吐字快时浏览器卡死。
特性 3:内置 MCP Server ------ 让 Agent 直接管文档
这是我自己最爱的部分。Web-Doc 内置了一个 Model Context Protocol 端点:
POST /mcp # JSON-RPC 2.0 over Streamable HTTP,Bearer Token 鉴权
暴露的工具集:
| 工具 | 用途 |
|---|---|
list_documents |
列出整棵文档树 |
get_document |
看某个文档的元信息和文件清单 |
create_document |
新建文档/文件夹(可附初始 HTML) |
delete_document |
递归删除 |
read_document_file |
读文档下任意文本文件 |
upload_html |
写/覆盖单文件 |
upload_zip_base64 |
用 zip 整体替换文档 |
这意味着:任何支持 MCP 的 Agent(Claude Desktop、Cursor、CodeBuddy ...)都可以把 Web-Doc 当作自己的"知识库写入端"。
四、一个真实案例:用 Claude + MCP 自动整理周报
讲个我前两周真实跑通的工作流。
场景
我每周需要交一份周报,内容包括:本周完成、下周计划、风险项。素材散落在文档、Git commit、群聊里。
步骤
1. 在 Web-Doc 里建一个文件夹 周报/:
2. 在 Web-Doc 设置里签发一个 MCP Token:
UI → 用户菜单 → MCP Token → "新建",得到一个 wdmcp_xxx 的 Bearer。
3. 把这个 MCP 配置注册到 Claude Desktop:
json
{
"mcpServers": {
"web-doc": {
"url": "http://localhost:8787/mcp",
"headers": {
"Authorization": "Bearer wdmcp_xxx"
}
}
}
}
4. 对 AI Agent 说:
"我把这周做的事粘给你,同时参考下代码和git commit,帮我生成一份带 Tailwind 样式的 HTML 周报,标题用'Week 20 周报',然后用
create_document工具放到我 Web-Doc 的周报文件夹下。"
Claude 会:
- 调
list_documents找到周报/节点的id; - 生成 HTML 内容;
- 调
create_document创建子节点,parentId指向周报/,initial_html是渲染好的页面。
5. 浏览器刷新 Web-Doc:周报已经躺在树里了。
6. 一键分享:
UI 上点"分享" → 拿到 /s/{token} 短链,发到群里。链接打开是干净的、无 app chrome 的预览页,老板看着像一个正经网站。
这个流程闭环之后,我做周报的"组织/排版"成本几乎降到 0,剩下的全是"想清楚自己这周干了什么"。
五、第二个案例:用 AI 改写一个静态原型
这次完全不出 Web-Doc:
- 我先粘了一段从设计稿截图里 OCR 出来的文字进 AI 生成对话框,让它生成一个登录页 HTML;
- 流式生成的过程中,右边 iframe 一边长出按钮、一边长出表单;
- 看完不满意,点"改写当前文档 "模式,对它说: "把按钮改成 primary 紫色 + 圆角 12px,输入框加 focus ring。"
- Monaco 编辑器里 HTML 一行行变化,iframe 同步重载;
- 觉得 OK,
⌘S落盘(其实流式写入早就落盘了,这一步只是保险); - 直接拖一张 logo 图到这个文档对应的目录里(或在 UI 上传),
fsnotify监听到新文件,预览也会刷; - 改
<img src="logo.png">→ 完成。
整个过程:没切窗口、没开 VSCode、没 npm install、没 deploy。
六、技术栈与一些工程选择
| 层 | 选型 | 理由 |
|---|---|---|
| 后端 | Go + Gin + GORM + Postgres | 单二进制 / 易自托管 / 文件 IO + 长连接友好 |
| 文件监听 | fsnotify |
跨平台、稳定、几乎无依赖 |
| 实时推送 | gorilla/websocket |
与 fsnotify 配合做热更新 |
| AI | OpenAI 兼容协议(流式 SSE) | 一份代码兼容十几家模型 |
| 前端 | React 19 + Vite + TS | 现代、热模块替换香 |
| 编辑器 | Monaco | 与 VSCode 同源体验 |
| 拖拽 | dnd-kit | 树形结构里同级排序 + 跨层移动 |
| 状态 | Zustand | 比 Redux 轻得多 |
| UI | TailwindCSS + shadcn/ui | 默认就好看 |
| 部署 | 单 Go 二进制 / Docker Compose / Nginx | 适合自托管 |
几个值得一提的工程细节:
- 路径穿越防护 :所有
read/write路径都拒绝..与绝对路径; - ZIP 上传白名单:只允许 html/js/css/png/jpg/svg/woff2/...;
- 单文档 50 MB 上限:避免被人滥用;
- iframe sandbox:把用户 JS 关进笼子;
- JWT 多用户隔离:每个用户的文档、AI 设置、Prompt、MCP Token 都互不可见;
WEBDOC_DISABLE_REGISTER=1:可关闭注册,做"私有云"很方便。
七、自托管,三种姿势
bash
# A. 本地开发(最快,需 Node 18+ / Go 1.21+ / Postgres)
npm run install:all
npm run dev # api:8787 · web:5173
# B. 单二进制
npm run build
WEBDOC_WEB_ROOT=$(pwd)/apps/web/dist npm start
# C. Docker Compose 一把梭
docker compose -f docker-compose.full.yml up -d
文档存在 docs 这个 named volume 里,数据库存在 pgdata。换机器把这两个目录 tar 走就行,没有任何隐式状态。
八、它适合谁
- 经常让 AI 生成 HTML、却找不到地方存的人;
- 想做轻量"原型站"、不想为每个 demo 单独搭工程的设计师/PM;
- 有自托管偏好、不想让笔记类内容跑去三方云的开发者;
- 想给自己的 Agent 一个"写文档"工具的 AI 折腾爱好者;
- 不想再为"分享一个 HTML 文件"打包压缩包的人。
九、还能往哪走
短期 roadmap 里我考虑的几个点:
- 文档版本/快照(基于目录的 git-like 增量);
- 协作编辑(CRDT,可能上 Yjs);
- 评论与 review 流;
- 更细粒度的分享权限(密码、过期);
- 接入更多 Agent 平台的 MCP 注册指引。
十、结语
Web-Doc 的本质思想其实很朴素:
AI 生成的内容是一等公民,不应该被锁在某个聊天框里。
我希望它能像 Obsidian 之于 Markdown 一样,成为 HTML 文档的"本地+自托管+AI 原生"的一个起点。
如果你也在被"AI 生成的 HTML 满天飞"困扰,欢迎试用、提 issue、提 PR。
仓库 :web-doc (Go + React,单二进制可跑)
License:详见仓库