起因
最开始的 Arisaka.ETH 其实就是一个普通的个人博客。
基于 Fuwari 修改,使用 Astro 构建静态页面。
后来陆续加上了 Arweave、ENS、文章加密、Live2D、MIDI、Telegram Moment、CTF、MoeQ、AI 聊天、在线写作等功能,单纯把所有东西继续塞在博客仓库里已经不太合适。
因此现在项目主要拆成两个仓库:
- TomorrowX6/arisaka.eth:博客本体、文章和前端。
- TomorrowX6/arisaka.eth-workers:Cloudflare Workers、API、D1、R2 和后台任务。
简单来说就是:
arisaka.eth ↓博客 / 内容 / 前端 ↓Cloudflare Workers ↓D1 / R2 / Telegram / GitHub / AI / Ethereum静态的东西尽量保持静态,需要实时处理的东西全部拆到 Worker。
arisaka.eth 主站
主站目前使用 Astro 5 + Svelte,博客的文章、About、友链、Moment 页面以及大部分前端交互都放在这里。
主要技术栈:
- Astro / Svelte
- TypeScript
- Tailwind CSS
- Swup
- Pagefind
- Arweave
- ENS
- GitHub Actions
文章依然以 Markdown 为主:
Markdown ↓Astro ↓dist/不过生成 dist/ 后,并不是直接上传到普通静态托管。
Arweave + ENS
博客目前主要通过 Arweave 永久发布,再由 ENS 指向当前版本。
发布流程大概是:
Markdown ↓Astro Build ↓dist/ ↓Arweave ↓Path Manifest ↓ENS contenthash ↓arisaka.eth构建完成后会对文件计算 SHA-256。
如果一个文件的内容之前已经上传过,就直接复用原来的 Arweave Transaction,不需要每次发布都重新上传整个博客。
也就是:
旧文件→ 复用原 Transaction
修改过的文件→ 上传新的 Transaction最终再生成新的 Path Manifest。
Manifest 可以理解成当前博客版本的文件表:
/→ index.html
/posts/example/→ posts/example/index.html
/_entry/example.js→ 对应的 Arweave Transaction然后将 Manifest 写入 arisaka.eth 的 ENS contenthash。
因此实际上访问链路是:
arisaka.eth ↓ENS ↓Arweave Manifest ↓具体页面文件相比每次整个站点重新上传,这种方式能少传不少重复资源。
发布验证
Arweave 返回 Transaction ID 不代表公共网关一定已经可以立即读取。
所以在更新 ENS 之前,还会先验证新版本。
主要检查:
- Manifest 是否可以读取。
- 页面和静态资源是否可以读取。
- 文件大小是否正确。
- SHA-256 是否一致。
- MIME 类型是否正确。
- 页面路由是否正常。
流程大概是:
上传 Arweave ↓生成 Manifest ↓公网验证 ↓全部通过 ↓更新 ENS如果验证失败,就不会切换 ENS。
旧版本仍然继续工作。
这样至少不会因为新版本刚上传但网关还没有同步完成,直接把线上站点切到一个不可访问的版本。
ENS Gas
更新 ENS 本质上还是一笔 Ethereum 交易,自然也要考虑 Gas。
平时博客更新并没有必要为了立即上线,在 Base Fee 很高的时候硬发一笔交易。
因此部署流程设置了 Base Fee 上限。
Arweave 上传完成 ↓检查 Base Fee ↓费用较低 ↓更新 ENS如果费用超过设置值,则:
Arweave 上传完成 ↓暂不更新 ENS ↓release-watch ↓继续等待 ↓费用满足条件 ↓激活新版本也就是把“上传”和“上线”拆成两个阶段。
Arweave 上的新版本可以已经准备完成,但 ENS 仍然保持旧版本。
GitHub Actions
上面的发布过程主要通过 GitHub Actions 自动完成。
现在一次完整发布涉及:
- 安装依赖。
- Astro 检查。
- TypeScript 检查。
- 构建博客。
- 恢复 Arweave 上传缓存。
- 对比文件 SHA-256。
- 增量上传文件。
- 生成 Manifest。
- 验证公网 Arweave 内容。
- 检查 Ethereum Base Fee。
- 更新 ENS。
- 保存上传缓存和 outbox。
因此现在 GitHub Actions 已经不只是简单的:
pnpm build而更接近完整的发布流水线。
文章加密
博客支持直接发布加密文章。
没有采用密码直接加密正文的方式,而是拆成 KEK / DEK 两层。
密码 ↓scrypt ↓KEK ↓AES-256-GCM ↓DEK ↓AES-256-GCM ↓正文 HTML其中:
- KEK:由用户密码通过 scrypt 派生。
- DEK:每次构建随机生成。
- KEK 只负责加密 DEK。
- DEK 才真正负责加密文章正文。
这样密码派生和正文加密是分开的。
加密文章的正文不会进入:
- 首页摘要。
- RSS 正文。
- Pagefind 搜索。
- 初始文章目录。
用户输入密码后才会在浏览器内完成解密。
页面切换时已经解密的 DOM 也会清理。
当然,这种设计依然不能解决弱密码被离线爆破的问题,所以真要使用还是得设置高强度密码。
Provenance
既然文章经过:
ENS ↓Manifest ↓Arweave File那么也可以反过来查询当前看到的页面到底来自哪次发布。
因此文章底部加入了 Provenance 信息。
大概会显示:
ENS contenthash ↓Arweave Manifest TX ↓文章 File TX ↓Content Digest ↓App Version主要就是方便确认:
当前网页到底对应哪一个 Arweave Transaction 和哪一次博客发布。
并不是什么完整的密码学证明,只是把原本隐藏在发布流程里的信息展示出来。
Live2D
博客桌面端还放了一个 Live2D Roro。
最开始只是单纯的桌宠,后面又给它加上了聊天和文章话题功能。
AI 请求不会直接从浏览器调用模型 API,而是:
Browser ↓live2d-chat Worker ↓AI APIAPI Key 只保存到 Worker Secret。
Worker 负责请求检查、额度限制和模型调用。
在公开文章中,还可以根据文章内容生成相关话题。
首页、About、Friends 等非文章页面则主要播放动漫语录。
所以 Roro 现在已经不只是页面装饰,也算一个独立的交互入口了。
MIDI
侧栏播放器使用 SpessaSynth 在浏览器直接合成 MIDI。
支持:
.mid.midi.rmi- 顺序播放
- 随机播放
- 单曲循环
- 播放列表
- 进度拖动
合成器、AudioWorklet 和 SoundFont 都会等用户真正开始播放以后再加载,没必要刚打开博客就全部下载。
虚拟终端
Live2D 菜单里还能打开一个虚拟终端。
终端不是简单打印几行固定文字,而是做了基本的命令解析、历史记录、Tab 补全和虚拟文件系统。
例如:
lscdpwdcatopenwhoamiunamedateneofetchhistoryclearexit里面的部分文件直接来源于博客真实内容。
例如:
~/README/~/posts/~/about.mdopen 还可以直接打开博客文章。
Moment
Moment 是后来加的一个 Telegram 动态页面。
主要展示 @azusabot0 中的:
- 文字。
- 图片。
- 视频封面。
- 原 Telegram 帖子链接。
最开始很容易想到直接用 GitHub Actions 定时同步:
Telegram ↓GitHub Actions ↓生成数据 ↓重新构建博客但这样最大的问题就是:
发一条 Telegram,就得把整个博客重新构建和发布一次。
感觉多少有点抽象。
所以后来把同步完全拆到了 arisaka.eth-workers。
现在变成:
Telegram ↓Moment Worker ↓D1 + R2 ↓moment.000.moe ↓博客 Moment 页面帖子信息保存到 D1。
图片和视频封面保存到 R2。
博客只请求:
https://moment.000.moe/api/posts然后在浏览器里动态显示。
这样 Telegram 新增一条动态和博客构建就完全没关系了。
arisaka.eth-workers
随着动态服务越来越多,最后将 Cloudflare Workers 单独拆到了:
TomorrowX6/arisaka.eth-workers
它现在更像整个博客的后端 Monorepo。
目前主要包括:
blog-proxy/ctf-game/live2d-chat/moment/release-watch/waline/randimg/writer/docs/每个 Worker 都有自己的依赖、配置和部署流程。
blog-proxy
blog-proxy 负责把 Arweave 上的博客通过:
000.moewww.000.moe提供出来。
Worker 会读取 arisaka.eth 当前的 ENS contenthash,找到对应的 Arweave Manifest。
不过不会让每一个访问请求都实时查询 Ethereum RPC。
ENS 解析结果会缓存在 Cloudflare Edge,缓存过期后再后台刷新。
大概就是:
用户 ↓000.moe ↓Cloudflare Worker ↓缓存的 ENS Manifest ↓Arweave这样 ENS 仍然是博客版本的来源,但不会直接卡在每一个用户请求的关键路径上。
moment
moment 负责 Telegram 频道同步。
Worker 定时读取 Telegram getUpdates:
Telegram ↓Worker ├─ D1 └─ R2 ↓Moment APID1 保存帖子和同步状态。
R2 保存图片以及视频封面。
主站只负责展示。
live2d-chat
live2d-chat 主要负责 Roro 聊天以及相关 AI 功能。
包括:
- AI 对话。
- 文章话题生成。
- 话题缓存。
- 请求来源检查。
- 调用额度限制。
- MoeQ 桌宠相关接口。
模型 API Key 全部放在 Worker 后端。
release-watch
release-watch 主要负责博客版本激活和状态监控。
当一次部署已经把内容上传到 Arweave,但因为:
- Arweave 网关尚未完全可读。
- Ethereum Base Fee 太高。
暂时不能更新 ENS 时,就由它继续跟踪。
新版本 ↓Arweave 已上传 ↓等待可用 ↓等待低 Gas ↓触发激活 ↓ENS 更新同时也会对相关站点进行监控。
waline
评论系统也迁到了 Worker。
前端仍然使用 Waline 客户端,但后端数据保存在 D1。
Blog ↓Waline ↓Worker ↓D1这样评论功能就不再需要单独维护传统服务器。
randimg
随机图片服务以前使用:
FastAPI+nginx+SQLite现在已经迁移成:
Cloudflare Worker+R2+D1WebP 图片保存到 R2。
图片索引保存到 D1。
并且旧的图片 ID 和 URL 结构继续保持兼容,所以博客里已有的链接不需要因为迁移重新修改。
Writer
writer 用来解决写博客的问题。
目前同时为 Android Writer 和 Web Writer 提供后端。
登录使用 GitHub App。
编辑完成后可以直接把 Markdown 提交到:
TomorrowX6/arisaka.eth的 main 分支。
整个过程就变成了:
Writer ↓GitHub ↓arisaka.eth ↓GitHub Actions ↓Astro ↓Arweave ↓ENS也就是说,不一定非要在电脑上:
git clonevimgit addgit commitgit push才能更新文章。
在 Writer 里写完也可以直接发布。
Web Writer 本身还包括文章列表、Markdown 编辑、实时预览、标题大纲、字数统计、命令面板等功能。
CTF
CTF 后来基本已经从博客本体独立出来了。
博客中主要留下入口,而实际游戏运行在 ctf-game 中。
包括:
- Plasma 风格浏览器桌面。
- CTF 关卡。
- 运行时 Worker。
- 存档。
- 自动化测试。
- 部署流程。
主站负责:
展示入口Worker 负责:
真正运行MoeQ
MoeQ 也放在 ctf-game 相关服务中。
包含题库、媒体处理和运行时逻辑。
目前题目不再只是单纯的文字选择题,还涉及角色、声优、音乐、语录、图片等不同类型。
题库及媒体处理本身比较重,因此和博客内容构建分开维护会方便很多。
不然更新一次题库就重新跑整个 Arweave 博客发布,多少有点离谱。
support/blog
ctf-game/support/blog/ 保存 CTF 和博客之间真正需要共享的最小公开内容。
也就是说没有为了 CTF 再把博客整个复制进 Workers 仓库。
部署流程只导出需要更新的入口文件,然后再交给博客仓库。
两个项目之间尽量保持低耦合。
docs
CTF 和 MoeQ 原本有一部分运行、维护和实现说明放在博客仓库。
拆分 Worker 后,相应文档也一起迁到了 arisaka.eth-workers/docs/。
这样 Worker 的代码、部署方式和对应运维说明都放在同一个仓库里。
现在的架构
现在整体关系大概是:
┌──────────────────┐ │ arisaka.eth │ │ Astro / Svelte │ └────────┬─────────┘ │ GitHub Actions │ ▼ ┌──────────────────┐ │ Arweave │ │ Manifest / Files │ └────────┬─────────┘ │ ▼ ENS │ ▼ ┌──────────────────┐ │ blog-proxy │ │ 000.moe │ └──────────────────┘
arisaka.eth │ ├── Moment ───────── moment Worker ───── D1 / R2 / Telegram │ ├── Roro ─────────── live2d-chat ─────── AI API │ ├── Comments ─────── waline ──────────── D1 │ ├── Random Image ─── randimg ─────────── D1 / R2 │ ├── Writer ───────── writer ───────────── GitHub │ └── CTF / MoeQ ───── ctf-game博客本体基本只负责内容和前端。
需要状态、数据库、第三方 API 或定时任务的功能则交给 Workers。
为什么拆开
最明显的好处就是各部分不再互相绑死。
以前可能是:
Telegram 更新 ↓博客数据变化 ↓重新构建 ↓重新上传 Arweave ↓更新 ENS现在:
Telegram 更新 ↓Moment Worker ↓D1 / R2结束。
评论也是一样:
新增评论↓D1不影响博客。
AI 出问题:
live2d-chat出问题。
静态文章照样可以正常访问。
随机图片服务出问题,也不会影响 Arweave 上的文章。
最终把整个项目分成两种东西:
静态内容→ arisaka.eth
动态服务→ arisaka.eth-workers各自独立部署,也各自独立维护。
总结
Arisaka.ETH 最开始只是想做一个自己的博客。
后来慢慢变成:
Astro+Svelte+GitHub Actions+Arweave+ENS+Ethereum+Cloudflare Workers+D1+R2+Telegram+GitHub+AI很多东西并不是一开始就计划好的。
基本都是遇到问题以后再慢慢改。
比如:
不想依赖传统服务器→ Arweave
想使用链上域名→ ENS
不想重复上传文件→ SHA-256 增量缓存
Gas 太贵→ release-watch
Telegram 更新不想重建博客→ Moment Worker
不想继续维护图片服务器→ R2 + D1
AI Key 不能放前端→ live2d-chat
想随时写文章→ Writer做着做着,最后就成现在这样了。
不过目前两个仓库的职责已经比较清楚:
arisaka.eth 负责博客本身,arisaka.eth-workers 负责博客背后的动态服务。
至少相比以前什么都塞在一起,现在维护起来舒服多了。
图片预览






- ENS Contenthash
- Resolving…
- Article File TX
- Resolving…
- Content Digest
- Resolving…
- Source Version
- Resolving…