最后更新: 2026-09-04
项目概述
个人域名 loopv.net 下的全栈项目集合,基于 GitHub + Cloudflare 全家桶零成本运行。
子站点
架构
apps/
├── portal/ Astro SSG → Cloudflare Pages 部署
├── docs/ Astro SSG → Cloudflare Pages 部署(构建时读取仓库 md 渲染)
└── chat/
├── src/
│ ├── worker.ts Hono HTTP 路由 + API(含 chat + admin 两套)
│ ├── chat-room.ts Durable Object (WebSocket 广播 + 认证)
│ ├── rate-limiter.ts Durable Object (按 IP 登录/注册限流,防暴力破解)
│ └── auth.ts 密码哈希 + session 工具
├── public/
│ ├── chat/ 聊天室前端 (原生 HTML/CSS/JS)
│ └── admin/ 管理平台前端
└── migrations/ D1 建表 SQL
聊天室消息流
浏览器 ──WebSocket──> Worker ──获取DO──> ChatRoom DO ──广播──> 所有在线客户端
│ │
│ └── D1 持久化写入
│
├── GET /api/history → D1 读取历史(需登录;过滤已删除,撤回消息脱敏不返回原文)
├── POST /api/upload → R2 存储文件
├── GET /media/:name → R2 读取文件
└── admin API(撤回/删除)→ D1 + DO 广播实时推送
数据模型
users 表
| 字段 |
说明 |
username / password_hash / salt |
用户名 + PBKDF2-SHA256 哈希密码 + 随机盐 |
nickname / avatar_url |
昵称 / 头像(R2,可选) |
is_admin |
管理员标记(第一个注册用户自动成为管理员) |
is_test |
测试用户标记(权限同普通用户,后台仅创建时一次性回显明文密码,列表不展示) |
plain_password |
测试用户明文密码(仅 is_test=1 有值) |
banned |
封禁标记 |
sessions 表
token / user_id / expires_at:session token,7 天过期
messages 表
deleted 字段四态语义:
0 = 正常
1 = 用户撤回(chat 显示「消息已撤回」)
2 = 管理员撤回(chat 显示「已被管理员撤回」)
3 = 已删除(chat 完全不显示)
settings 表(key/value 键值对,002 迁移)
invite_code_enabled / invite_code:注册邀请码开关与邀请码(admin 后台维护)
docs_hidden:docs.loopv.net 隐藏文档 slug 列表(JSON 数组,admin 文档管理维护)
deleted_usernames:已注销/被删除用户名列表(JSON 数组,用户名 tombstone,同名禁止重新注册,防历史归属混淆)
关键设计决策
- 聊天室不用第三方现成方案:GitHub 上无完全匹配的开源项目(需要 Workers+DO+D1+R2+匿名+多媒体),选择基于
cloudflare/workers-chat-demo (1.1k⭐) 的架构自行实现
- Hibernation API:使用 Durable Object 的 WebSocket Hibernation,空闲时不计费,保持长连接
- 认证状态用 serializeAttachment:DO 休眠唤醒后内存 Map 会失效,用
serializeAttachment 存储每连接的认证状态(Cloudflare 官方方案)
- chat + admin 共用一个 Worker:通过
host header 区分(admin. 前缀),复用 D1/R2 绑定
- 门户反 AI 感设计:低饱和深墨绿单强调色(
#2e5d4f)+ 暖灰中性底,Outfit 字体 + 非对称布局,浅色纸感终端,刻意避免 AI 紫渐变、模板化三段式等 AI 生成痕迹
- Monorepo:pnpm workspaces 管理多子站点,共享依赖
- 按 IP 限流用 Durable Object:登录/注册暴力破解防护用独立
RateLimiter DO(DO storage 持久化),不用 D1 建表——避免手动 SQL migration,DO 的 new_sqlite_classes migration 随 wrangler deploy 自动生效
- 上传安全策略:R2 上传走 MIME + 扩展名双重黑名单,危险类型(html/svg/js/xml 等)直接拒绝;媒体响应加
nosniff;WebSocket 消息的 media_url 仅接受 /media/ 前缀
- 文档站单一事实源:docs.loopv.net 内容 =
apps/docs/src/lib/docs.ts 的 SOURCES 清单登记的仓库 md(根目录 *.md + docs/*.md),构建时由 apps/docs 读取渲染成静态页,推送 master 自动重建;新增 md 必须登记到 SOURCES(含 repoPath/slug/group),不会自动收录。内容本身不做运行时编辑,但显示开关由 admin 控制——settings 表 docs_hidden 存隐藏 slug 列表,admin 保存后触发 Pages Deploy Hook(env secret DOCS_DEPLOY_HOOK)重建;docs 构建拉取公开 GET /api/docs/visibility 过滤,失败降级为全部显示;/manifest.json 静态端点输出全部文档(含隐藏项)供 admin 跨域读取
- 隐私与数据安全整改(2026-09-04):自审后落地一批约束——
/api/history 强制登录且对撤回消息(deleted 1/2)返回时脱敏;封禁/删除用户/自助注销/改密经 DO 内部 /kick 即时断开在线连接;注销/删除用户记录 username tombstone(settings deleted_usernames)防历史归属混淆;RateLimiter DO 每日 Alarm 清理过期限流键;媒体/头像文件名 32 位加密随机;全站字体 self-host。详见 docs/security-review.md「整改记录」
Cloudflare 资源
| 资源 |
名称 |
用途 |
| D1 Database |
loopv-chat-db |
用户/会话/消息持久化 |
| R2 Bucket |
loopv-chat-media |
聊天室图片/视频/音频/头像存储 |
| Pages 项目 |
loopv-portal |
门户主页部署 |
| Pages 项目 |
loopv-docs |
文档站部署 |
| Worker |
loopv-chat |
聊天室 + 管理平台 API + WebSocket |
部署流程
- 在 Cloudflare 控制台创建 D1 (
loopv-chat-db) 和 R2 (loopv-chat-media)
- 在 D1 Console 执行
migrations/001_init.sql 与 migrations/002_invite_settings.sql
- 将 D1 database_id 填入
apps/chat/wrangler.toml
- chat Worker 自动部署:推送到 master 触发 GitHub Actions(
.github/workflows/deploy-chat.yml)执行 build + wrangler deploy(DO new_sqlite_classes migration 随部署自动生效),无需本地手动部署;首次使用前需在仓库 Actions secrets 配置 CLOUDFLARE_API_TOKEN / CLOUDFLARE_ACCOUNT_ID
- Pages 连接 GitHub → 部署 portal(root:
/, build: pnpm --filter @loopv/portal build, output: apps/portal/dist)
- Pages 连接 GitHub → 部署 docs(root:
/, build: pnpm --filter @loopv/docs build, output: apps/docs/dist)
- 绑定域名:loopv.net → Pages(portal);chat.loopv.net / admin.loopv.net → Worker;docs.loopv.net → Pages(docs)
命名约定
- 文件名: kebab-case (
chat-room.ts, global.css)
- 目录: kebab-case
- npm 包:
@loopv/* scope
- 组件: PascalCase (Astro components)