阿木的 Vibe Coding 建站实录:从 AI 生图提示词档案到 NAS 公网部署
记录我如何用 Vibe Coding 和单元式开发,完成一个 AI 生图提示词分享网站,并通过 Debian NAS、Docker 与 Cloudflare Tunnel 部署到公网。
阿木的 Vibe Coding 建站实录:从 AI 生图提示词档案到 NAS 公网部署
这是我,阿木,做 MU / PICPROMPT 这个 AI 生图提示词分享网站的完整记录。
这个项目最后没有采用纯 Cloudflare Worker 生产部署,而是运行在家庭 Debian NAS 的 Docker 中,再通过 Cloudflare Tunnel 穿透到公网。Cloudflare 负责 DNS、HTTPS 和 Tunnel 入口,网站和 API 仍然由 NAS 自托管。
公网地址:
https://mupicprom.amuspaces.top
这篇文章想讲的不是“我用了什么框架”,而是一个更重要的问题:
如何把一个模糊的建站想法,拆成 AI 可以持续执行、每一步都能验收、最后真的能上线的开发过程。
一、最初的建站想法
我想做的是一个 AI 生图提示词及 AI 生图图片分享博客网站。
它不只是放图片,也不只是放一段 prompt,而是让用户完成一条完整路径:
看到图片 → 理解画面 → 查看结构化提示词 → 复制使用 → 按条件检索类似作品
所以它的核心理念是:
所见即所得,高收纳性,高检索性。
每张图片都应该对应一套清楚的创作语言;每条提示词都应该可以被拆开、理解、复制和再次组合。
二、先用建站方法论回答五个问题
我使用了 build-site-methodology 这套建站方法论。它没有一上来让我选择框架,而是先要求回答五个问题。
1. 核心用户是谁?
不是“所有互联网用户”,而是三类人:
- 想直接复用提示词的 AI 生图用户
- 想学习提示词结构的初学者
- 需要整理灵感和素材的创作者
2. 用户来这里做什么?
核心动词只有四个:
- 看图
- 理解
- 复制
- 检索
3. 网站提供什么价值?
一句话概括:
用成图、结构化提示词和元数据,降低 AI 生图的学习与复用成本。
4. 哪些事情暂时放弃?
早期主动放弃:
- 复杂社交系统
- 私信和关注
- 社区等级
- 广告系统
- 低价值 SEO 页面
- 过度动效
- 在线生图平台
- 一开始就做复杂多语言
这一步很重要。很多项目不是做得太少,而是在核心功能还没有打磨好之前,把时间花在了不重要的地方。
5. 收益从哪里来?
第一阶段不急着变现,先验证内容价值和自然访问。未来再考虑投稿增值、会员或相关服务。
三、为什么采用单元式开发
这次最大的开发方法变化,是把工作分为两个角色:
总调度会话
总调度会话只负责:
- 拆分任务
- 安排单元顺序
- 检查范围
- 查看验收结果
- 记录决策和踩坑
- 给下一个 Agent 准备上下文
总调度会话不应该直接写业务代码。
开发单元会话
每个开发单元都新开一个对话,只完成一个目标:
- 先读取项目规则
- 明确本单元做什么
- 明确本单元不做什么
- 先写测试或验收标准
- 开发功能
- 验证构建和关键路径
- 把结果写回 AGENTS.md
- 结束会话
这样做的好处是,AI 不会在一个对话里不断扩大范围,也不会因为上下文变长而逐渐忘记最初的产品重点。
四、项目的完整开发单元
Unit 00:预备骨架
这是总调度会话中产生的预备代码,不计入正式开发单元。
完成了:
- React + Vite + TypeScript 骨架
- 首页视觉原型
- 图片瀑布流原型
- 独立作品详情页
- 基础提示词结构
- README 和 AGENTS 文档
后来我意识到,这些代码是在总调度会话中直接写出来的,违反了单元开发规则,所以在 AGENTS.md 中明确标记为“预备骨架”,不冒充正式 Unit 01。
Unit 01:内容模型与 Markdown 接入
目标是让网站脱离 App.tsx 里的硬编码数组。
完成了:
- PromptEntry 类型
- PromptSections 类型
- GenerationParameters 类型
- Markdown Frontmatter 解析
- 标题分段解析
- import.meta.glob 构建期加载
- slug 作为稳定详情页地址
- 一个完整示例提示词文件
每张图的提示词被拆成:
- 主体描述
- 动作与姿态
- 场景与环境
- 构图与镜头
- 风格与画面表现
- 质量与渲染
- 负面提示词
- 生成参数
同时保留 rawPrompt 和 rawNegativePrompt,避免结构化时丢失模型原始语法。
Unit 02:搜索与多条件筛选
搜索没有一开始就引入外部搜索引擎,而是采用构建期生成的客户端内存索引。
支持:
- 标题搜索
- 作者搜索
- 标签搜索
- 模型搜索
- 生成器筛选
- 提示词全文搜索
- 多关键词 AND 语义
- 多条件交集筛选
- 清除筛选
- 明确空状态
- 搜索结果进入独立详情页
后续还修复了筛选条件变化后瀑布流可见数量没有重置的问题,以及 CSS 多栏布局在懒加载时产生的图片重排问题。
Unit 03:图片资源和 WebP 策略
这一单元确立了图片资产规则:
- 统一使用 WebP 展示图
- 列表和详情使用统一资源路径
- 使用原生 lazy 加载
- 使用异步解码
- 使用稳定 aspect-ratio 占位
- 使用模糊到清晰的渐进加载
- 图片加载失败显示明确占位
我没有把原图源目录直接纳入网站。
项目中的 图/ 是 AI 生图原图源目录,必须满足:
- 不扫描
- 不索引
- 不压缩
- 不重命名
- 不移动
- 不提交 Git
- 不参与构建
网站真正使用的是独立的 public/images/ WebP 资产。
Unit 04:Cloudflare Worker、D1、R2 API
这一单元完成了 Cloudflare 全栈形态的技术实现:
- Worker 页面和 API
- D1 数据库结构
- 作品查询
- 作品详情
- R2 展示资源读取
- 数据迁移脚本
- API 单元测试
不过这个单元没有伪造生产绑定。D1、R2 和密钥仍然使用占位配置,真实生产授权被明确留到后面。
Unit 05:投稿和审核
这一单元让网站具备基本的内容运营能力:
- 投稿页面
- 管理审核台
- 投稿草稿
- 审核通过后发布
- 管理员令牌校验
- R2 图片上传
- 浏览器端转 WebP
- 图片压缩到 100 KB 以内
- 审核前编辑标题、作者、模型、标签、提示词和参数
- 删除无效图片资产
这里采取了一个很实际的策略:
投稿先进入草稿和审核记录,只有审核通过,作品才进入公开列表。
Unit 06:SEO、性能和基础安全
完成了:
- Title
- Description
- Canonical
- Open Graph
- Twitter Card
- 详情页动态元信息
- /admin 和 /submit 的 noindex
- 未知作品的 404 状态
- R2 Key 命名空间限制
- 上传大小限制
- CORS 收紧
- 构建期搜索索引
- 图片 lazy、async 和 aspect-ratio 策略
- 依赖版本锁定
Unit 07:账号、OAuth 和邀请码策略
这一单元曾经实现 GitHub OAuth、session、投稿额度和匿名邀请码。
但在后续安全策略调整中,账号投稿、OAuth 和 session 接口被关闭,当前正式策略改为:
- 投稿必须使用管理员邀请码
- 每个邀请码最多成功投稿 5 次
- 普通用户不能绕过邀请码
- 管理员令牌只放在本地环境变量中
这也是 Vibe Coding 中很重要的一点:功能做出来之后,还要敢于根据实际运营风险收缩功能,而不是为了“功能更多”强行保留。
Unit 08:生产安全基础
完成了:
- 图片真实文件魔数校验
- WebP、PNG、JPEG 类型识别
- 上传内容类型校验
- 客户端 IP 识别
- 60 秒窗口限流
- RATE_LIMIT KV binding
- 管理员令牌轮换
- 当前令牌和旧令牌过渡窗口
- 邀请码并发递增回滚
- 健康检查只暴露绑定状态,不泄露密钥
需要注意:KV 的 get + put 不是严格原子操作,因此它适合做边缘限流窗口,但邀请码的严格次数上限仍然由 D1 条件更新负责。
Unit 09:生产投稿链路和可观测性
本单元完成了本地可观测性:
- x-request-id
- 错误响应回显 requestId
- 429 响应和 Retry-After
- 结构化请求日志
- 不记录 Authorization
- 不记录邀请码
- 不记录请求体
- 不记录用户数据
但是由于当时没有正式生产 URL、D1/R2/KV 绑定确认和隔离邀请码,真实生产验收被明确标记为阻塞,没有用本地 mock 冒充生产证据。
Unit 10:严格生产审计契约
这一单元把真实并发验收标准固定下来:
- 10 个请求必须严格得到 5 个 202
- 另外 5 个必须得到 429
- 每个 429 必须有正数 Retry-After
- 失败请求不能残留 submission、prompt 或 R2 对象
- 成功测试数据必须清理
- Cloudflare 日志必须确认没有敏感字段
脚本在没有生产配置时会安全跳过,而不是伪造成功。
Unit 11:NAS Docker + Cloudflare Tunnel 上线
最终我选择了家庭 Debian NAS 自托管,而不是直接把生产服务放到 Cloudflare Worker。
实际架构如下:
Cloudflare DNS / HTTPS / Tunnel
↓
cloudflared
↓
web 容器 :4173
↓ /api/*
api 容器 :8787
↓
SQLite + 本地对象目录 + 本地 KV 文件
web 容器负责:
- Vite 生产构建产物
- 静态页面
- /api/* 反向代理
api 容器负责:
- Worker API 的本地 Node 运行实例
- SQLite 数据库
- 投稿和审核接口
- 本地对象存储目录
- 本地限流状态
Cloudflare Tunnel 将单域名:
mupicprom.amuspaces.top → http://web:4173
NAS 侧完成了:
- Docker Compose 配置检查
- web/api 镜像构建
- 容器启动
- 健康检查
- API 重启恢复检查
- SQLite 数据保留检查
- 对象目录保留检查
- 隔离投稿和清理测试
- 公网访问确认
五、开发过程中遇到的主要问题
问题一:PowerShell 工具曾经启动失败
最开始终端工具报找不到:
C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe
后来通过工具检查确认这个文件实际存在,原因是 Harness 终端进程的瞬时启动失败,而不是用户电脑没有 PowerShell。重试后终端恢复正常。
经验:不要看到 ENOENT 就立即断定项目环境缺少依赖,要区分“命令没有执行”和“命令执行后报错”。
问题二:React 类型缺失导致大量 JSX 报错
初始依赖没有安装:
- @types/react
- @types/react-dom
- Vite CSS 类型声明
补充依赖并新增 vite-env.d.ts 后,npm run build 通过。
问题三:Vite 因原图和临时文件 EBUSY 崩溃
Vite 默认监听项目目录时,遇到以下文件会崩溃:
- 图/ 下被其他程序占用的 PNG/JPG
- Agent 编辑文件产生的 .tmpdir 临时目录
- .AGENTS.md.
<id>
.tmpdir 这类动态临时目录
解决方式:
- 明确把 图/ 视为外部原图源
- 在 .gitignore 中忽略 图/
- 在 Vite watcher 中排除 图/ 和 .tmpdir 路径
- 每次重启后查看后台服务日志
问题四:Playwright 浏览器运行时不完整
Playwright 默认 headless shell 没有完整下载,浏览器验收时报 executable missing。后来改用已下载的 Chromium channel 完成验收。
经验:自动化测试不只是写测试代码,还要确认浏览器运行时本身存在,并把 CI 环境的浏览器安装写清楚。
问题五:不能把安全跳过当成生产通过
Unit 08、09、10 的生产脚本在缺少真实 URL 或凭据时会安全跳过并返回 0。
这个 0 只代表“脚本安全结束”,不代表:
- 生产 D1 已验证
- R2 已验证
- KV 已验证
- 并发限制已验证
- 清理已验证
- Cloudflare 日志已验证
这是非常容易被自动化流程误判的地方。
问题六:D1 mock 和真实 D1 行为不同
测试替身的 run().meta.changes 在旧版本中不存在。代码后来只在真实 D1 返回变化数时严格判定条件更新,生产前仍要求使用真实 SQLite/D1 做并发测试。
问题七:NAS 上 npm ci 失败
在 NAS 的 node:22-bookworm-slim 镜像中,npm ci 出现:
Exit handler never called
最后采取的方案是:
- Windows 侧先完成 npm install 和生产构建
- Docker 不在 NAS 内重新安装完整前端依赖
- Docker 只打包 dist 和零依赖 Node 运行时
- web/api 镜像成功构建并运行
这不是最理想的依赖构建方案,但它解决了 NAS 环境的实际兼容问题,并且没有把密钥或数据写进镜像。
六、Vibe Coding 的完整建站思路
第一步:先写产品,不先写代码
先明确:
- 为谁服务
- 用户来做什么
- 核心价值是什么
- 哪些功能暂时不做
- 如何验证项目值得继续
第二步:建立业务地图
把所有功能按用户感知排序:
- 图片展示
- 图片和提示词绑定
- 结构化提示词
- 搜索筛选
- 详情分享
- 内容维护
- 收藏、投稿和社交
这张地图是防止 AI 乱加功能的依据。
第三步:先做骨架,再做核心
先让页面能看、能点、能走通,再接入真实内容和数据层。
但骨架必须被明确标注为预备代码,不能误把视觉原型当成产品完成。
第四步:每个单元只解决一个问题
一个好的开发单元应该能用一句话描述:
这一轮只负责把 Markdown 内容接入首页和详情页。
如果一句话里出现“顺便再加上登录、搜索、投稿、部署”,说明范围已经膨胀。
第五步:让 AGENTS.md 成为项目的长期记忆
每个单元必须把以下内容写回 AGENTS.md:
- 做了什么
- 为什么这样做
- 验收结果
- 没做什么
- 哪些问题还没解决
- 新踩了什么坑
- 下一单元做什么
这样新 Agent 不需要重新猜项目背景,也不会重复踩相同的坑。
第六步:测试不是最后才做
每个单元都应先写验收标准,再写代码。
至少覆盖:
- npm test
- npm run build
- 关键页面交互
- 移动端布局
- 图片加载
- API 错误状态
- 安全边界
第七步:生产环境不做假验收
没有真实绑定、真实 URL、真实隔离数据和清理权限时,就只能说“本地通过”或“脚本安全跳过”。
不能因为命令返回 0,就声称生产已经验收。
第八步:安全配置永远不写进仓库
以下内容只通过 NAS 环境变量、Cloudflare Secret 或部署环境注入:
- 管理员令牌
- 上传密钥
- Tunnel Token
- Cloudflare ID
- 数据库密码
- 邀请码
仓库只保存:
- .env.example
- .dev.vars.example
- 占位绑定
- 部署说明
七、为什么最后选择 NAS + Cloudflare Tunnel
纯 Cloudflare Worker 的方案是可行的,Unit 04 到 Unit 10 也完成了相应的 Worker、D1、R2、KV 和安全代码准备。
但实际部署时,我更看重:
- 家庭 NAS 已经长期运行
- 图片和数据库都在自己手里
- 不需要先购买额外服务器
- Docker 方便拆分 web 和 api
- Cloudflare Tunnel 不需要暴露家庭公网 IP
- 单域名 HTTPS 入口比较简单
最终形态是:
NAS 负责运行,Docker 负责隔离,Cloudflare Tunnel 负责穿透和公网入口。
这也说明架构不是信仰,而是根据实际资源和维护成本做选择。
八、最终上线后的维护注意事项
当前网站已经通过家庭 NAS 和 Cloudflare Tunnel 对外提供访问。
日常维护应遵循:
- 更新前先备份 SQLite 和对象目录
- 不使用 docker compose down -v
- 不把 data、.env、.dev.vars 和 Tunnel 凭据提交 Git
- 修改后重新执行 Docker Compose 配置检查
- 检查 web 和 api 容器健康状态
- 检查 /api/health
- 检查公网 HTTPS 页面
- 检查投稿后审核和清理链路
当前记录中明确选择了不配置自动备份和日志轮转,所以后续如果内容量增加,我会优先补充:
- 自动备份
- 备份保留策略
- 日志轮转
- 磁盘空间监控
- R2 或异地对象备份
九、总结
这个项目真正让我学到的,不是某个框架 API,而是如何和 AI 一起做一个可维护的项目。
我最后形成的建站方式是:
产品五问
↓
业务地图
↓
架构取舍
↓
预备骨架
↓
独立开发单元
↓
测试和构建验收
↓
AGENTS.md 沉淀结果和踩坑
↓
生产环境隔离验证
↓
NAS Docker + Cloudflare Tunnel 上线
Vibe Coding 不是把一句“帮我做个网站”丢给 AI,然后等待奇迹。
真正有效的 Vibe Coding 是:
- 我负责目标、判断和取舍
- AI 负责实现、测试和重复劳动
- 文档负责保存上下文
- 单元负责控制范围
- 测试负责证明结果
- 生产验收负责区分“能运行”和“真的上线”
对我来说,这个网站不是一次性生成出来的,而是在不断拆分、验证、修正和记录中长出来的。
这就是我,阿木,用 Vibe Coding 建完一个 AI 生图提示词档案网站的全过程。