1. 目标
之前博客已经通过 scripts/baidu-sync.mjs(xpan 开放 API + 网页端内部接口)实现了「资料」条目附件的同步上传 → 创建分享 → frontmatter 回写 → 资料页「☁️ 百度网盘」备用按钮。在此基础上希望扩展三件事:
- 前台「网盘资源」总览页(
/disk/):不跟资料条目绑定,直接把网盘/apps/mynote下的全部文件按类型分组展示,附带备用下载链接和提取码,npm run baidu:sync一跑就自动更新。 - 文章正文内嵌下载卡片:在任意文章/笔记正文的指定位置写
{{netdisk 文件名.zip}}或{{网盘 文件名.zip}},构建时自动渲染为带下载按钮和「一键复制提取码」的卡片,不用回资料页绕一圈。 - CMS 后台网盘管理页(
/admin/disk/):在后台里浏览网盘目录、创建分享、上传文件、复制「嵌入代码」——复用现有 Cloudflare OAuth Worker 做鉴权和代理,不新增后端。
整体约束:不能增加任何需要 24h 开机的后端,保持 GitHub Pages + 若干 Secrets 的静态/Serverless 架构。
2. 最终效果
上线后三条独立使用路径:
- 读者路径:导航栏点「网盘」→ 按图片/文档/压缩包/其他分组浏览,有链接的文件显示「☁️ 百度网盘」蓝色按钮和「提取码」按钮(点击即复制),待分享的文件显示灰色徽标(下一次 CI 会自动补齐)。
- 作者路径(写文章):在正文中写
{{netdisk GD32固件手册.pdf}},npm run build时自动从src/data/netdisk.json查找 basename,渲染带下载 + 提取码复制的内联卡片,代码块里的 token 不会被处理。 - 作者路径(后台管理):访问
/admin/disk/→ 用站点所有者的 GitHub 账号登录(复用 Decap CMS 的 OAuth 弹窗,token 存在sessionStorage,和 Decap 隔离)→ 表格列出网盘中所有文件的名称、大小、分享状态,行操作包括创建分享、复制链接、复制提取码、复制{{netdisk …}}嵌入代码(粘贴到文章即生效);右上角「上传文件」按钮最大 64MB(XHR 带进度条),秒传命中直接跳过。
3. 架构流程图
┌───────────────────────────────────────────────────────┐
│ 构建期(npm run build / CI) │
│ │
src/files/*.md ◄────── frontmatter baidu/code 回写──────┐ │
│ │ │
▼ │ │
public/files/*.{zip,pdf,...} ──► scripts/baidu-sync.mjs │ │
│ ▲ 1. precreate/秒传/分片上传 │
│ │ 2. 网页端 /share/set 创建分享 │
│ │ 3. 回写 frontmatter │
│ │ 4. 生成清单 ◄──────┐ │
│ └────────────────────────┐ │
▼ ▼ │
src/data/netdisk.json │
(version,generatedAt,remoteDir,files[]) │
▲ ▲ ▲ │
┌────────────────┘ │ │ │
▼ ▼ │ │
src/pages/disk/ src/utils/remark-netdisk.mjs │ │
index.astro ───import────► 构建期遍历 mdast, │ │
(按扩展名分组渲染) {{netdisk 文件名}} 替换成 │ │
自包含 nd-card HTML │ │
│ │
└─────────────────────────────────────────────────┼────┘
│
推送后的 GitHub Pages (纯静态 dist/) ◄─────┘
导航 /disk/ 任意文章内 nd-card ┌ /admin/disk/ 独立HTML
│
┌─────────────────────────────────────────────────┼──────────┐
│ ▼ │
│ Cloudflare OAuth Worker(/disk/* 端点) │
│ Secrets: GITHUB_ALLOWED_OWNER / BAIDU_APP_KEY / ... │
│ │
│ GET /disk/list ◄── 1. xpan list + merge netdisk.json │
│ POST /disk/share ◄── 网页端 /share/set(BDUSS 凭证) │
│ POST /disk/upload ◄── precreate → 秒传 → 8MB分片→create│
│ OPTIONS /disk/* ◄── CORS 白名单:<username>.github.io│
│ · localhost:8080 · 127.0.0.1:8787 │
│ GET /auth → /callback ◄── Decap CMS 原有握手(字节不动) │
│ │
└─────────────────────────▲───────────────────────────────────┘
│ Authorization: Bearer <gh-token>
│ verifyOwner() 比对 GITHUB_ALLOWED_OWNER
│
public/admin/disk/index.html
· GitHub OAuth 弹窗(postMessage authorizing:github ↔ authorization:github:success:)
· XHR 上传 + 进度条
· 事件委托复制(clipboard API + execCommand 降级)
· data-theme=dark 与 localStorage.theme 同步
核心设计原则——所有分享链接在 build 期就固化到产物里:浏览器直链不会污染 access_token,Worker 只在后台管理时才接触敏感凭证,前台页面零密钥。
4. 关键技术点
4.1 单一数据源:src/data/netdisk.json
三个功能(前台总览、文章卡片、后台清单合并)共用一份构建期清单,schema 非常简洁:
{
"version": 1,
"generatedAt": "ISO 时间",
"remoteDir": "/apps/mynote",
"files": [{
"basename": "xxx.zip",
"size": 123456,
"fsId": 123456,
"md5": "十六进制串",
"link": "https://pan.baidu.com/s/…?pwd=abcd", // null = 待分享,下次 sync 补齐
"code": "abcd",
"source": "md" | "sync" | "prev",
"updatedAt": "ISO 时间"
}]
}
内容不变时 saveManifest 会跳过写入,避免 CI 每次都产生无意义的 diff commit。优先级:sync 本轮新建 > src/files/*.md frontmatter 已有的 > 上一轮清单里保留的。
4.2 构建期 MD 渲染:remark 插件
Astro 7.x 默认用 Sätteri 处理器,想用 remarkPlugins 必须手动装 @astrojs/markdown-remark(否则配置校验直接报错——这个点第一次 build 才踩雷)。
插件实现关键点:
- 语法正则:
/\{\{(?:netdisk|网盘)\s+([^{}]+?)\s*\}\}/g(中文安全,文件名不能含{}/}); - 只处理
text节点,递归遍历 mdast 时跳过code/inlineCode子树,保证代码块里的示例不被渲染; - 匹配到 basename → 查
netdisk.json生成cardHtml(entry);未匹配生成missingHtml占位并console.warn; - 卡片 HTML 只用
data-*属性,不写内联onclick——复制提取码由 BaseLayout.astro 全局脚本 事件委托.nd-copy,所有文章/笔记/首页自动覆盖。
4.3 后台管理页(/admin/disk/index.html)
独立 HTML + 原生 JS,不受 Decap CMS 10 分钟配置缓存影响,<meta name="robots" content="noindex">。关键设计:
- 登录完全复用 Decap 的 OAuth 握手协议:
window.open(worker/auth),弹窗先向主窗口postMessage('authorizing:github'),主窗口回发同样字符串后弹窗才投递authorization:github:success:{token:...}(含 2 秒超时的兜底)。token 存sessionStorage["disk-admin-token"],和 Decap 的netlify-cms-user隔离。 - CORS 安全:Worker 只放行三个来源——
https://<owner>.github.io、http://localhost:8080(npm run cms本地)、http://127.0.0.1:8787(wrangler dev),非白名单来源 OPTIONS 只返回 204、不带Access-Control-Allow-Origin,浏览器自动拦截。 - Owner 鉴权:除 OPTIONS 外所有
/disk/*先调api.github.com/user(Bearer 传入的 token)比对login或id等于GITHUB_ALLOWED_OWNER,否则 401。避免把 Worker 变成「任何 GitHub 账号都能调百度接口」的代理。 - 上传用 XMLHttpRequest 不用 fetch:fetch 没有上传进度事件,XHR 的
upload.onprogress直接渲染顶部进度条。
4.4 Worker /disk/upload + 内联纯 JS MD5
Worker 运行时 WebCrypto 只提供 SHA 家族,没有 MD5,但 xpan 的 precreate/秒传需要每片 8MB 的 MD5 做 block_list——只能手写一份纯 JS MD5(MD5 是公共算法,不涉及密钥)。在落地前对拍了 0B / 3B / 43B / 55/56/57B(MD5 填充边界) / 64B / 8MB / 8MB+1B 共 9 组输入,与 node:crypto 输出完全一致才合入。
上传大小上限 64MB(Workers 免费档单请求 ~128MB 内存,留足余量),超限直接 413 提示用户改用本地 npm run baidu:sync。
4.5 refresh_token 轮换自愈
openapi.baidu.com/oauth/2.0/token 用 refresh_token 换 access_token 时,会偶尔下发新的 refresh_token 并作废旧的。Worker 没有持久化层无法自己保存,这里采用轻量方案:
- Worker 检测到响应里返回了和 Secret 不一样的新 refresh_token → 响应头加
x-baidu-rotated: 1,同时业务数据正常返回(不让用户的单次操作失败); /admin/disk/顶部隐藏横幅检测到该头 → 显示红色提醒:npx wrangler secret put BAIDU_REFRESH_TOKEN重配;- 本地
baidu-sync.mjs每次换 token 都会写.baidu-token.json,所以本地方案自动兼容。
5. 遇到的主要困难与排查过程
困难 1:qrLogin 扫码一直只拿到 32 位无效 BDUSS
这是整个项目里最费时的坑,前后让你扫了五六次码。
- 表现:扫码完成后
.env里写入的BAIDU_BDUSS总是 32 字符(印象里正常 BDUSS 是 ~190 字符),用它调任何网页端接口都报errno=-6 登录失效。 - 第一次诊断(误以为是 JSONP 解析问题):
qrbdussloginv3 返回类型是text/html的 JSONP 内容,且错误模板里键名用单引号'data': {...},parseLoose直接 JSON.parse 失败 → 回退取cv.v这个 32 位临时值。修了 JSONP 解析 + 加callback参数后,拿到了 192 位的 bduss,但预检仍然是 -6。 - 关键突破:把
qrbdusslogin响应的Set-Cookie头完整 dump 出来——下发了 9 个 cookie(BDUSS/STOKEN/PTOKEN/UBI/PASSID/BDUSS_BFESS/STOKEN_BFESS/PTOKEN_BFESS/UBI_BFESS),而 body JSONP 里那份 192 位 bduss 用在独立请求里,访问disk/main被 302 跳回登录页,说明它根本不是有效的会话值。真正有效的 BDUSS 存在于响应的 Set-Cookie 头。 - 最终修复:
qrLogin改成全程维护 Cookie jar,qrbdusslogin请求带齐历史 cookie(BAIDUID等会话标识),然后从本次响应的 Set-Cookie 读BDUSS/STOKEN存.env。现在写入的 bduss 是 192 位、stoken 64 位。
困难 2:gettemplatevariable 预检「永远」-6
修复 BDUSS 之后,getBdstoken()(/api/gettemplatevariable?fields=["bdstoken"])还是报错 -6,导致分享流程在第一步就被判为「登录失效」。
- 排查:带整套 cookie 访问
disk/main页面,在window.locals里发现isLogin:0, bdstoken:""——服务器眼里我们就是没登录!但明明api/list?dir=/apps/mynote(同样的 BDUSS+STOKEN)能正常列目录、errno=0。 - 决定性实测:直接跳过 getBdstoken 预检,
/share/set里把bdstoken字段传空字符串,发送请求 → errno=0 创建成功,link + pwd 都正确。 - 结论:百度在最近一次升级里对
/share/set?channel=chunlei&clienttype=0&web=1已不再强制校验 bdstoken,而gettemplatevariable接口对「非浏览器真实会话」的 API 调用加了更严的风控。所有路径(本地脚本 + Worker)彻底删除 getBdstoken 这一步,直接用 BDUSS/STOKEN 调 /share/set,errno=-6 才真的提示登录失效。
这两个踩坑的组合花了大半天的扫码才定位清楚。教训:当「预检接口」和「真正业务接口」的鉴权结论不一致时,以业务接口的真实返回为准,不要迷信预检的单一错误码。
困难 3:Astro 7 移除了 markdown-remark
注册 remarkPlugins 的第一版 astro.config.ts 在 build 时报:
markdown.remarkPlugins run on the unified processor from @astrojs/markdown-remark,
which is no longer installed by default now that Sätteri is the default Markdown processor.
额外 npm install @astrojs/markdown-remark 就好。已在 Hard Constraints 里标注,防止以后换环境重踩。
6. 后续建议
- CI 每月一次的定时 baidu:sync——百度的分享链接理论上永久有效,但长期可能因为账号风控被批量失效;在
.github/workflows/deploy.yml加一个每月 1 号的schedulecron,跑npm run baidu:sync后提交 src/files + netdisk.json 变更,可以自动修复任何静默失效(目前是手动或每次 push 触发)。 - Worker 持久化 refresh_token——目前 refresh_token 轮换要人工
wrangler secret put;给 Worker 加一个 KV Namespace(wrangler.toml的kv_namespaces),检测到轮换时await KV.put("BAIDU_REFRESH_TOKEN", newRt),可彻底解放人工。 - /files/ 资料页的客户端 marked 预览现在不会渲染
{{netdisk …}}token(marked 不经过 Astro 的 remark 流程)—— 可在src/pages/files/index.astro的 marked 渲染前,对文本内容先跑一次正则替换,把 token 替换成 nd-card HTML(和 remark-netdisk 渲染规则保持一致)。 - Worker /disk/download 代理目前计划里 v1 没做——大文件会把 Workers 出口流量翻一倍;如果以后想支持「临时直链下载」不走 pan.baidu.com 跳转,要评估一下 Workers R2 作为中转缓存,避免重复拉取。
- 文件名含
{}/}的边界情况——目前正则[^{}]+会截断,遇到再处理即可;文档和管理页提示里已写明这是已知限制。
MyNote