diff --git a/.cnb.yml b/.cnb.yml index bea79763..dddb6634 100644 --- a/.cnb.yml +++ b/.cnb.yml @@ -55,23 +55,15 @@ hugo version - # ── /conf 快照(评论首屏优化)──────────────────────────────────── - # 构建期拉一次 Artalk 的 /conf 落到 data/,Hugo 会内联进每个页面。 - # 前端据此对匿名 /conf 请求本地合成响应 —— Artalk 内部 conf 与评论列表 - # 是严格串行的,省掉这一跳等于首屏少一整个跨境往返。 - # 服务端这份 conf 只由 settings 表决定(唯一因人而异的 imgUpload 由前端 - # 按登录态放行真实请求),所以匿名访客的快照人人相同。 - # 拉取失败一律降级:删文件 → 模板不内联 → 前端自动走原生远程请求。 - # 绝不让它拖垮构建(在 if 条件里失败不会触发 set -e)。 - mkdir -p data - if curl -fsS --max-time 30 --retry 1 --retry-delay 2 \ - "https://api.200181.xyz/api/v2/conf" -o data/artalk_conf.json \ - && python3 -c "import json;json.load(open('data/artalk_conf.json',encoding='utf-8'))"; then - echo "✅ conf 快照就绪 $(wc -c < data/artalk_conf.json | tr -d ' ') 字节" - else - rm -f data/artalk_conf.json - echo "⚠️ conf 快照拉取失败,已降级为运行时远程拉取" - fi + # ── 构建期数据快照(评论 conf / 友链 / 友圈)──────────────────── + # ★ 这里必须用「先 curl 落盘、模板再读本地文件」,**不许**模板里 + # 用 resources.GetRemote:那是「渲染过程中发网络请求」,footer 每页 + # 都要走一遍,构建节点到 Cloudflare 一抖动就会重试到撞穿 Hugo 的 + # partial 30s 超时 → 整站构建失败(2026-10-04 实测白等 14.5 分钟)。 + # 改成快照后最坏只是数据为空,构建永远不挂。 + # 详见 scripts/fetch_snapshots.sh 顶部注释。 + # 脚本内部对每个快照都做了「拉失败就删文件」的降级,且恒 exit 0。 + bash scripts/fetch_snapshots.sh python3 scripts/add_draft_to_hidden.py rm -rf public resources/_gen diff --git a/.gitignore b/.gitignore index 93ef50bc..53bfd4f2 100644 --- a/.gitignore +++ b/.gitignore @@ -15,5 +15,8 @@ node_modules/ .ci_status # CNB 密钥仓库的本地草稿(含真实令牌,仅供往 Web 界面粘贴,绝不入库) cnb-secrets.yml -# 构建期拉取的 Artalk /conf 快照(每次构建现拉,不入库,避免陈旧配置被 git 固化) +# 构建期拉取的数据快照(每次构建现拉,不入库,避免陈旧数据被 git 固化) +# 由 scripts/fetch_snapshots.sh 生成,见 .cnb.yml「Hugo 构建」stage /data/artalk_conf.json +/data/friend_links.json +/data/friend_feeds.json diff --git a/scripts/fetch_snapshots.sh b/scripts/fetch_snapshots.sh new file mode 100644 index 00000000..3d042b44 --- /dev/null +++ b/scripts/fetch_snapshots.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash +# ============================================================================= +# 构建期数据快照 —— 把「模板渲染时必须用、但只能从远端拿」的数据先 curl 成本地文件 +# ============================================================================= +# 为什么必须这么做(2026-10-04 实战教训): +# footer.html 原先用 resources.GetRemote 拉 /api/links 与 /api/feeds。Hugo 的 +# GetRemote 是**在模板渲染过程中同步发请求**的,而 footer 每个页面都要走一遍。 +# 构建节点(国内)到 api.200181.xyz(Cloudflare)一旦抖动: +# WARN Links API fetch failed: dial tcp 104.21.58.236:443: i/o timeout +# WARN Links API fetch failed: ... connection reset by peer +# Artalk 无关,但这两个拉取会一直重试 → 撞穿 Hugo 的 `partial` 30 秒超时 → +# ERROR render of "home" failed: partial "footer.html" timed out after 30s +# Error: error building site → exit code: 1 +# 整站构建失败、站点不更新(实测白等 14.5 分钟)。 +# +# 改成「stage 里 curl 一次 + 模板只读本地文件」后: +# 网络问题最多让某个快照变旧/为空(模板 with 判空 → 渲染空值), +# **永远不会再让构建挂掉**。命中失败时一律删文件,绝不留半截内容。 +# +# 在流水线里由 .cnb.yml 的「Hugo 构建」stage 调用;本地预览想拿到同款数据 +# 也可以直接跑:bash scripts/fetch_snapshots.sh +# +# 产物(均在 .gitignore 里,不入库): +# data/artalk_conf.json 评论 /conf(首屏省一跳跨境往返,见 partials/artalk.html) +# data/friend_links.json 友链 → window.friendLinks(评论区「友链」徽章) +# data/friend_feeds.json 友圈 → window.friendFeeds(评论区「友圈」徽章) +# ============================================================================= + +set -u + +BASE="${RSSAPI_BASE:-https://api.200181.xyz}" +BASE="${BASE%/}" +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +DATA_DIR="$ROOT/data" +mkdir -p "$DATA_DIR" + +# JSON 合法性校验:python3 不一定有(本机 Windows 可能没有),没有就跳过校验 +validate_json() { + command -v python3 >/dev/null 2>&1 || return 0 + python3 -c 'import json,sys;json.load(open(sys.argv[1],encoding="utf-8"))' "$1" 2>/dev/null +} + +# snap <输出文件名> +# 成功:原子替换(先写 .tmp 再 mv),并打印字节数 +# 失败:删除目标文件(含 .tmp),返回非 0 —— 但本脚本最后统一 exit 0, +# 因为「快照拿不到」是可接受的降级,不该阻断构建。 +snap() { + local name="$1" url="$2" out tmp ok=1 + out="$DATA_DIR/$name" + tmp="$out.tmp" + + if curl -fsS --max-time 30 --retry 2 --retry-delay 2 --retry-connrefused \ + -H 'User-Agent: hugo-build-snapshot' "$url" -o "$tmp" 2>/dev/null; then + if [ -s "$tmp" ] && validate_json "$tmp"; then + mv -f "$tmp" "$out" + echo "✅ $name 就绪 $(wc -c < "$out" | tr -d ' ') 字节" + return 0 + fi + echo "⚠️ $name 内容校验失败(空文件或非法 JSON)" + else + echo "⚠️ $name 拉取失败:$url" + fi + + rm -f "$tmp" "$out" + return 1 +} + +echo "── 构建期快照(源:$BASE)──" +snap artalk_conf.json "$BASE/api/v2/conf" || true +snap friend_links.json "$BASE/api/links?all=1" || true +snap friend_feeds.json "$BASE/api/feeds" || true + +exit 0 diff --git a/themes/Ying/assets/js/modules/artalk.js b/themes/Ying/assets/js/modules/artalk.js index 9cf9d877..7d04f865 100644 --- a/themes/Ying/assets/js/modules/artalk.js +++ b/themes/Ying/assets/js/modules/artalk.js @@ -718,6 +718,18 @@ window.initArtalk = function() { saveInitCount(); // 持久化到 sessionStorage var config = Object.assign({}, window.artalkConfig); // Clone config + + // ── 提交前校验(昵称 / 邮箱必填;网址随意)────────────────────────── + // Artalk 的 editor.submit() 实现是: + // conf.beforeSubmit ? conf.beforeSubmit(editor, next) : trigger('editor-submit') + // 也就是官方就留了拦截点 —— 不调 next() 就不会走提交流程。 + // 挂在这份 config 上(而不是内联 -{{/* ====== 3. 远程数据获取 - 构建时 ====== */}} -{{/* Fetch friend data at build time — no runtime network requests */}} +{{/* ====== 3. 友链 / 友圈数据 —— 构建期快照,模板**不发网络请求** ====== */}} +{{/* ★ 这里以前是 resources.GetRemote。它是在「每个页面的模板渲染过程中」同步发请求的, + 而 footer 每页都要走一遍:构建节点(国内)到 api.200181.xyz(Cloudflare)一抖动, + 两个拉取就会重试到撞穿 Hugo 的 partial 30 秒超时 → 整站构建失败、站点不更新 + (2026-10-04 实测:WARN Links API fetch failed ... i/o timeout → + ERROR render of "home" failed: partial "footer.html" timed out after 30s)。 + 现在改为读 data/ 下的本地快照,由流水线 stage 里的 scripts/fetch_snapshots.sh + curl 落盘(见 .cnb.yml「Hugo 构建」)。快照缺失(本地预览 / 拉取失败)时 + with 判空 → 渲染空数组,最坏只是评论区少了「友链 / 友圈」徽章,构建立马过。 */}} {{ $linkHosts := slice }} {{ $linkNames := slice }} -{{ with resources.GetRemote (printf "%s/api/links?all=1" (site.Params.rssapi.base | default "https://api.200181.xyz")) }} - {{ with .Err }} - {{ warnf "Links API fetch failed: %s" . }} - {{ else }} - {{ with .Content | transform.Unmarshal }} - {{ range .links }} - {{ $host := "" }} - {{ with .url | urls.Parse }} - {{ $host = .Host | strings.TrimPrefix "www." }} - {{ end }} - {{ $title := .name | default "" }} - {{ if $host }} - {{ $linkHosts = $linkHosts | append (dict "host" $host "title" $title) }} - {{ end }} - {{ if $title }} - {{ $linkNames = $linkNames | append (dict "lower" (lower $title) "title" $title) }} - {{ end }} - {{ end }} +{{ with site.Data.friend_links }} + {{ range .links }} + {{ $host := "" }} + {{ with .url | urls.Parse }} + {{ $host = .Host | strings.TrimPrefix "www." }} + {{ end }} + {{ $title := .name | default "" }} + {{ if $host }} + {{ $linkHosts = $linkHosts | append (dict "host" $host "title" $title) }} + {{ end }} + {{ if $title }} + {{ $linkNames = $linkNames | append (dict "lower" (lower $title) "title" $title) }} {{ end }} {{ end }} {{ end }} @@ -110,24 +111,18 @@ {{ $feedHosts := slice }} {{ $feedNames := slice }} -{{ with resources.GetRemote (printf "%s/api/feeds" (site.Params.rssapi.base | default "https://api.200181.xyz")) }} - {{ with .Err }} - {{ warnf "Feeds API fetch failed: %s" . }} - {{ else }} - {{ with .Content | transform.Unmarshal }} - {{ range .feeds }} - {{ $host := "" }} - {{ with .url | urls.Parse }} - {{ $host = .Host | strings.TrimPrefix "www." }} - {{ end }} - {{ $title := .feedTitle | default "" }} - {{ if $host }} - {{ $feedHosts = $feedHosts | append (dict "host" $host "title" $title) }} - {{ end }} - {{ if $title }} - {{ $feedNames = $feedNames | append (dict "lower" (lower $title) "title" $title) }} - {{ end }} - {{ end }} +{{ with site.Data.friend_feeds }} + {{ range .feeds }} + {{ $host := "" }} + {{ with .url | urls.Parse }} + {{ $host = .Host | strings.TrimPrefix "www." }} + {{ end }} + {{ $title := .feedTitle | default "" }} + {{ if $host }} + {{ $feedHosts = $feedHosts | append (dict "host" $host "title" $title) }} + {{ end }} + {{ if $title }} + {{ $feedNames = $feedNames | append (dict "lower" (lower $title) "title" $title) }} {{ end }} {{ end }} {{ end }}