fix(comments): 额度故障态提示去冗余 + 修「误恢复」bug + 本地复现开关

服务端 D1 额度用尽时同屏会出现三层重复提示,且故障态会被定期误解除。

- 提示从三层收敛为两层:横幅只留一句极简状态(⏸ 评论暂停中,预计明早 8:00 恢复),
  卡片是唯一详细说明并紧贴编辑器;删掉「点击编辑器再弹一遍 toast」
- 修 bug:恢复判定原来过于宽松,任何 conf/comments/captcha/votes/pages 请求成功都算恢复;
  但 /captcha/status 走 KV 不读 D1,60s 探针恒成功 → 故障态每 60s 被误撤,编辑器看似正常
  实则列表还挂着。现在恢复只认评论列表本身成功,探针改用 GET /comments?limit=1(真读 D1)
- 故障态不再灰化编辑器(opacity+grayscale 观感很差),只禁交互,原因由横幅说明
- 服务端 errors.ts 文案去掉与卡片标题重复的「评论服务暂时不可用」前缀
- 新增本地调试开关:仅 localhost 下 ?atk_svc=<code> 强制进入故障态,便于排查,生产不生效

改动文件:themes/Ying/assets/js/modules/artalk.js、blog-admin/src/lib/errors.ts
(errors.ts 需下次部署 Worker 才在线上生效,前端已做旧文案兼容清洗)
This commit is contained in:
zqlit committed 2026-10-04 18:04:47 +08:00
1 parent 571d552c9d
commit e8c991274a
2 files changed
+57 -23

No files matched your search

