后端(新增 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
177 lines
7.7 KiB
Markdown
177 lines
7.7 KiB
Markdown
# 在线编辑文章 · 集成到 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,得配套做重定向。
|