快捷菜单
常用功能一站直达
更多功能请点顶栏「快捷菜单」
2026.9.5-边缘函数配额已耗尽
网站报http 523错误了:

来到自己eo边缘函数地方,发现不对劲了,天塌了,说是边缘函数配额已耗尽,完了,丝滑的One站点废了呀。。。
不是呀,这才是月初9.5,怎么额度会没了呢???(是被别人刷了嘛???)



价格对比:
想让我升级vip,是万万不可能的哈哈哈:

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

备份文件位置:.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 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, '&')
427 .replace(/</g, '<')
428 .replace(/>/g, '>')
429 .replace(/"/g, '"')
430 .replace(/'/g, ''');
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}1现在有个问题:我当前OneBlog部署是通过eo边缘函数+cnb私有库来部署的,具体看.cnb.yml文件,但是最近发现eo边缘函数直接报边缘函数配额已耗尽,难搞,于是我想把这个部署方式直接切换到 cnb私有库+eop方式,这个是不是会影响我当前博客功能呀,我记得好多功能都是依赖于边缘函数里的一些代码的,请评估下。结论:切到 CNB + EOP 不会废掉博客主体功能;真正会丢的,主要是「主站边缘函数」里那几段运行时逻辑,不是后台那套 API。
先分清两套东西,别混在一起:
| 部分 | 现状 | 配额问题 |
|---|---|---|
主站 onedayxyy.cn | EO 边缘函数 从 CNB 公开库 dist 拉静态页 | 就是现在耗尽、523 的那套 |
后台 oneblogadmin.onedayxyy.cn | 已经是 edgeone makers deploy(Pages/Makers + Cloud Functions) | 另一套,和这次主站配额无关 |
后台写文、重建、升级、七牛、评论 API、友链申请等,都不跑在主站那个边缘函数里;切 EOP 不会动它们。
edgeone/blog-cnb-origin.js)它本质上是「动态反代 + 几条增强」,不是业务后端:
dist 拉 HTML/CSS/JS/feed → feed.xmlindex.html404.html/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 源站,并关掉/解绑当前那个会吃配额的边缘函数。
只改域名、不改发布链路的话,站点还是起不来。
/feed → feed.xml 重定向。如果你要动手,我可以直接改 .cnb.yml(备份已有 .cnb-2026.9.5边缘函数.yml),并补上 _redirects / 维护模式的最小替代方案。
.cnb.yml:构建后 edgeone makers deploy ./oneblog-site -n oneblog,不再推 hugo-teek-public-dist.cnb-2026.9.5边缘函数.yml(旧 CNB dist + 边缘函数方案)oneblog-site/:EOP 项目根(edgeone.json + middleware.js 维护模式 / /feed)hugo-teek-site/static/_redirects:/feed → /feed.xmledgeone/README.mdmaster 流水线跑通,确认 Pages 项目 oneblog 有新部署oneblog → 绑定域名 onedayxyy.cnX-OneBlog-Maint;访问 /feed 应拿到 RSS

https://onedayxyy.cn/ 可打开(非 523)/feed 返回 RSS哈哈哈,可以正常访问啦,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 静态文件。几乎每次进函数都会干这些事:
/api/maintenance-status(内存缓存只有 5 秒,且按 isolate,经常打不中)fetch 一次远端 HTML/资源arrayBuffer 后再 CompressionStream 压缩(CPU 很吃)404.html 等,一次页面可能触发多次远端请求粗算:300万 ms ÷ 66.6万次 ≈ 每次约 4.5 ms CPU。对「反代 + 压缩」不算夸张,但免费额度只有 300 万毫秒/月,正常浏览几天就容易见底。
更像是架构没优化好,而不是典型刷量:
边缘函数若没真正把大部分流量用边缘缓存打住,Hugo 静态站也会被当成「每次都跑函数的动态站」来计费。
根因:用边缘函数当整站静态反代(维护探测 + 拉 CNB + gzip),单次 CPU 贵、免费 CPU 额度又极小,所以月初 CPU 先耗尽;请求次数配额其实还很宽裕。
你们后来切到 EOP 静态托管,就是为了让主站不再走这套会吃 CPU 配额的边缘函数。
1但是eop每次部署都要2min多,这个时间能优化下嘛?eop部署耗时:(优化前)2.9min

能抠一点,但很难再压成「几秒级」——你这边慢,主要不是 Hugo 构建,而是 往 EOP 上传的体积太大。
本地产物大致是:
topics ~306MB、linux ~145MB、docs ~102MB(不少页面本身就 1~4MB)当前流水线是:拷到 oneblog-site/public → npx 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 级说明页内嵌了太多东西,长期最该治 |
如果你要,我可以直接改 .cnb.yml:部署阶段改成「打 ZIP + deploy-eopages 镜像」,并顺带把明显不该进站的大文件排除出去。
已改完并推到 master,CNB 会自动跑一轮新流水线。
改动要点:
scripts/deploy-oneblog-eop.sh:拷贝产物 → 打 ZIP → edgeone makers deploymaster / web_trigger / rebuild)改用 tencentcom/deploy-eopages:latest,不再每次 npx edgeone@latest在 CNB 看本次「deploy blog to edgeone pages」步骤即可对比:ZIP 若压到几十~一百多 MB,上传一般会明显快于原先 ~657MB 逐文件。
问题:咋越优化越慢了。。。
确实是 ZIP 拖慢了:对约 650MB HTML 做压缩,往往比上传本身还久,所以从 ~2min 变成了 3.1min。
已改回目录直传,只保留 deploy-eopages 预装 CLI(省掉每次 npx)。已推送,这轮应回到接近原来的 2min 量级。
坦白说:产物这么大时,EOP 全量上传很难再明显砍时间;要想再快,得减产物体积(大 HTML / 本地媒体外置),而不是再折腾打包方式。
eop部署耗时:(优化后)3min(嚓,没优化多少。。算了,不)

精选 · 友链 · 更多

One的公众号
爱折腾博客的小白