Files
blog/在线编辑器集成评估.md
T
zqlit cac64f18c7 feat(editor): 在线编辑文章(Worker 前端 + 轻量 docker 后端)
后端(新增 editor-api/,零 npm 依赖,只用 node 内置模块):
- 只做文章相关:列表/读取/新建/保存/删除/图片上传/git 状态·发布·同步
- front matter 往返保真:未改动的块按字节照抄,CRLF/块标量/引号写法都不动
- 列表用目录名当 id,slug 撞名不再静默改错文件(返回 409 列候选)
- 鉴权只有一条路:X-Editor-Token(只存在 Worker 侧,浏览器拿不到)
- 端口只绑 127.0.0.1,由宿主机 nginx 反代出去

部署(新增 deploy/editor-api/bootstrap.sh):
- 一条命令在新机器上完成 克隆仓库→写 .env→起容器→健康检查
- 状态全在两个目录(/srv/blog 仓库工作区 + /srv/editor-api 配置),
  迁移 = 复制目录或在新机重跑本脚本,容器本身无状态

Cloudflare Worker 侧(blog-admin):
- src/routes/editor.ts:鉴权 + 反代,浏览器只跟 Worker 说话
- 管理面板新增「文章编辑」:两栏布局 + 快捷插入面板(13 项短代码,
  与 write-server 的 ShortcutPanel 一致)+ 底部 草稿/保存/发布
- 系统设置改为 schema 驱动的表单,且**以运行时实际生效的配置为准**
  (frontend_conf/captcha/moderator/ip_region/site_default + KV human_check),
  修复「表单显示一套、评论系统跑另一套」的脱节问题

验证:tsc 0 错;front matter 往返 130/130;保存往返 130/130;
API e2e 44/44;无头 Chrome UI e2e 14/14
2026-10-04 21:16:06 +08:00

7.7 KiB
Raw Blame History

在线编辑文章 · 集成到 api.200181.xyz/admin

日期:2026-10-04(定稿) 结论:已实现并本地全链路验证通过。 形态:前端长在现有 api.200181.xyz/admin 面板里(新增「文章编辑」tab), 后端是一个零 npm 依赖的轻量 Docker 容器(editor-api/),替代臃肿的 write-server。


0. 一句话

把 write-server/ 里只有「文章在线编辑」这一件事剥出来,做成 200 行级的轻量后端 容器;/admin 面板加一个 tab,Worker 只做「登录鉴权 + 注入令牌转发」。


1. 最终架构

浏览器
  │  (只用后台已有的管理员登录态,不需要任何新凭据)
  ▼