+3 -3
View File
@@ -52,7 +52,7 @@ export function friendlyError(e: unknown): FriendlyError {
return {
status: 503,
code: 'd1_quota',
msg: '评论服务暂时不可用:数据库今日读取额度已用尽,北京时间明早 8:00 自动恢复。给你带来不便,可以先看看文章,稍后再来。',
msg: '数据库今日读取额度已用尽,明早 8:00 自动恢复。你可以先看看文章,稍后再来~',
detail,
};
}
@@ -61,7 +61,7 @@ export function friendlyError(e: unknown): FriendlyError {
return {
status: 503,
code: 'd1_unavailable',
msg: '评论服务暂时不可用(数据库连接异常),我们正在处理,请稍后再试。',
msg: '数据库连接异常,我们正在处理,请稍后再试。',
detail,
};
}
@@ -79,7 +79,7 @@ export function friendlyError(e: unknown): FriendlyError {
return {
status: 503,
code: 'unavailable',
msg: '评论服务暂时不可用,请稍后重试。',
msg: '服务或网络有点问题,请稍后重试。',
detail,
};
}
+54 -20
View File
@@ -6,6 +6,39 @@
// (见 artalk-cf/src/lib/errors.ts:d1_quota / d1_unavailable / timeout / ...),
// 所以这里把它换成一句人话 + 可展开的技术详情(留着给站长排查)。
// ============================================================================
// ── 本地调试开关:给 URL 加 ?atk_svc=<code> 强制复现服务故障态 ──────────────
// 例:/comment.html?atk_svc=d1_quota
// 仅 localhost 生效(生产环境这段完全不执行),用来本地看禁用态/错误卡片的观感,
// 不必等真的把 D1 额度跑满。支持的 code 见下面的 INFO 表。
(function () {
var host = location.hostname;
if (host !== 'localhost' && host !== '127.0.0.1' && host !== '') return;
var m = /[?&]atk_svc=([a-z0-9_]+)/.exec(location.search || '');
if (!m) return;
var code = m[1];
var MSG = {
d1_quota: '数据库今日读取额度已用尽,明早 8:00 自动恢复。你可以先看看文章,稍后再来~',
d1_unavailable: '数据库连接异常,我们正在处理,请稍后再试。',
timeout: '评论服务响应超时,请稍后重试。',
unavailable: '服务或网络有点问题,请稍后重试。',
internal: '评论服务出了点小问题,请稍后重试。'
};
if (!MSG[code]) return;
var orig = window.fetch;
window.fetch = function (input, init) {
var url = String(typeof input === 'string' ? input : (input && input.url) || '');
if (/\/api\/v2\/comments(\?|$)/.test(url)) {
return Promise.resolve(new Response(
JSON.stringify({ code: code, msg: MSG[code], detail: '[demo] forced by ?atk_svc=' + code }),
{ status: 503, statusText: 'Service Unavailable', headers: { 'Content-Type': 'application/json; charset=utf-8' } }
));
}
return orig.apply(this, arguments);
};
try { console.info('[artalk] 调试模式:已强制评论接口返回 ' + code); } catch (e) {}
})();
(function () {
if (window.__atkErrGuard) return; // PJAX 会重复执行本文件,只装一次
window.__atkErrGuard = true;
@@ -13,7 +46,7 @@
var last = null; // 最近一次 /api/v2 失败:{ status, code, msg, detail, at }
var INFO = {
d1_quota: { ic: '😴', title: '评论暂时加载不出来', desc: '数据库今日读取额度已用尽,北京时间明早 8:00 自动恢复。你可以先看看文章,稍后再来~' },
d1_quota: { ic: '😴', title: '评论暂停中', desc: '数据库今日读取额度已用尽,明早 8:00 自动恢复。你可以先看看文章,稍后再来~' },
d1_unavailable: { ic: '🛠️', title: '评论服务正在维护', desc: '数据库连接异常,我们已经在处理,请稍后再试。' },
timeout: { ic: '🐢', title: '评论服务响应有点慢', desc: '这次请求超时了,稍后再试一次。' },
unavailable: { ic: '📡', title: '评论服务暂时不可用', desc: '服务或网络有点问题,请稍后再试。' },
@@ -126,9 +159,11 @@
});
} catch (e) { /* clone 不支持就放弃记录,不影响请求 */ }
} else if (res && res.ok) {
// 某些端点不读 D1(/human/* 走 KV),它们 200 不代表服务恢复;
// 只有真正读 D1 的端点成功才算恢复
if (svcDown && /\/(conf|comments|captcha|votes|pages)/.test(url)) setSvcUp();
// 「服务恢复」只认评论列表本身:禁用的直接原因就是列表挂了,
// 列表回来了才算好。votes/pv/captcha 这些端点不读 D1 或只读
// 一行(KV),它们成功≠列表能拉出来 —— 曾据此误判恢复,把
// 置灰和横幅撤了,只剩一张孤零零的错误卡片挂在页面底部。
if (svcDown && /\/comments(\?|$)/.test(url)) setSvcUp();
}
return res;
}).catch(function (e) {
@@ -165,7 +200,8 @@
'.atk-err-retry:active{transform:scale(.96)}' +
'@media (max-width:640px){.atk-error-layer{margin:10px 0 30px!important}.atk-err-card{padding:20px 14px}.atk-err-ic{font-size:26px}' +
'.atk-err-t{font-size:14px}.atk-err-d{font-size:12.5px}}' +
'.atk-svc-down .atk-main-editor{opacity:.55!important;filter:grayscale(1)!important}' +
// 禁用态不改编辑器外观(降透明/灰化整块很难看);只禁交互,
// 为什么点不动由上方横幅说明
'.atk-svc-down .atk-copyright{display:none!important}' +
'.atk-svc-down .atk-main-editor *{pointer-events:none}' +
'.atk-svc-down .atk-human-tf{display:none}' +
@@ -235,20 +271,11 @@
bar = document.createElement('div');
bar.id = 'atk-editor-off-bar';
bar.className = 'atk-editor-off-bar uk-animation-slide-top-small'; // UIKit 入场动画
// 只说一句状态 + 恢复时间:详细解释由错误卡片负责(列表区),这里不重复
bar.innerHTML = code === 'd1_quota'
? '⏸ 评论服务暂时不可用(数据库今日额度已用尽),预计明早 8:00 自动恢复,先看看文章吧'
: '⏸ 评论服务暂时不可用,暂时无法发表评论,请稍后再试';
? '⏸ 评论暂停中,预计明早 8:00 恢复'
: '⏸ 评论暂停中,请稍后再试';
ed.parentNode.insertBefore(bar, ed);
// 编辑器全部子元素已 pointer-events:none,点击会落到编辑器容器本身 ——
// 借这个事件弹 toast 告诉用户为什么点不动(幂等:节点换新才重挂)
if (!ed.dataset.svcGuard) {
ed.dataset.svcGuard = '1';
ed.addEventListener('click', function () {
svcToast(downCode === 'd1_quota'
? '评论服务暂时不可用(额度已用尽),预计明早 8:00 自动恢复'
: '评论服务暂时不可用,请稍后再试');
});
}
}
function removeBar() {
@@ -268,7 +295,10 @@
if (!probeTimer) {
probeTimer = setInterval(function () {
if (!apiBase) return;
fetch(apiBase + '/api/v2/captcha/status').then(function (r) {
// 探针必须真读 D1:/captcha/status 走 KV,额度没恢复它也 200,
// 会把故障态 60s 一轮地误解除。评论列表 limit=1 是最便宜的
// 「真读 D1」请求(rows_read 个位数),它通了才算恢复。
fetch(apiBase + '/api/v2/comments?page_key=' + encodeURIComponent('/') + '&limit=1').then(function (r) {
if (r.ok) setSvcUp();
}).catch(function () { /* 还没恢复 */ });
}, 60000);
@@ -322,9 +352,13 @@
var info = INFO[code] || INFO.internal;
// 服务端文案是给「直接调 API 的人」写的,自带一句"评论服务暂时不可用:",
// 放到卡片里会和标题重复,这里去掉冗余前缀
// 放到卡片里会和标题重复,这里去掉冗余前缀;顺带规整两句别扭的措辞
// (服务端旧文案,下次 Worker 部署后这两条 replace 自然变成空操作)
var desc = fresh && fresh.msg
? fresh.msg.replace(/^评论服务暂时不可用(?:[((][^))]*[))])?[::]\s*/, '')
? fresh.msg
.replace(/^评论服务暂时不可用(?:[((][^))]*[))])?[::,,]\s*/, '')
.replace('北京时间明早', '明早')
.replace(/给你带来不便,可以先看看文章,稍后再来[。~~]?/, '你可以先看看文章,稍后再来~')
: info.desc;
// 服务端故障(非读者自身网络问题)→ 禁用编辑器并挂说明横幅。