跳到主要内容
边缘函数配额已耗尽-切到eop+cnb私有库
博客OneBlog

边缘函数配额已耗尽-切到eop+cnb私有库

7735 字 39 分钟 -- 阅读
最后更新于:
AI 摘要

边缘函数配额已耗尽-切到eop+cnb私有库

2026.9.5-边缘函数配额已耗尽

故障现象

网站报http 523错误了:

来到自己eo边缘函数地方,发现不对劲了,天塌了,说是边缘函数配额已耗尽,完了,丝滑的One站点废了呀。。。

不是呀,这才是月初9.5,怎么额度会没了呢???(是被别人刷了嘛???)

价格对比:

想让我升级vip,是万万不可能的哈哈哈:

解决办法

看来只能上eop了,虽然eop部署有点慢,没办法了。。。

配置备份

当前最新OneBlog版本.cnb.yml代码配置

备份文件位置:.cnb-2026.9.5边缘函数.yml

  1# Hugo-Teek One Blog — CNB 构建
  2# 产物:推送到公开库 dist 分支,由 EdgeOne 边缘函数拉取(不再上传 COS,避免对象存储欠费)
  3# 密钥:https://cnb.cool/onedayxyy/secret/-/blob/main/envs.yml
  4#   钉钉 SECRET / WEBHOOK
  5#   Gitee 备份 GIT_USERNAME / GIT_ACCESS_TOKEN
  6#   后台 EdgeOne EDGEONE_API_TOKEN(及控制台 QINIU_*)
  7#   EO 刷新缓存可用腾讯云 API 密钥(历史变量名 COS_SECRET_ID / COS_SECRET_KEY,不必开通 COS)
  8
  9master:
 10  push:
 11    - runner:
 12        cpus: 64
 13
 14      imports:
 15        - https://cnb.cool/onedayxyy/secret/-/blob/main/envs.yml
 16
 17      docker:
 18        image: docker.cnb.cool/yuwen-gueen/docker-images-chrom/hugo-teek-blog:latest
 19
 20      env:
 21        # 公开产物仓库(EdgeOne 边缘函数读取此库 dist)
 22        TARGET_REPO: "https://cnb.cool/onedayxyy/hugo-teek-public-dist.git"
 23        DIST_BRANCH: dist
 24
 25      stages:
 26        # ------------------------------
 27        # 1️⃣ 记录开始时间
 28        # ------------------------------
 29        - name: set env
 30          script: echo -n $(date +%s)
 31          exports:
 32            info: CUSTOM_ENV_START_TIME
 33
 34        # ------------------------------
 35        # 2️⃣ 同步 CNB 仓库到 Gitee 备份(master 有 push 即触发,含后台写入)
 36        # 密钥库 envs.yml:GIT_USERNAME、GIT_ACCESS_TOKEN(Gitee 私人令牌,需 projects 写权限)
 37        # ------------------------------
 38        - name: 同步仓库到 Gitee 备份
 39          image: tencentcom/git-sync
 40          settings:
 41            branch: master
 42            auth_type: https
 43            username: ${GIT_USERNAME}
 44            password: ${GIT_ACCESS_TOKEN}
 45            target_url: https://gitee.com/onlyonexl/ongblog-gitee-backup.git
 46            push_tags: true
 47            force: true
 48
 49        # ------------------------------
 50        # 3️⃣ 构建 Hugo
 51        # ------------------------------
 52        - name: build
 53          script: |
 54            make build-docker
 55
 56        # ------------------------------
 57        # 4️⃣ 部署到 CNB 公开产物库(替代原 COS sync)
 58        # ------------------------------
 59        - name: 部署到 CNB 公开产物库
 60          script: |
 61            set -e
 62            REPO_URL="${TARGET_REPO}"
 63            BRANCH="${DIST_BRANCH:-dist}"
 64            PUBLIC_DIR="$(pwd)/hugo-teek-site/public"
 65            if [ ! -d "${PUBLIC_DIR}" ]; then
 66              echo "❌ 构建产物不存在: ${PUBLIC_DIR}"
 67              exit 1
 68            fi
 69            DEPLOY_DIR="/workspace/deploy"
 70            rm -rf "${DEPLOY_DIR}"
 71            mkdir -p "${DEPLOY_DIR}"
 72            cd "${DEPLOY_DIR}"
 73            git init -b "${BRANCH}"
 74            git config user.email "ci@cnb.cool"
 75            git config user.name "CNB Deploy"
 76            git remote add origin "${REPO_URL}"
 77            cp -al "${PUBLIC_DIR}/." . 2>/dev/null || cp -a "${PUBLIC_DIR}/." .
 78            git add -A
 79            if git diff --cached --quiet; then
 80              echo "产物无变更,仍强制推送以触发下游拉取"
 81              git commit --allow-empty -m "deploy: $(date +%Y-%m-%dT%H:%M:%S%z) (empty)" --no-gpg-sign
 82            else
 83              git commit -m "deploy: $(date +%Y-%m-%dT%H:%M:%S%z)" --no-gpg-sign
 84            fi
 85            # CNB 上并发 force-push dist 会偶发 CAS 失败(incorrect old value),加重试
 86            PUSH_OK=0
 87            for i in 1 2 3 4 5; do
 88              if git push --force origin "${BRANCH}"; then
 89                PUSH_OK=1
 90                break
 91              fi
 92              echo "⚠️ force-push dist 失败 (attempt ${i}/5),等待后重试…"
 93              sleep $((i * 3))
 94            done
 95            if [ "${PUSH_OK}" != "1" ]; then
 96              echo "❌ 多次 force-push dist 仍失败"
 97              exit 1
 98            fi
 99            echo "✅ 已推送到 ${REPO_URL} (${BRANCH})"