Cloudflare Worker  api.200181.xyz            blog-admin/src/routes/editor.ts
  /api/v2/editor/*                             只做两件事:
  │                                             ① isAdminRequest() 鉴权
  │  X-Editor-Token 只存在这里                   ② 注入 X-Editor-Token 反代
  ▼
nginx  post.usj.cc  /editor-api/  ──►  127.0.0.1:8017
                                        editor-api 容器(零 npm 依赖)
                                        读写 /srv/blog/content/posts/**,
                                        图片落文章同级目录,
                                        发布 = git add/commit/pull --rebase/push

三层各自的职责被切得很干净:

层 文件 职责 故意不做的
前端 blog-admin/public/admin/admin.js(新增 ~450 行) 列表 / 编辑 / 图片粘贴 / 保存 / 发布 不碰令牌、不直连后端
Worker blog-admin/src/routes/editor.ts(新增,150 行) 鉴权 + 反代,10 条路由 不碰仓库
后端 editor-api/(新增目录) 读写 Markdown + git 不做账号、评论、AI、部署编排

2. 顺带发现的安全问题(write-server 生产环境)

评估过程中实测发现 write-server 线上完全没有鉴权(只读核实,未做任何写操作):

现象 证据
后端公网裸奔 23.254.236.47:8016 直接返回 200
文章全量泄露 https://post.usj.cc/api/posts 无凭据返回全部 133 篇
X-Auth-User 无签名 只读明文用户名,可任意伪造
写操作全裸 PUT / DELETE / upload / deploy / ai 全部无鉴权、无 middleware
nginx 未加 auth_basic —

这正是这次重写的动机:不是把 write-server 搬个家,而是换成一个「默认安全」的小东西—— 令牌只在 Worker 里、端口只绑回环、没配令牌直接拒绝启动。


3. 改动清单

新增

  • editor-api/server.mjs + editor-api/src/{frontmatter,posts,git}.mjs —— 零依赖后端
  • editor-api/test/{frontmatter-roundtrip,save-roundtrip,api-e2e}.mjs —— 三个测试
  • editor-api/{Dockerfile,README.md}、docker-compose.editor.yml
  • blog-admin/src/routes/editor.ts —— Worker 反代
  • blog-admin/.dev.vars.example 的编辑器两项

修改

  • blog-admin/src/index.ts —— 注册 10 条 /editor/* 路由
  • blog-admin/src/types.ts —— Env 加 EDITOR_API_BASE / EDITOR_TOKEN
  • blog-admin/wrangler.toml —— [vars] 加 EDITOR_API_BASE
  • blog-admin/public/admin/admin.js —— 新增「文章编辑」tab(+约 450 行)
  • blog-admin/public/admin/admin.css —— 编辑器样式(+约 80 行)
  • .gitignore —— 加 .editor-tmp/ / .editor-trash/

4. 测试结论(全部通过)

测试 结果 说明
frontmatter-roundtrip.mjs 130/130 split/join 逐字节还原 + parse/stringify 深相等
save-roundtrip.mjs 130/130 每篇「读出来原样存回去」sha256 不变,测完全部还原
api-e2e.mjs 44/44 鉴权/读取/回存/只改正文/改字段/新建删除/上传/git/撞名/无空行
Worker 反代链路(.editor-tmp/verify-relay.mjs) 26/26 真实走 wrangler dev,含鉴权 403 / 404 / 409
后台 UI(无头 Chrome 真点) 全通过 登录→列表→打开→改→保存→发布弹层,4 张截图

过程中抓到并修掉的真 bug:

  1. front matter 后的空行:语料里 111 篇有空行、19 篇没有;getPost 把前导空行剥掉后 信息丢了,savePost 又无条件补一个 → 那 19 篇一保存就被平白多插一个空行。 修法:readIndex 记 bodyLead,回写时按原文件风格还原。
  2. slug 撞名静默改错文件(详见第 6 节):定位键从 slug 换成目录名。
  3. CRLF/LF、块标量 chomping(|- / | / |+)、getPost 漏 filePath/eol、 重复 slug 返回 500 —— 都是早期测试抓到的。

5. 上线步骤

① 服务器侧(23.254.236.47)

# 1. 博客仓库 checkout 到 /srv/blog(main 分支,git 工作区)
#    (原 write-server 用的那份仓库可以直接沿用,确认 remote 是 CNB + GitHub 两段)

# 2. 放 compose 与令牌
cd /srv/blog
mkdir -p /srv/editor-trash
cat > .env <<'EOF'
EDITOR_TOKEN=<openssl rand -hex 32 生成的值>
EOF
docker compose -f docker-compose.editor.yml up -d --build
curl -s http://127.0.0.1:8017/health     # 应返回 {"ok":true,...}

② nginx(post.usj.cc 的 server 块里加一段)

    # —— 文章编辑后端:只给 Cloudflare Worker 反代用 ——
    # 令牌本身就是鉴权(X-Editor-Token),所以这里不再叠 auth_basic。
    location /editor-api/ {
        proxy_pass http://127.0.0.1:8017/;   # 末尾的 / 会剥掉 /editor-api 前缀
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        client_max_body_size 25m;             # 图片直传
        proxy_read_timeout 120s;              # git push 可能慢
    }

可选加固:location 里再叠 allow <Cloudflare IP 段>; deny all;, 让这条路径只有 CF 能碰到。令牌泄露才是真风险,这层属于纵深防御。

③ Cloudflare 侧

cd blog-admin
npx wrangler secret put EDITOR_TOKEN     # 粘贴与服务器 .env 相同的值
npx wrangler deploy

EDITOR_API_BASE 已写在 wrangler.toml 的 [vars](https://post.usj.cc/editor-api)。

④ 验证

打开 https://api.200181.xyz/admin → 「内容管理 / 文章编辑」→ 随便开一篇 → 改一个字 → 保存 → 「发布 / 同步」里确认文件出现在待发布列表。

⑤ 下线 write-server(确认新编辑器好用之后再做)

  1. nginx 里摘掉 write-server 的 location /(先只留 location /editor-api/)
  2. docker stop write-server && docker update --restart=no write-server
  3. 关掉 8016 的公网映射(compose 里删 ports 或改绑 127.0.0.1)
  4. 观察几天没问题,再删镜像与数据卷(删前先备份)

6. 遗留问题:slug 撞名(需要你决定)

仓库里有 5 组文章共用同一个 slug,Hugo 的 permalink 是 /:slug,所以每组里 有一篇在线上是被另一篇覆盖掉的(打不开):

slug 两篇
20210901 Twitter主题加入加载耗时… / 无悔
20211122 情侣恋爱倒计时小工具… / 这组照片的主题,咱就叫它光吧
20211128 大学生体测… / 可惜不能一直做小孩子…
20211223 更换掉jsdelivr… / 放假之前最后一次的照片合集…
20240602 parsec远程软件报6023错误 / idea关闭ai自动补全

编辑器已经把这件事标出来了(列表里黄色「URL 冲突」标签;拿撞名 slug 去查会返回 409 并列出候选篇目,不会猜)。但改哪一篇的 slug、还是让后写的那篇换个 slug,需要你定。

顺带:改 slug = 改网址,旧链接会 404。如果在意 SEO,得配套做重定向。