Files
blog/在线编辑器集成评估.md
T
zqlit c217d20c30 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

177 lines
7.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 在线编辑文章 · 集成到 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)
```bash
# 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 块里加一段)
```nginx
# —— 文章编辑后端:只给 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 侧
```bash
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,得配套做重定向。