100
101        # ------------------------------
102        # 5️⃣ 后台管理端 → EdgeOne Makers(仅相关目录变更时)
103        # ------------------------------
104        - name: deploy to edgeone makers
105          ifModify:
106            - "oneblog-admin/**"
107            - "admin-frontend/**"
108          image: node:20
109          script: |
110            cd admin-frontend && npm ci && npm run build && cd ..
111            test -f oneblog-admin/public/admin/index.html
112            test -f oneblog-admin/scripts/write-version.mjs
113            echo "==> Build posts archive seed (cold-start fast path)"
114            node oneblog-admin/scripts/build-posts-archive.mjs
115            test -f oneblog-admin/public/data/posts-archive.json
116            echo "==> Write admin version stamp (日期-commit前6位)"
117            node oneblog-admin/scripts/write-version.mjs
118            # EdgeOne Pages 环境变量需在控制台手动配置(CLI 不传递 QINIU_*):
119            # QINIU_AK, QINIU_SK, QINIU_BUCKET, QINIU_DOMAIN — 与 CNB secret envs.yml 一致
120            npx edgeone makers deploy ./oneblog-admin -n oneblog-admin -t $EDGEONE_API_TOKEN
121
122        # ------------------------------
123        # 6️⃣ 刷新 EdgeOne 缓存(腾讯云 API,不依赖 COS 桶)
124        # ------------------------------
125        - name: 🧘‍♂️ 刷新缓存
126          image: docker.cnb.cool/znb/cdn-refresh
127          settings:
128            ak: "${COS_SECRET_ID}"
129            sk: "${COS_SECRET_KEY}"
130            kind: "tencenteo"
131            rtype: "path"
132            domain: "onedayxyy.cn"
133            urls:
134              - "https://onedayxyy.cn/"
135              - "https://onedayxyy.cn/yule/photo/"
136              - "https://onedayxyy.cn/yule/photo/index.html"
137              - "https://oneblogadmin.onedayxyy.cn/"
138              - "https://oneblogadmin.onedayxyy.cn/admin/"
139              - "https://oneblogadmin.onedayxyy.cn/admin/index.html"
140              - "https://oneblogadmin.onedayxyy.cn/data/posts-archive.json"
141
142        # ------------------------------
143        # 7️⃣ 计算耗时
144        # ------------------------------
145        - name: ⏱️ 计算耗时
146          script: |
147            end_time=$(date +%s)
148            duration=$((end_time - $CUSTOM_ENV_START_TIME))
149            minutes=$((duration / 60))
150            seconds=$((duration % 60))
151            echo -n "${minutes}分${seconds}秒"
152          exports:
153            info: CUSTOM_ENV_BUILD_TIME
154
155        # ------------------------------
156        # 8️⃣ 钉钉通知
157        # ------------------------------
158        - name: 钉钉通知
159          image: docker.cnb.cool/yuwen-gueen/docker-images-chrom/tencentcom-dingtalk-bot-msg:latest_amd64
160          settings:
161            content: "「Hugo-Teek One Blog」发布完成(CNB dist),耗时: ${CUSTOM_ENV_BUILD_TIME}"
162            c_type: "text"
163            secret: $SECRET
164            webhook: $WEBHOOK
165            isAtAll: false
166            debug: false
167
168  # 页面「构建&部署」按钮(.cnb/web_trigger.yml)手动触发
169  web_trigger:
170    - runner:
171        cpus: 64
172      imports:
173        - https://cnb.cool/onedayxyy/secret/-/blob/main/envs.yml
174      docker:
175        image: docker.cnb.cool/yuwen-gueen/docker-images-chrom/hugo-teek-blog:latest
176      env:
177        TARGET_REPO: "https://cnb.cool/onedayxyy/hugo-teek-public-dist.git"
178        DIST_BRANCH: dist
179      stages:
180        - name: build
181          script: make build-docker
182        - name: 部署到 CNB 公开产物库
183          script: |
184            set -e
185            REPO_URL="${TARGET_REPO}"
186            BRANCH="${DIST_BRANCH:-dist}"
187            PUBLIC_DIR="$(pwd)/hugo-teek-site/public"
188            test -d "${PUBLIC_DIR}"
189            DEPLOY_DIR="/workspace/deploy"
190            rm -rf "${DEPLOY_DIR}"
191            mkdir -p "${DEPLOY_DIR}"
192            cd "${DEPLOY_DIR}"
193            git init -b "${BRANCH}"
194            git config user.email "ci@cnb.cool"
195            git config user.name "CNB Deploy"
196            git remote add origin "${REPO_URL}"
197            cp -al "${PUBLIC_DIR}/." . 2>/dev/null || cp -a "${PUBLIC_DIR}/." .
198            git add -A
199            git diff --cached --quiet || git commit -m "deploy: $(date +%Y-%m-%dT%H:%M:%S%z)" --no-gpg-sign
200            git push --force --quiet origin "${BRANCH}"
201        - name: 🧘‍♂️ 刷新缓存
202          image: docker.cnb.cool/znb/cdn-refresh
203          settings:
204            ak: "${COS_SECRET_ID}"
205            sk: "${COS_SECRET_KEY}"
206            kind: "tencenteo"
207            rtype: "path"
208            domain: "onedayxyy.cn"
209            urls:
210              - "https://onedayxyy.cn/"
211              - "https://onedayxyy.cn/yule/photo/"
212              - "https://onedayxyy.cn/yule/photo/index.html"
213              - "https://oneblogadmin.onedayxyy.cn/"
214              - "https://oneblogadmin.onedayxyy.cn/admin/"
215              - "https://oneblogadmin.onedayxyy.cn/admin/index.html"
216              - "https://oneblogadmin.onedayxyy.cn/data/posts-archive.json"
217
218  # 后台保存内容:应用文件变更 → commit → push(push 再触发 master 构建)
219  api_trigger_admin_write:
220    - runner:
221        cpus: 64
222      imports:
223        - https://cnb.cool/onedayxyy/secret/-/blob/main/envs.yml
224      docker:
225        image: docker.cnb.cool/yuwen-gueen/docker-images-chrom/hugo-teek-blog:latest
226      stages:
227        - name: apply admin write
228          script: |
229            set -e
230            ACTION="${ADMIN_WRITE_ACTION:-update}"
231            TARGET="${ADMIN_WRITE_PATH:-}"
232            if [ -z "$TARGET" ]; then echo "missing ADMIN_WRITE_PATH"; exit 1; fi
233            if [ "$ACTION" = "delete" ]; then
234              rm -f "$TARGET"
235            else
236              if [ -z "$ADMIN_WRITE_CONTENT_B64" ]; then echo "missing ADMIN_WRITE_CONTENT_B64"; exit 1; fi
237              mkdir -p "$(dirname "$TARGET")"
238              if [ "${ADMIN_WRITE_ENCODING}" = "gzip" ]; then
239                echo "$ADMIN_WRITE_CONTENT_B64" | base64 -d | gunzip > "$TARGET"
240              else
241                echo "$ADMIN_WRITE_CONTENT_B64" | base64 -d > "$TARGET"
242              fi
243            fi
244        - name: commit and push
245          script: |
246            set -e
247            TARGET="${ADMIN_WRITE_PATH:-}"
248            MSG="$(echo "${ADMIN_WRITE_MESSAGE_B64:-}" | base64 -d 2>/dev/null || echo "chore: admin write")"
249            git config user.email "admin@oneblogadmin.onedayxyy.cn"
250            git config user.name "OneBlog Admin"
251            if git status --porcelain -- "$TARGET" | grep -q .; then
252              git add -- "$TARGET"
253              git commit -m "$MSG"
254              git pull --rebase origin master
255              git push origin HEAD:master
256            else
257              echo "No changes to commit for $TARGET"
258            fi
259
260  # 后台「博客升级」:从上游同步主题/后台/脚本 → commit → push(再触发 master 构建)
261  # 不覆盖 user-configuration、content、.env、.cnb.yml
262  api_trigger_blog_upgrade:
263    - runner:
264        cpus: 64
265      imports:
266        - https://cnb.cool/onedayxyy/secret/-/blob/main/envs.yml
267      docker:
268        image: docker.cnb.cool/yuwen-gueen/docker-images-chrom/hugo-teek-blog:latest
269      stages:
270        - name: apply blog upgrade
271          script: |
272            set -e
273            chmod +x scripts/apply-blog-upgrade.sh 2>/dev/null || true
274            if [ ! -f scripts/apply-blog-upgrade.sh ]; then
275              echo "missing scripts/apply-blog-upgrade.sh — please pull latest OneBlog first"
276              exit 1
277            fi
278            bash scripts/apply-blog-upgrade.sh
279        - name: commit and push
280          script: |
281            set -e
282            MSG="chore: blog upgrade from upstream $(date +%Y-%m-%dT%H:%M:%S%z)"
283            git config user.email "upgrade@oneblogadmin.onedayxyy.cn"
284            git config user.name "OneBlog Upgrade"
285            # 明确不把密钥与用户配置整目录误提交
286            git status --porcelain | head -n 80 || true
287            if git status --porcelain | grep -q .; then
288              git add -A
289              # 防御:若误暂存 .env 则撤出
290              git reset HEAD -- .env .env.local 2>/dev/null || true
291              if git diff --cached --quiet; then
292                echo "No staged changes after safeguards"
293              else
294                git commit -m "$MSG"
295                git pull --rebase origin master
296                git push origin HEAD:master
297                echo "✅ Upgrade committed and pushed — master.push will rebuild"
298              fi
299            else
300              echo "Already up to date with upstream (no file changes)"
301            fi
302
303  # 后台手动重建或 api_trigger_rebuild 事件
304  api_trigger_rebuild:
305    - runner:
306        cpus: 64
307      imports:
308        - https://cnb.cool/onedayxyy/secret/-/blob/main/envs.yml
309      docker:
310        image: docker.cnb.cool/yuwen-gueen/docker-images-chrom/hugo-teek-blog:latest
311      env:
312        TARGET_REPO: "https://cnb.cool/onedayxyy/hugo-teek-public-dist.git"
313        DIST_BRANCH: dist
314      stages:
315        - name: build
316          script: make build-docker
317        - name: 部署到 CNB 公开产物库
318          script: |
319            set -e
320            REPO_URL="${TARGET_REPO}"
321            BRANCH="${DIST_BRANCH:-dist}"
322            PUBLIC_DIR="$(pwd)/hugo-teek-site/public"
323            test -d "${PUBLIC_DIR}"
324            DEPLOY_DIR="/workspace/deploy"
325            rm -rf "${DEPLOY_DIR}"
326            mkdir -p "${DEPLOY_DIR}"
327            cd "${DEPLOY_DIR}"
328            git init -b "${BRANCH}"
329            git config user.email "ci@cnb.cool"
330            git config user.name "CNB Deploy"
331            git remote add origin "${REPO_URL}"
332            cp -al "${PUBLIC_DIR}/." . 2>/dev/null || cp -a "${PUBLIC_DIR}/." .
333            git add -A
334            git diff --cached --quiet || git commit -m "deploy: $(date +%Y-%m-%dT%H:%M:%S%z)" --no-gpg-sign
335            git push --force --quiet origin "${BRANCH}"
336        - name: 🧘‍♂️ 刷新缓存
337          image: docker.cnb.cool/znb/cdn-refresh
338          settings:
339            ak: "${COS_SECRET_ID}"
340            sk: "${COS_SECRET_KEY}"
341            kind: "tencenteo"
342            rtype: "path"
343            domain: "onedayxyy.cn"
344            urls:
345              - "https://onedayxyy.cn/"
346              - "https://onedayxyy.cn/yule/photo/"
347              - "https://onedayxyy.cn/yule/photo/index.html"
348              - "https://oneblogadmin.onedayxyy.cn/"
349              - "https://oneblogadmin.onedayxyy.cn/admin/"
350              - "https://oneblogadmin.onedayxyy.cn/admin/index.html"
351              - "https://oneblogadmin.onedayxyy.cn/data/posts-archive.json"
352
353include:
354  - .cnb/vscode.yml

当前最新OneBlog版本边缘函数代码配置

  1/**
  2 * EdgeOne 边缘函数:从 CNB 公开产物库拉取静态站
  3 *
  4 * 用法:复制到腾讯云 EdgeOne 控制台 → 站点 → 边缘函数(整文件覆盖)
  5 *
  6 * 相对旧版改进:
  7 * 1. HTML/CSS/JS 等文本资源做 gzip(缓冲后再压,避免流式 CompressionStream 在 EO 静默失败)
  8 * 2. 指纹静态资源长缓存;HTML 边缘缓存(s-maxage + stale-while-revalidate)
  9 * 3. 响应头 X-EO-Compress 便于确认压缩是否生效
 10 * 4. 找不到页面时回退到产物库中的 404.html(Hugo 自定义 404)
 11 * 5. 维护模式:轮询 Admin 公开状态;开启时对所有请求返回 503 维护页
 12 */
 13addEventListener('fetch', (event) => {
 14  event.respondWith(dispatch(event.request));
 15});
 16
 17const BASE = 'https://cnb.cool/onedayxyy/hugo-teek-public-dist/-/git/raw/dist';
 18const MAINTENANCE_STATUS_URL = 'https://oneblogadmin.onedayxyy.cn/api/maintenance-status';
 19const MAINTENANCE_CACHE_TTL_MS = 5000;
 20const MAINTENANCE_FETCH_TIMEOUT_MS = 2500;
 21
 22async function dispatch(request) {
 23  const maintResult = await getMaintenanceStatus();
 24  const pathname = new URL(request.url).pathname;
 25  // 维护模式下仍放行站点图标,避免标签页变成空白文档图标
 26  if (maintResult.data && maintResult.data.enabled && !isFaviconPath(pathname)) {
 27    return buildMaintenanceResponse(maintResult.data, maintResult.source);
 28  }
 29  const response = await handleRequest(request);
 30  return withMaintProbe(response, maintResult);
 31}
 32
 33async function handleRequest(request) {
 34  const url = new URL(request.url);
 35  let pathname = url.pathname;
 36
 37  // RSS 订阅:对外统一 /feed,产物为 feed.xml
 38  if (pathname === '/feed' || pathname === '/feed/') {
 39    const feedResp = await fetchOrigin(`${BASE}/feed.xml`);
 40    if (feedResp.ok && feedResp.body) {
 41      let rawBuf;
 42      try {
 43        rawBuf = await feedResp.arrayBuffer();
 44      } catch (e) {
 45        const headers = new Headers();
 46        headers.set('Content-Type', 'application/rss+xml; charset=utf-8');
 47        headers.set('Cache-Control', 'public, max-age=600, s-maxage=3600, stale-while-revalidate=86400');
 48        headers.set('X-EO-Compress', 'passthrough-read-fail');
 49        const retry = await fetchOrigin(`${BASE}/feed.xml`);
 50        return new Response(retry.body, { status: retry.status, headers });
 51      }
 52      const contentType = 'application/rss+xml; charset=utf-8';
 53      const headers = new Headers();
 54      headers.set('Content-Type', contentType);
 55      headers.set('Cache-Control', 'public, max-age=600, s-maxage=3600, stale-while-revalidate=86400');
 56      headers.set('Content-Length', String(rawBuf.byteLength));
 57      const uncompressed = new Response(rawBuf, { status: 200, headers });
 58      return maybeGzip(request, uncompressed, 'feed.xml', rawBuf, contentType);
 59    }
 60  }
 61
 62  if (pathname !== '/' && !pathname.endsWith('/') && !isFile(pathname)) {
 63    const redirectUrl = new URL(request.url);
 64    redirectUrl.pathname = pathname + '/';
 65    return Response.redirect(redirectUrl.toString(), 301);
 66  }
 67
 68  let uri = pathname;
 69  if (uri.startsWith('/')) uri = uri.slice(1);
 70  if (!uri || uri === '') uri = 'index.html';
 71  if (uri.endsWith('/')) uri = uri + 'index.html';
 72
 73  let target = `${BASE}/${uri}`;
 74  let resp = await fetchOrigin(target);
 75
 76  if (!resp.ok && !isFile(uri)) {
 77    const indexUri = uri.endsWith('/') ? uri + 'index.html' : uri + '/index.html';
 78    target = `${BASE}/${indexUri}`;
 79    resp = await fetchOrigin(target);
 80    if (resp.ok) uri = indexUri;
 81  }
 82
 83  if (!resp.ok || !resp.body) {
 84    // 回退到 Hugo 构建的自定义 404 页,避免纯文本 "Page Not Found"
 85    const notFound = await fetchOrigin(`${BASE}/404.html`);
 86    if (notFound.ok && notFound.body) {
 87      const rawBuf = await notFound.arrayBuffer();
 88      const contentType = 'text/html; charset=utf-8';
 89      const headers = new Headers();
 90      headers.set('Content-Type', contentType);
 91      headers.set('Cache-Control', 'public, max-age=60, must-revalidate');
 92      headers.set('Content-Length', String(rawBuf.byteLength));
 93      const uncompressed = new Response(rawBuf, {
 94        status: 404,
 95        headers,
 96      });
 97      return maybeGzip(request, uncompressed, '404.html', rawBuf, contentType);
 98    }
 99
100    return new Response('Page Not Found', {
101      status: 404,
102      headers: { 'Content-Type': 'text/html; charset=utf-8' },
103    });
104  }
105
106  const contentType =
107    getContentType(uri) || resp.headers.get('content-type') || 'text/html; charset=utf-8';
108
109  // 大体积 CSS/JS(如 main.css ~1MB)若整包 arrayBuffer + gzip,EO 易直接 545。
110  // 指纹静态资源改为直通,交给平台智能压缩。
111  const originLen = Number(resp.headers.get('content-length') || 0);
112  if (shouldPassthrough(uri, contentType, originLen)) {
113    const headers = new Headers();
114    headers.set('Content-Type', contentType);
115    headers.set('Cache-Control', cacheControlFor(uri));
116    if (originLen > 0) headers.set('Content-Length', String(originLen));
117    headers.set('X-EO-Compress', 'passthrough');
118    return new Response(resp.body, { status: resp.status, headers });
119  }
120
121  // 先读入缓冲:EO 上对流式 pipeThrough(CompressionStream) 经常不生效
122  let rawBuf;
123  try {
124    rawBuf = await resp.arrayBuffer();
125  } catch (e) {
126    const headers = new Headers();
127    headers.set('Content-Type', contentType);
128    headers.set('Cache-Control', cacheControlFor(uri));
129    headers.set('X-EO-Compress', 'passthrough-read-fail');
130    const retry = await fetchOrigin(target);
131    return new Response(retry.body, { status: retry.status, headers });
132  }
133
134  // 超大 CSS/JS 跳过边缘 gzip(易 OOM/545);HTML 文本可压到更大体积(长文 ~500KB)
135  const isHtml = /text\/html/i.test(contentType) || /\.html?(?:$|\?)/i.test(uri);
136  const maxBytes = isHtml ? HTML_GZIP_MAX_BYTES : PASS_THROUGH_MAX_BYTES;
137  if (rawBuf.byteLength > maxBytes) {
138    const headers = new Headers();
139    headers.set('Content-Type', contentType);
140    headers.set('Cache-Control', cacheControlFor(uri));
141    headers.set('Content-Length', String(rawBuf.byteLength));
142    headers.set('X-EO-Compress', 'skip-large');
143    return new Response(rawBuf, { status: resp.status, headers });
144  }
145
146  const headers = new Headers();
147  headers.set('Content-Type', contentType);
148  headers.set('Cache-Control', cacheControlFor(uri));
149  headers.set('Content-Length', String(rawBuf.byteLength));
150
151  const uncompressed = new Response(rawBuf, {
152    status: resp.status,
153    headers,
154  });
155
156  return maybeGzip(request, uncompressed, uri, rawBuf, contentType);
157}
158
159/** 超过该体积不再做边缘 gzip 缓冲(main.css 约 1MB) */
160const PASS_THROUGH_MAX_BYTES = 400 * 1024;
161/** 长文 HTML 常 >400KB;gzip 后通常 <120KB,仍应压缩(否则 Lighthouse 文本压缩/性能崩) */
162const HTML_GZIP_MAX_BYTES = 1536 * 1024;
163
164function shouldPassthrough(uri, contentType, contentLength) {
165  const isHtml = /text\/html/i.test(contentType) || /\.html?(?:$|\?)/i.test(uri);
166  // HTML 再大也走缓冲 gzip,勿直通
167  if (isHtml) return false;
168  if (contentLength > PASS_THROUGH_MAX_BYTES) return true;
169  // 指纹 CSS/JS 体积常偏大,优先直通
170  if (/\.min\.[a-f0-9]{8,}\.(?:css|js)(?:$|\?)/i.test(uri)) {
171    if (contentLength === 0) return true; // 未知长度时也不冒险缓冲
172    if (contentLength > 200 * 1024) return true;
173  }
174  return false;
175}
176
177function fetchOrigin(target) {
178  return fetch(target, {
179    headers: {
180      'User-Agent': 'curl/8.0.0',
181      Accept: '*/*',
182      'Accept-Encoding': 'identity',
183    },
184  });
185}
186
187function isFile(path) {
188  return /\.[^/]+$/.test(path);
189}
190
191function isFaviconPath(pathname) {
192  const p = String(pathname || '').toLowerCase();
193  return (
194    p === '/favicon.ico' ||
195    p === '/favicon.png' ||
196    p === '/favicon.svg' ||
197    p === '/apple-touch-icon.png' ||
198    p === '/apple-touch-icon-precomposed.png' ||
199    p.endsWith('/favicon.ico')
200  );
201}
202
203function getContentType(uri) {
204  const map = {
205    html: 'text/html; charset=utf-8',
206    css: 'text/css; charset=utf-8',
207    js: 'application/javascript; charset=utf-8',
208    mjs: 'application/javascript; charset=utf-8',
209    json: 'application/json; charset=utf-8',
210    png: 'image/png',
211    jpg: 'image/jpeg',
212    jpeg: 'image/jpeg',
213    gif: 'image/gif',
214    svg: 'image/svg+xml',
215    webp: 'image/webp',
216    avif: 'image/avif',
217    ico: 'image/x-icon',
218    woff: 'font/woff',
219    woff2: 'font/woff2',
220    txt: 'text/plain; charset=utf-8',
221    xml: 'application/xml; charset=utf-8',
222    map: 'application/json',
223  };
224  if (!isFile(uri)) return 'text/html; charset=utf-8';
225  const lower = String(uri || '').toLowerCase();
226  if (lower === 'feed.xml' || lower.endsWith('/feed.xml')) {
227    return 'application/rss+xml; charset=utf-8';
228  }
229  const ext = uri.split('.').pop().toLowerCase();
230  return map[ext];
231}
232
233function cacheControlFor(uri) {
234  const u = String(uri || '').toLowerCase();
235
236  if (/\.(?:css|js|mjs|woff2?|png|jpe?g|gif|webp|avif|svg|ico)(?:$|\?)/i.test(uri)) {
237    return 'public, max-age=31536000, immutable';
238  }
239
240  if (/\.json(?:$|\?)/i.test(uri)) {
241    return 'public, max-age=300, s-maxage=3600, stale-while-revalidate=86400';
242  }
243
244  if (/\.xml(?:$|\?)/i.test(uri)) {
245    return 'public, max-age=600, s-maxage=3600, stale-while-revalidate=86400';
246  }
247
248  if (u === '404.html') {
249    return 'public, max-age=60, must-revalidate';
250  }
251
252  // 首页:浏览器短缓存 + 边缘中等缓存(新文章发布后较快刷新)
253  if (u === 'index.html' || u === '') {
254    return 'public, max-age=60, s-maxage=1800, stale-while-revalidate=3600';
255  }
256
257  // 文章 / 列表 / 文档等 HTML:边缘长缓存,浏览器 5 分钟
258  if (/\.html(?:$|\?)/i.test(uri) || !isFile(uri)) {
259    return 'public, max-age=300, s-maxage=86400, stale-while-revalidate=86400';
260  }
261
262  return 'public, max-age=60, must-revalidate';
263}
264
265function isCompressible(uri, contentType) {
266  if (/\.(?:png|jpe?g|gif|webp|avif|woff2?|mp4|webm|gz|br)(?:$|\?)/i.test(uri)) {
267    return false;
268  }
269  const type = contentType || '';
270  return /text\/|javascript|json|xml|svg/.test(type);
271}
272
273async function compressGzip(rawBuf) {
274  if (typeof CompressionStream === 'undefined') {
275    return { ok: false, reason: 'no-compression-stream' };
276  }
277  try {
278    const cs = new CompressionStream('gzip');
279    const writer = cs.writable.getWriter();
280    await writer.write(new Uint8Array(rawBuf));
281    await writer.close();
282    const compressed = await new Response(cs.readable).arrayBuffer();
283    if (!compressed || compressed.byteLength < 2) {
284      return { ok: false, reason: 'empty-output' };
285    }
286    // gzip magic 1F 8B
287    const u8 = new Uint8Array(compressed);
288    if (u8[0] !== 0x1f || u8[1] !== 0x8b) {
289      return { ok: false, reason: 'bad-magic' };
290    }
291    return { ok: true, buf: compressed };
292  } catch (e) {
293    return { ok: false, reason: 'err:' + (e && e.message ? String(e.message).slice(0, 80) : 'unknown') };
294  }
295}
296
297async function maybeGzip(request, response, uri, rawBuf, contentType) {
298  const accept = (request.headers.get('Accept-Encoding') || '').toLowerCase();
299  // EdgeOne 常会剥掉/改写传入边缘函数的 Accept-Encoding,导致误判 skip-client。
300  // 现代浏览器都支持 gzip:仅当明确只要 identity 时才跳过。
301  const identityOnly =
302    accept === 'identity' ||
303    (/identity/.test(accept) && !/gzip|\*|br/.test(accept));
304  if (identityOnly) {
305    const h = new Headers(response.headers);
306    h.set('X-EO-Compress', 'skip-identity');
307    return new Response(rawBuf, { status: response.status, headers: h });
308  }
309  if (!isCompressible(uri, contentType)) {
310    const h = new Headers(response.headers);
311    h.set('X-EO-Compress', 'skip-type');
312    return new Response(rawBuf, { status: response.status, headers: h });
313  }
314
315  const result = await compressGzip(rawBuf);
316  if (!result.ok) {
317    const h = new Headers(response.headers);
318    h.set('X-EO-Compress', result.reason || 'fail');
319    // 不设 Vary,尽量让 EO「智能压缩」有机会接管
320    return new Response(rawBuf, { status: response.status, headers: h });
321  }
322
323  const headers = new Headers(response.headers);
324  headers.set('Content-Encoding', 'gzip');
325  headers.set('Content-Length', String(result.buf.byteLength));
326  headers.set('Vary', 'Accept-Encoding');
327  headers.set(
328    'X-EO-Compress',
329    'gzip-ok;ae=' + (accept ? accept.slice(0, 40) : 'missing')
330  );
331  return new Response(result.buf, { status: response.status, headers });
332}
333
334// ==================== 站点维护模式 ====================
335// fail-open:状态接口失败/超时则照常提供站点,避免 Admin 故障锁死主站。
336// 仅用内存短缓存;不用 Cache API(EdgeOne 上 max-age 可能不失效,会把旧的 enabled:false 粘住)。
337
338let maintenanceMemoryCache = { at: 0, result: null };
339
340/**
341 * @returns {{ data: object|null, source: string }}
342 * source: memory | fetch | http-<code> | timeout | error | empty
343 */
344async function getMaintenanceStatus() {
345  const now = Date.now();
346  if (
347    maintenanceMemoryCache.result &&
348    now - maintenanceMemoryCache.at < MAINTENANCE_CACHE_TTL_MS
349  ) {
350    return {
351      data: maintenanceMemoryCache.result.data,
352      source: 'memory',
353    };
354  }
355
356  // 不用 AbortController:部分 EO 运行时对 signal 支持不完整,改为 Promise.race 超时
357  let timer = null;
358  let fetchPromise = null;
359  try {
360    fetchPromise = fetch(MAINTENANCE_STATUS_URL, {
361      method: 'GET',
362      headers: {
363        Accept: 'application/json',
364        'Cache-Control': 'no-cache',
365        Pragma: 'no-cache',
366      },
367      redirect: 'follow',
368    });
369    const timeoutPromise = new Promise((_, reject) => {
370      timer = setTimeout(
371        () => reject(new Error('maintenance-status-timeout')),
372        MAINTENANCE_FETCH_TIMEOUT_MS
373      );
374    });
375    const resp = await Promise.race([fetchPromise, timeoutPromise]);
376    if (!resp || !resp.ok) {
377      const result = {
378        data: null,
379        source: resp ? `http-${resp.status}` : 'empty',
380      };
381      maintenanceMemoryCache = { at: now, result };
382      return result;
383    }
384    const data = await resp.json();
385    const result = { data, source: 'fetch' };
386    maintenanceMemoryCache = { at: now, result };
387    return result;
388  } catch (err) {
389    const msg = err && err.message ? String(err.message) : '';
390    const result = {
391      data: null,
392      source: msg.indexOf('timeout') >= 0 ? 'timeout' : 'error',
393    };
394    maintenanceMemoryCache = { at: now, result };
395    return result;
396  } finally {
397    if (timer) clearTimeout(timer);
398    // 超时获胜时避免未处理的 fetch rejection
399    if (fetchPromise) fetchPromise.catch(() => {});
400  }
401}
402
403/** 给普通响应打诊断头。必须原地改 headers,禁止 new Response(body) 重建:
404 *  EdgeOne 重建带 Content-Encoding:gzip 的响应对 body 可能已是明文,浏览器会白屏。 */
405function withMaintProbe(response, maintResult) {
406  const enabled = !!(maintResult && maintResult.data && maintResult.data.enabled);
407  const source = (maintResult && maintResult.source) || 'unknown';
408  let probe = 'off';
409  if (source === 'fetch' || source === 'memory') {
410    probe = enabled ? 'on' : 'off';
411  } else {
412    probe = `fail:${source}`;
413  }
414  try {
415    response.headers.set('X-OneBlog-Maint', probe);
416    response.headers.set('X-OneBlog-Maint-Src', source);
417    return response;
418  } catch (_) {
419    // 极端情况下 headers 只读:宁可不打诊断头,也不重建 body(避免 gzip 错乱)
420    return response;
421  }
422}
423
424function escapeHtml(str) {
425  return String(str || '')
426    .replace(/&/g, '&amp;')
427    .replace(/</g, '&lt;')
428    .replace(/>/g, '&gt;')
429    .replace(/"/g, '&quot;')
430    .replace(/'/g, '&#39;');
431}
432
433function buildMaintenanceHtml(status) {
434  const title = escapeHtml(status.title || '站点维护中');
435  const message = escapeHtml(status.message || '正在升级,马上回来。');
436  const until = status.until ? escapeHtml(status.until) : '';
437  const untilBlock = until
438    ? `<p class="until"><span>预计恢复</span><strong>${until}</strong></p>`
439    : '';
440
441  return `<!DOCTYPE html>
442<html lang="zh-CN">
443<head>
444  <meta charset="utf-8">
445  <meta name="viewport" content="width=device-width, initial-scale=1">
446  <meta name="robots" content="noindex">
447  <title>${title} · One Blog</title>
448  <link rel="icon" type="image/webp" href="https://oneimg.onedayxyy.cn/images/site-icons/xyy-logo_58fed615.avif?w=150&h=150&fit=crop&fm=webp&q=80">
449  <link rel="apple-touch-icon" href="https://oneimg.onedayxyy.cn/images/site-icons/xyy-logo_58fed615.avif?w=180&h=180&fit=crop&fm=webp&q=80">
450  <link rel="preconnect" href="https://fonts.googleapis.com">
451  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
452  <link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,600;9..144,700&family=Noto+Sans+SC:wght@400;500;700&display=swap" rel="stylesheet">
453  <style>
454    :root {
455      --ink: #1a2e2a;
456      --muted: #5a6f68;
457      --accent: #2f8f73;
458      --accent-soft: #7bc4a8;
459      --paper: #f6f3eb;
460      --glow: #d8efe4;
461    }
462    * { box-sizing: border-box; }
463    html, body { height: 100%; margin: 0; }
464    body {
465      min-height: 100%;
466      display: grid;
467      place-items: center;
468      padding: 28px 20px;
469      color: var(--ink);
470      font-family: "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;
471      background:
472        radial-gradient(ellipse 80% 55% at 18% 12%, #dff3ea 0%, transparent 55%),
473        radial-gradient(ellipse 70% 50% at 88% 88%, #efe4c8 0%, transparent 50%),
474        radial-gradient(ellipse 50% 40% at 70% 20%, #cfe8de 0%, transparent 45%),
475        linear-gradient(165deg, #eef6f2 0%, var(--paper) 48%, #ebe4d6 100%);
476      overflow: hidden;
477      position: relative;
478    }
479    .blob {
480      position: absolute;
481      border-radius: 50%;
482      filter: blur(48px);
483      opacity: .55;
484      pointer-events: none;
485      animation: drift 14s ease-in-out infinite;
486    }
487    .blob-a {
488      width: 340px; height: 340px;
489      left: -80px; top: -40px;
490      background: #9fd9c2;
491    }
492    .blob-b {
493      width: 280px; height: 280px;
494      right: -60px; bottom: -30px;
495      background: #e8d5a3;
496      animation-delay: -5s;
497      animation-duration: 18s;
498    }
499    .blob-c {
500      width: 180px; height: 180px;
501      left: 42%; top: 62%;
502      background: #b7e0d2;
503      animation-delay: -9s;
504      animation-duration: 16s;
505    }
506    .grain {
507      position: absolute;
508      inset: 0;
509      pointer-events: none;
510      opacity: .04;
511      background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
512    }
513    .stage {
514      position: relative;
515      z-index: 1;
516      width: min(520px, 100%);
517      text-align: center;
518      animation: enter .8s cubic-bezier(.22,1,.36,1) both;
519    }
520    .visual {
521      width: 168px;
522      height: 168px;
523      margin: 0 auto 28px;
524      position: relative;
525      animation: float 5.5s ease-in-out infinite;
526    }
527    .visual svg { width: 100%; height: 100%; display: block; filter: drop-shadow(0 18px 28px rgba(47,143,115,.18)); }
528    .ring {
529      position: absolute;
530      inset: -10px;
531      border-radius: 50%;
532      border: 1.5px dashed rgba(47,143,115,.28);
533      animation: spin 28s linear infinite;
534    }
535    .brand {
536      font-family: Fraunces, "Noto Serif SC", Georgia, serif;
537      font-size: clamp(34px, 7vw, 48px);
538      font-weight: 700;
539      letter-spacing: -.02em;
540      line-height: 1;
541      margin: 0 0 8px;
542      color: var(--ink);
543    }
544    .brand em {
545      font-style: normal;
546      background: linear-gradient(120deg, var(--accent), #4aa888 45%, #c4a35a);
547      -webkit-background-clip: text;
548      background-clip: text;
549      color: transparent;
550    }
551    .domain {
552      margin: 0 0 28px;
553      font-size: 13px;
554      letter-spacing: .16em;
555      text-transform: uppercase;
556      color: var(--muted);
557      font-weight: 500;
558    }
559    h1 {
560      margin: 0 0 12px;
561      font-size: clamp(24px, 4.5vw, 32px);
562      font-weight: 700;
563      letter-spacing: .02em;
564    }
565    .message {
566      margin: 0 auto;
567      max-width: 34em;
568      font-size: 16px;
569      line-height: 1.75;
570      color: var(--muted);
571      white-space: pre-wrap;
572      font-weight: 400;
573    }
574    .progress {
575      width: min(220px, 70%);
576      height: 4px;
577      margin: 28px auto 0;
578      border-radius: 999px;
579      background: rgba(47,143,115,.12);
580      overflow: hidden;
581    }
582    .progress > i {
583      display: block;
584      height: 100%;
585      width: 42%;
586      border-radius: inherit;
587      background: linear-gradient(90deg, var(--accent-soft), var(--accent), #c4a35a);
588      animation: slide 2.4s ease-in-out infinite;
589    }
590    .until {
591      display: inline-flex;
592      align-items: center;
593      gap: 10px;
594      margin: 22px 0 0;
595      padding: 8px 14px;
596      border-radius: 999px;
597      background: rgba(255,255,255,.55);
598      border: 1px solid rgba(47,143,115,.16);
599      font-size: 13px;
600      color: var(--muted);
601      backdrop-filter: blur(8px);
602    }
603    .until span { opacity: .75; }
604    .until strong { color: var(--ink); font-weight: 600; }
605    .hint {
606      margin: 26px 0 0;
607      font-size: 12px;
608      color: rgba(90,111,104,.72);
609    }
610    @keyframes enter {
611      from { opacity: 0; transform: translateY(18px) scale(.98); }
612      to { opacity: 1; transform: none; }
613    }
614    @keyframes float {
615      0%, 100% { transform: translateY(0); }
616      50% { transform: translateY(-10px); }
617    }
618    @keyframes drift {
619      0%, 100% { transform: translate(0, 0) scale(1); }
620      50% { transform: translate(24px, -18px) scale(1.08); }
621    }
622    @keyframes spin { to { transform: rotate(360deg); } }
623    @keyframes slide {
624      0% { transform: translateX(-120%); }
625      100% { transform: translateX(280%); }
626    }
627    @media (prefers-reduced-motion: reduce) {
628      .blob, .visual, .ring, .progress > i, .stage { animation: none !important; }
629    }
630  </style>
631</head>
632<body>
633  <div class="blob blob-a" aria-hidden="true"></div>
634  <div class="blob blob-b" aria-hidden="true"></div>
635  <div class="blob blob-c" aria-hidden="true"></div>
636  <div class="grain" aria-hidden="true"></div>
637  <main class="stage">
638    <div class="visual" aria-hidden="true">
639      <div class="ring"></div>
640      <svg viewBox="0 0 168 168" fill="none" xmlns="http://www.w3.org/2000/svg">
641        <defs>
642          <linearGradient id="g1" x1="28" y1="24" x2="140" y2="148" gradientUnits="userSpaceOnUse">
643            <stop stop-color="#E8F7F1"/>
644            <stop offset="1" stop-color="#B7E0D2"/>
645          </linearGradient>
646          <linearGradient id="g2" x1="56" y1="62" x2="118" y2="118" gradientUnits="userSpaceOnUse">
647            <stop stop-color="#2F8F73"/>
648            <stop offset="1" stop-color="#C4A35A"/>
649          </linearGradient>
650        </defs>
651        <circle cx="84" cy="84" r="72" fill="url(#g1)"/>
652        <circle cx="84" cy="84" r="54" fill="#F8FBF9" fill-opacity=".72"/>
653        <path d="M58 92c8-18 22-28 36-28s28 10 36 28" stroke="url(#g2)" stroke-width="5" stroke-linecap="round"/>
654        <path d="M70 78c4-8 10-12 14-12s10 4 14 12" stroke="#2F8F73" stroke-width="4" stroke-linecap="round" opacity=".55"/>
655        <circle cx="84" cy="104" r="7" fill="#2F8F73"/>
656        <path d="M84 111v18" stroke="#2F8F73" stroke-width="4" stroke-linecap="round"/>
657        <path d="M74 126h20" stroke="#C4A35A" stroke-width="4" stroke-linecap="round"/>
658        <circle cx="118" cy="58" r="5" fill="#C4A35A" opacity=".8"/>
659        <circle cx="52" cy="64" r="3.5" fill="#2F8F73" opacity=".45"/>
660      </svg>
661    </div>
662    <p class="brand">One <em>Blog</em></p>
663    <p class="domain">onedayxyy.cn</p>
664    <h1>${title}</h1>
665    <p class="message">${message}</p>
666    <div class="progress" aria-hidden="true"><i></i></div>
667    ${untilBlock}
668    <p class="hint">若仍看到旧页面,请强制刷新或使用无痕窗口</p>
669  </main>
670</body>
671</html>`;
672}
673
674function buildMaintenanceResponse(status, source) {
675  const html = buildMaintenanceHtml(status);
676  return new Response(html, {
677    status: 503,
678    headers: {
679      'Content-Type': 'text/html; charset=utf-8',
680      'Cache-Control': 'no-store, no-cache, must-revalidate',
681      Pragma: 'no-cache',
682      'Retry-After': '300',
683      'X-OneBlog-Maintenance': '1',
684      'X-OneBlog-Maint': 'on',
685      'X-OneBlog-Maint-Src': source || 'fetch',
686    },
687  });
688}

ai评估

1现在有个问题:我当前OneBlog部署是通过eo边缘函数+cnb私有库来部署的,具体看.cnb.yml文件,但是最近发现eo边缘函数直接报边缘函数配额已耗尽,难搞,于是我想把这个部署方式直接切换到 cnb私有库+eop方式,这个是不是会影响我当前博客功能呀,我记得好多功能都是依赖于边缘函数里的一些代码的,请评估下。

结论:切到 CNB + EOP 不会废掉博客主体功能;真正会丢的,主要是「主站边缘函数」里那几段运行时逻辑,不是后台那套 API。

先分清两套东西,别混在一起:

部分现状配额问题
主站 onedayxyy.cnEO 边缘函数 从 CNB 公开库 dist 拉静态页就是现在耗尽、523 的那套
后台 oneblogadmin.onedayxyy.cn已经是 edgeone makers deploy(Pages/Makers + Cloud Functions)另一套,和这次主站配额无关

后台写文、重建、升级、七牛、评论 API、友链申请等,都不跑在主站那个边缘函数里;切 EOP 不会动它们


边缘函数里到底干了啥(edgeone/blog-cnb-origin.js

它本质上是「动态反代 + 几条增强」,不是业务后端:

  1. 从 CNB raw dist 拉 HTML/CSS/JS
  2. gzip / Cache-Control
  3. /feedfeed.xml
  4. 尾斜杠 301、目录 → index.html
  5. 自定义 404.html
  6. 站点维护模式(轮询 Admin 的 /api/maintenance-status,开了就整站 503)

Hugo 文章、主题、相册、文档等内容本身是静态产物,EOP 直接托管即可。


影响评估

类别功能切 EOP 后
不受影响文章/文档/主题静态页、后台 SPA、CNB 触发写库/重建/升级、七牛图床、评论/友链申请(走 Admin API)正常
会坏一键站点维护(后台开关还在,主站不再拦截)需用 EOP middleware / 薄边缘函数补回,或暂时不用
会降级/feed 订阅地址(产物是 feed.xml);尾斜杠/404 细节;边缘 gzip/缓存精细策略_redirects / 平台智能压缩 / 已有 _headers 可兜大部分

仓库里 hugo-teek-site/static/_headers 本来就是给 Pages 用的,缓存策略已有备份;维护模式和 /feed 别名才是缺口。


部署形态也要改

现在是:CNB 构建 → 推公开库 dist边缘函数去拉

切 EOP 后通常是:CNB 构建 → 直接把 public 部署到 EOP(或让 EOP 绑公开库 dist),域名改指 Pages 源站,并关掉/解绑当前那个会吃配额的边缘函数。
只改域名、不改发布链路的话,站点还是起不来。


建议

  1. 可以切:主站改 EOP 静态托管,后台保持现状。
  2. 必须补:CI 改成部署到 EOP,不再依赖「边缘函数拉 CNB」。
  3. 按需补:维护模式(middleware)、/feedfeed.xml 重定向。
  4. 心理预期:EOP 发布比边缘拉源慢一点,功能上够用。

如果你要动手,我可以直接改 .cnb.yml(备份已有 .cnb-2026.9.5边缘函数.yml),并补上 _redirects / 维护模式的最小替代方案。


开始切换

已完成的仓库改动(2026.9.5)

  1. .cnb.yml:构建后 edgeone makers deploy ./oneblog-site -n oneblog,不再推 hugo-teek-public-dist
  2. 备份.cnb-2026.9.5边缘函数.yml(旧 CNB dist + 边缘函数方案)
  3. oneblog-site/:EOP 项目根(edgeone.json + middleware.js 维护模式 / /feed
  4. hugo-teek-site/static/_redirects/feed/feed.xml
  5. 说明edgeone/README.md

你需要在腾讯云控制台做的(代码推上去之后)

  1. 等 CNB master 流水线跑通,确认 Pages 项目 oneblog 有新部署
  2. Pages → oneblog绑定域名 onedayxyy.cn
  3. EdgeOne 站点 Zone → 停用/删除 原先「拉 CNB dist」的边缘函数(否则继续吃配额)
  4. 打开 Pages 智能压缩
  5. 无痕打开首页;看响应头 X-OneBlog-Maint;访问 /feed 应拿到 RSS

主要截图

验证清单

  • https://onedayxyy.cn/ 可打开(非 523)
  • /feed 返回 RSS
  • 后台开维护 → 主站 503;关掉后恢复
  • 后台发文 → CNB 构建 → EOP 更新

哈哈哈,可以正常访问啦,nice:

边缘函数配额耗尽原因

来自@w3c的回答:

 1边缘函数怎么还会超配额呀,这才月初[捂脸]
 2
 3因为你没配置isr swr
 4
 5[捂脸][捂脸][捂脸]  我是hugo,好像不支持isr啥的吧
 6那你应该不消耗函数配额
 7
 8hugo也不走函数
 9是别的什么东西被人刷了吧
10这就是isr swr的重要性了
11有了isr swr直接走cdn不走函数
12
13也不一定是被人刷了 你没优化好 消耗量大
14别人一次请求能完成的事情 你需要请求10次
15别人1ms能完成的事情 你 要几十秒
16
17[捂脸][捂脸]难搞,先切到eop先用着
18后期用了astro的话,再上isr swr
19
20看到那张截图了
21果然是没优化好 是时间超额了
22请求次数都没怎么消耗
23请求次数还剩那么多 运行时间居然没了
24证明你单次请求所需时间非常久
25
26还和时间有关系,太专业了[捂脸][捂脸],难搞

结论先说:不是请求次数被刷爆,而是「CPU 时间」配额先打满了。 截图里请求约 66.6 万/300 万(还剩很多),CPU 已是 300 万毫秒/300 万毫秒,所以服务停了。

直接原因

免费档边缘函数有两项独立限额,任一打满就会停

指标用量状态
请求次数~22%还够
CPU 时间100%已耗尽

说明:流量不算特别大,但单次请求的 CPU 开销偏高,所以月初就把时间额度用光了。

为什么单次那么贵

主站当时的架构是:访客 → Zone 边缘函数 → 再去拉 CNB dist 静态文件。几乎每次进函数都会干这些事:

  1. 维护模式探测:先请求 Admin 的 /api/maintenance-status(内存缓存只有 5 秒,且按 isolate,经常打不中)
  2. 回源 CNB raw:再 fetch 一次远端 HTML/资源
  3. 缓冲 + gziparrayBuffer 后再 CompressionStream 压缩(CPU 很吃)
  4. 失败还可能多拉几次:目录回退、404.html 等,一次页面可能触发多次远端请求

粗算:300万 ms ÷ 66.6万次 ≈ 每次约 4.5 ms CPU。对「反代 + 压缩」不算夸张,但免费额度只有 300 万毫秒/月,正常浏览几天就容易见底。

和「被刷」的关系

更像是架构没优化好,而不是典型刷量

  • 若被狂刷,通常请求次数也会一起飙
  • 现在是「次数还剩很多、时间没了」→ 典型是每次进函数太重,且大量请求仍进函数,而不是长期只走 CDN 缓存

边缘函数若没真正把大部分流量用边缘缓存打住,Hugo 静态站也会被当成「每次都跑函数的动态站」来计费。

一句话总结

根因:用边缘函数当整站静态反代(维护探测 + 拉 CNB + gzip),单次 CPU 贵、免费 CPU 额度又极小,所以月初 CPU 先耗尽;请求次数配额其实还很宽裕。

你们后来切到 EOP 静态托管,就是为了让主站不再走这套会吃 CPU 配额的边缘函数。

分析eop部署耗时原因

1但是eop每次部署都要2min多,这个时间能优化下嘛?

eop部署耗时:(优化前)2.9min

能抠一点,但很难再压成「几秒级」——你这边慢,主要不是 Hugo 构建,而是 往 EOP 上传的体积太大

瓶颈在哪

本地产物大致是:

  • 约 2281 个文件 / ~657 MB
  • 大头在 HTML:topics ~306MB、linux ~145MB、docs ~102MB(不少页面本身就 1~4MB)

当前流水线是:拷到 oneblog-site/publicnpx edgeone@latest makers deploy(每次还要下 CLI)→ 平台打包分发。
文件又多又大时,上传 + 平台处理 很容易就到 2 分钟;这块有一部分是平台固定开销,改不掉。

能优化的(按收益)

手段预期说明
先打 ZIP 再 deploy通常最明显官方也建议文件多时用 ZIP;HTML 压缩率高,上传体积常能少一大截
tencentcom/deploy-eopages 镜像省 20~40s预装 CLI,去掉每次 npx edgeone@latest
大资源外置看内容media/yuzhou.mp4、大封面图走七牛,别打进 Pages 包
HTML 体积治理中长期单页 MB 级说明页内嵌了太多东西,长期最该治

现实预期

  • 做完 ZIP + 预装 CLI:有机会从 2min+ 降到约 1~1.5min(视当日网速和产物而定)
  • 很难稳定压到 30s 内:EOP 全量上传 + 边缘分发本身就有底噪
  • 以前边缘函数「秒级可见」,是因为只推公开库、边缘去拉,部署快、运行时吃配额;EOP 是反过来

如果你要,我可以直接改 .cnb.yml:部署阶段改成「打 ZIP + deploy-eopages 镜像」,并顺带把明显不该进站的大文件排除出去。

直接改

已改完并推到 master,CNB 会自动跑一轮新流水线。

改动要点:

  • 新增 scripts/deploy-oneblog-eop.sh:拷贝产物 → 打 ZIP → edgeone makers deploy
  • 三处主站部署(master / web_trigger / rebuild)改用 tencentcom/deploy-eopages:latest,不再每次 npx edgeone@latest
  • 日志会打印:文件数、原体积、ZIP 体积、部署耗时

在 CNB 看本次「deploy blog to edgeone pages」步骤即可对比:ZIP 若压到几十~一百多 MB,上传一般会明显快于原先 ~657MB 逐文件。


问题:咋越优化越慢了。。。

确实是 ZIP 拖慢了:对约 650MB HTML 做压缩,往往比上传本身还久,所以从 ~2min 变成了 3.1min

已改回目录直传,只保留 deploy-eopages 预装 CLI(省掉每次 npx)。已推送,这轮应回到接近原来的 2min 量级。

坦白说:产物这么大时,EOP 全量上传很难再明显砍时间;要想再快,得减产物体积(大 HTML / 本地媒体外置),而不是再折腾打包方式。


eop部署耗时:(优化后)3min(嚓,没优化多少。。算了,不)

最新文章

文档导航