我完全是在手机上面弄的。所有各种东西都很难弄。所以就搞了一些脚本(实际上是大肥鱼做)

mdtohtml

#!/data/data/com.termux/files/usr/bin/bash

# ========== 按你的实际路径修改这三行 ==========
MD_DIR="/storage/emulated/0/我的文件/开发/博客md"
TPL="/storage/emulated/0/我的文件/开发/脚本/mdtohtml_/post.html"
OUT_DIR="/storage/emulated/0/我的文件/开发/博客/posts"
# ==============================================

# ---------- 可选参数 ----------
#   --no-index  只转换,不刷新首页列表
#   --force     即使目标 html 比 md 新(疑似手改过)也覆盖
no_index=0
force=0
_args=()
for _a in "$@"; do
  case "$_a" in
    --no-index) no_index=1 ;;
    --force)    force=1 ;;
    *) _args+=("$_a") ;;
  esac
done
set -- "${_args[@]}"

if [ -z "$1" ]; then
  echo "用法: mdtohtml [--no-index] [--force] <文章名> [输出目录](不带 .md 后缀)"
  echo "示例: mdtohtml 未命名"
  echo "      mdtohtml --no-index 未命名   # 只转换,不刷新首页列表"
  echo "      mdtohtml --force 未命名      # 覆盖比 md 新的 html(谨慎)"
  echo "      mdtohtml 未命名 /storage/emulated/0/我的文件/开发/博客/posts/draft"
  exit 1
fi

# ---------- 清洗参数:去掉可能带上的各种后缀 ----------
name="$1"
name="${name%.md}"
name="${name%.html}"
name="${name%.md}"      # 防 .md.html 这种夹心

if [ -z "$name" ]; then
  echo "❌ 文章名为空(清洗后),请检查参数: $1"
  exit 1
fi

# ---------- 输出目录:第二个参数,留空则用默认 ----------
if [ -n "$2" ]; then
  out_dir="$2"
else
  out_dir="$OUT_DIR"
fi

src="$MD_DIR/$name.md"
dst="$out_dir/$name.html"

# ---------- 检查源文件和模板 ----------
if [ ! -f "$src" ]; then
  echo "❌ 找不到源文件: $src"
  exit 1
fi

if [ ! -f "$TPL" ]; then
  echo "❌ 找不到模板: $TPL"
  exit 1
fi

mkdir -p "$out_dir"

# ---------- 转换 ----------
# 守卫:目标 html 比源 md 新,说明生成后又被手改过,默认不覆盖
if [ "$force" -eq 0 ] && [ -f "$dst" ] && [ "$dst" -nt "$src" ]; then
  echo "⚠️  $dst"
  echo "    比源文件更新(疑似手改过),已跳过转换,保留你的修改。"
  echo "    确实要从 md 重新生成,请加 --force:mdtohtml --force $name"
else
  if ! pandoc --standalone \
    --template="$TPL" \
    --output="$dst" \
    "$src"; then
    echo "❌ pandoc 转换失败,未生成: $dst"
    exit 1
  fi
  echo "✅ 已生成: $dst"
fi

# ---------- 自动刷新首页文章列表 ----------
# 脚本放在 /sdcard 上,FUSE 会忽略可执行位,所以统一用 bash 调用。
GEN_INDEX="/storage/emulated/0/我的文件/开发/脚本/gen-index"
if [ "$no_index" -eq 1 ]; then
  echo "ℹ️  已跳过首页列表刷新(--no-index)"
elif [ -f "$GEN_INDEX" ]; then
  echo
  bash "$GEN_INDEX" || echo "⚠️  首页列表刷新失败,可手动重跑: bash $GEN_INDEX"
else
  echo "⚠️  找不到 $GEN_INDEX,跳过首页列表刷新"
fi

gen-index

#!/data/data/com.termux/files/usr/bin/bash
# ==========================================================================
# gen-index —— 根据 posts/*.html 自动重建首页文章列表
#
# 用法:
#   gen-index            # 用默认站点目录
#   gen-index <站点目录>  # 指定站点目录
#
# 做的事:
#   1. 扫描 <站点>/posts/*.html
#   2. 从每篇提取:
#        <h1 class="article-title">标题</h1>
#        <time datetime="YYYY-MM-DD">
#        <meta name="description" content="摘要">
#   3. 按日期倒序,重写 index.html 中
#        <!-- POSTS:START --> ... <!-- POSTS:END -->
#      之间的 <ul class="post-list"> 区块
#
# 设计要点:
#   - 全量重建、幂等,重复运行结果一致
#   - 缺元数据的文章自动回退(标题=文件名,日期=文件 mtime),并给出警告
#   - 改 index.html 前先备份为 index.html.bak,再原子替换
# ==========================================================================

set -u

DEFAULT_SITE="/storage/emulated/0/我的文件/开发/博客"
SITE_DIR="${1:-$DEFAULT_SITE}"
POSTS_DIR="$SITE_DIR/posts"
INDEX="$SITE_DIR/index.html"

START_MARK="POSTS:START"
END_MARK="POSTS:END"

info() { printf '%s\n' "$*"; }
warn() { printf '⚠️  %s\n' "$*" >&2; }
err()  { printf '❌ %s\n' "$*" >&2; exit 1; }

# ---------- 前置检查 ----------
[ -d "$POSTS_DIR" ] || err "找不到文章目录: $POSTS_DIR"
[ -f "$INDEX" ]     || err "找不到首页: $INDEX"
grep -q -- "$START_MARK" "$INDEX" || err "index.html 缺少 $START_MARK 标记,请先手工加上"
grep -q -- "$END_MARK"   "$INDEX" || err "index.html 缺少 $END_MARK 标记,请先手工加上"

# ---------- 工具函数 ----------

# 仅用于 href 属性里的文件名:只需处理 & 和 "
attr_escape() {
  printf '%s' "$1" | sed 's/&/\&amp;/g; s/"/\&quot;/g'
}

trim() {
  printf '%s' "$1" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//'
}

# 把整篇 HTML 压成一行,跨行属性、缩进都不再影响提取
flatten() {
  tr '\n\r\t' '   ' < "$1" | tr -s ' '
}

# 剥掉标签,只留文本(&amp; 这类实体原样保留,正好可以再放进 HTML)
strip_tags() {
  printf '%s' "$1" | sed 's|<[^>]*>||g'
}

# 标题:优先 <h1 class="article-title">(允许内嵌 <code>/<em> 等标签),
# 没有就用 <title> 去掉站点后缀
extract_title() {
  local flat t
  flat="$(flatten "$1")"
  t="$(printf '%s' "$flat" | sed -n 's|.*<h1[^>]*article-title[^>]*>\(.*\)</h1>.*|\1|p' | head -n1)"
  if [ -z "$t" ]; then
    t="$(printf '%s' "$flat" | sed -n 's|.*<title>\(.*\)</title>.*|\1|p' | head -n1)"
    t="${t%% · *}"
  fi
  trim "$(strip_tags "$t")"
}

# 日期:优先 <time datetime="...">,其次 <time> 文本;只返回 YYYY-MM-DD
extract_date() {
  local flat d
  flat="$(flatten "$1")"
  d="$(printf '%s' "$flat" | sed -n 's|.*<time[^>]*datetime="\([^"]*\)".*|\1|p' | head -n1)"
  if [ -z "$d" ]; then
    d="$(printf '%s' "$flat" | sed -n 's|.*<time[^>]*>\([^<]*\)</time>.*|\1|p' | head -n1)"
    d="$(strip_tags "$d" | tr -d '"')"
  fi
  d="$(trim "$d")"
  if printf '%s' "$d" | grep -Eq '^[0-9]{4}-[0-9]{2}-[0-9]{2}$'; then
    printf '%s' "$d"
  else
    printf ''
  fi
}

# 摘要:<meta name="description" content="...">,兼容属性换序、跨行书写
extract_desc() {
  local flat d
  flat="$(flatten "$1")"
  if printf '%s' "$flat" | grep -q 'name="description"[^>]*content="'; then
    d="$(printf '%s' "$flat" | sed -e 's|.*name="description"[^>]*content="||' -e 's|".*||')"
  elif printf '%s' "$flat" | grep -q 'content="[^"]*"[^>]*name="description"'; then
    d="$(printf '%s' "$flat" | sed -e 's|.*<meta[^>]*content="||' -e 's|".*||')"
  else
    d=""
  fi
  trim "$d"
}

# 文件修改日期,作为日期的兜底
mtime_date() {
  local f="$1" d
  d="$(stat -c %y "$f" 2>/dev/null | cut -d' ' -f1)"
  if ! printf '%s' "$d" | grep -Eq '^[0-9]{4}-[0-9]{2}-[0-9]{2}$'; then
    d="$(date -r "$f" +%Y-%m-%d 2>/dev/null)"
  fi
  printf '%s' "$d"
}

# ---------- 收集元数据 ----------
rows=""
count=0
warned=0

for f in "$POSTS_DIR"/*.html; do
  [ -e "$f" ] || continue
  base="$(basename "$f" .html)"

  title="$(extract_title "$f")"
  date="$(extract_date "$f")"
  desc="$(extract_desc "$f")"

  if [ -z "$title" ]; then
    title="$base"
    warn "「$base」缺少 article-title,标题回退为文件名"
    warned=$((warned + 1))
  fi
  if [ -z "$date" ]; then
    date="$(mtime_date "$f")"
    warn "「$base」缺少有效日期,改用文件修改时间 $date"
    warned=$((warned + 1))
  fi

  rows="${rows}${date}"$'\t'"${base}"$'\t'"${title}"$'\t'"${desc}"$'\n'
  count=$((count + 1))
done

if [ "$count" -eq 0 ]; then
  warn "posts/ 下没有 .html 文件,将生成空列表"
fi

# ---------- 排序:日期倒序,同日期按文件名倒序 ----------
sorted="$(printf '%s' "$rows" | grep -v '^[[:space:]]*$' | LC_ALL=C sort -t $'\t' -k1,1r -k2,2r)"

# ---------- 生成 <ul> 区块 ----------
block="$(mktemp "${INDEX}.block.XXXXXX")" || err "无法创建临时文件"
new_index="$(mktemp "${INDEX}.new.XXXXXX")" || { rm -f "$block"; err "无法创建临时文件"; }
trap 'rm -f "$block" "$new_index"' EXIT

{
  printf '\t\t<ul class="post-list">\n'
  while IFS=$'\t' read -r d b t ds; do
    [ -n "$d" ] || continue
    printf '\t\t  <li>\n'
    printf '\t\t\t<a class="post-card" href="posts/%s.html">\n' "$(attr_escape "$b")"
    printf '\t\t\t  <h2>%s</h2>\n' "$t"
    if [ -n "$ds" ]; then
      printf '\t\t\t  <p>%s</p>\n' "$ds"
    fi
    printf '\t\t\t  <time datetime="%s">%s</time>\n' "$d" "$d"
    printf '\t\t\t</a>\n'
    printf '\t\t  </li>\n'
  done <<< "$sorted"
  printf '\t\t</ul>\n'
} > "$block"

# ---------- 注入 index.html ----------
awk -v blockfile="$block" '
  index($0, "POSTS:START") {
    print
    while ((getline line < blockfile) > 0) print line
    close(blockfile)
    skip = 1
    next
  }
  index($0, "POSTS:END") { skip = 0 }
  skip { next }
  { print }
' "$INDEX" > "$new_index"

# ---------- 校验后再替换 ----------
[ -s "$new_index" ] || err "生成的 index.html 为空,已放弃写入"
grep -q -- "$END_MARK" "$new_index" || err "生成结果缺少 $END_MARK 标记,已放弃写入"
if [ "$count" -gt 0 ] && ! grep -q 'post-card' "$new_index"; then
  err "生成结果里没有文章卡片,已放弃写入"
fi

cp -p "$INDEX" "$INDEX.bak" || err "备份 index.html 失败"
mv -f "$new_index" "$INDEX" || err "替换 index.html 失败"

# ---------- 报告 ----------
info "✅ 已更新首页列表:$count 篇文章"
while IFS=$'\t' read -r d b t ds; do
  [ -n "$d" ] || continue
  info "   $d  $t"
done <<< "$sorted"

if [ "$warned" -gt 0 ]; then
  info "(有 $warned 项元数据缺失,见上方警告)"
fi

info "   首页: $INDEX"
info "   备份: $INDEX.bak"

exit 0

push-blog.sh

#!/data/data/com.termux/files/usr/bin/bash
# ============================================================
# push-blog.sh  v1.0
# 把博客源目录镜像到 git 仓库,自动生成提交信息并推送。
# 适用场景:Cloudflare Pages 直连 GitHub 仓库,push 即部署。
#
# 退出码:0 成功 / 1 环境问题 / 2 远端或推送失败 / 3 无变更
# ============================================================
set -euo pipefail

# 凭据失效时立刻报错,而不是卡在交互式的用户名/密码提示上
export GIT_TERMINAL_PROMPT=0
export GIT_ASKPASS=/bin/true

PROG="push-blog"
VERSION="1.0"

# ---------- 1. 默认值(配置文件可覆盖,命令行再覆盖) ----------
BLOG_SRC="/sdcard/我的文件/开发/博客"
BLOG_REPO="$HOME/My-blog"
BLOG_BRANCH="main"
BLOG_REMOTE="origin"
TITLE_SUFFIX=" · 我道呀博客"
LOG_FILE="${XDG_STATE_HOME:-$HOME/.local/state}/push-blog.log"
CONFIG_FILE="${PUSH_BLOG_CONFIG:-${XDG_CONFIG_HOME:-$HOME/.config}/push-blog/config}"

DRY_RUN=0
NO_PUSH=0
VERBOSE=0
QUIET=0
CUSTOM_MSG=""
EXTRA_EXCLUDES=()

# 仓库独有文件:源目录里没有,但绝不能因为 --delete 被删掉
PROTECTED_EXCLUDES=(
  ".git"
  "_headers" "_redirects" ".github"
  "CNAME" ".nojekyll" "wrangler.toml"
  ".gitignore" ".gitattributes" "LICENSE"
)
# 临时垃圾
JUNK_EXCLUDES=(
  ".DS_Store" "Thumbs.db" ".directory" ".obsidian" ".trash"
  "*.swp" "*.swo" "*~" "*.tmp" "*.bak"
)

# ---------- 2. 配置与参数 ----------
if [ -f "$CONFIG_FILE" ]; then
  . "$CONFIG_FILE"
fi

usage() {
  cat <<'EOF'
用法: push-blog [选项]

  裸跑即:把 BLOG_SRC 同步到 BLOG_REPO,生成提交信息并推送到远端分支。

选项:
  -s, --src <dir>       源目录           (默认取配置 BLOG_SRC)
  -r, --repo <dir>      仓库目录         (默认取配置 BLOG_REPO)
  -b, --branch <name>   推送分支         (默认 main)
  -m, --message <text>  手写提交信息,跳过自动生成
  -e, --exclude <pat>   追加 rsync 排除规则,可重复
  -n, --dry-run         预演:只看会改/删什么,不提交不推送
      --no-push         只提交到本地,不推送
  -v, --verbose         详细输出
  -q, --quiet           安静模式
  -h, --help            显示帮助
  -V, --version         显示版本

退出码: 0 成功 / 1 环境问题 / 2 推送失败 / 3 无变更
EOF
}

while [ $# -gt 0 ]; do
  case "$1" in
    -s|--src)     BLOG_SRC="${2:?--src 需要目录参数}"; shift 2 ;;
    -r|--repo)    BLOG_REPO="${2:?--repo 需要目录参数}"; shift 2 ;;
    -b|--branch)  BLOG_BRANCH="${2:?--branch 需要分支名}"; shift 2 ;;
    -m|--message) CUSTOM_MSG="${2:?--message 需要文本}"; shift 2 ;;
    -e|--exclude) EXTRA_EXCLUDES+=("${2:?--exclude 需要模式}"); shift 2 ;;
    -n|--dry-run) DRY_RUN=1; shift ;;
    --no-push)    NO_PUSH=1; shift ;;
    -v|--verbose) VERBOSE=1; shift ;;
    -q|--quiet)   QUIET=1; shift ;;
    -h|--help)    usage; exit 0 ;;
    -V|--version) printf '%s %s\n' "$PROG" "$VERSION"; exit 0 ;;
    *) printf '未知参数: %s\n\n' "$1" >&2; usage >&2; exit 1 ;;
  esac
done

BLOG_SRC="${BLOG_SRC%/}"
BLOG_REPO="${BLOG_REPO%/}"

# ---------- 3. 输出与日志 ----------
ts() { date '+%Y-%m-%d %H:%M:%S'; }

log()  { if [ "$QUIET" -eq 1 ]; then return 0; fi; printf '%s\n' "$*"; }
vlog() { if [ "$VERBOSE" -eq 0 ]; then return 0; fi; printf '[%s] %s\n' "$(ts)" "$*"; }
err()  { printf '%s\n' "$*" >&2; }

write_log() {
  [ -n "${LOG_FILE:-}" ] || return 0
  mkdir -p "$(dirname "$LOG_FILE")" 2>/dev/null || true
  printf '%s %s\n' "$(ts)" "$*" >>"$LOG_FILE" 2>/dev/null || true
}

notify() {
  local title="$1" content="$2"
  if command -v termux-notification >/dev/null 2>&1; then
    termux-notification --title "$title" --content "$content" >/dev/null 2>&1 || true
  fi
}

die() {
  local code="$1"; shift
  err "✗ $*"
  write_log "FAIL($code): $*"
  notify "博客推送失败" "$*"
  exit "$code"
}

# ---------- 4. 环境检查 ----------
[ -d "$BLOG_SRC" ]  || die 1 "源目录不存在:$BLOG_SRC"
[ -n "$(ls -A "$BLOG_SRC" 2>/dev/null || true)" ] || die 1 "源目录为空,中止:$BLOG_SRC"
[ -d "$BLOG_REPO/.git" ] || die 1 "不是 git 仓库:$BLOG_REPO"
command -v rsync >/dev/null 2>&1 || die 1 "未安装 rsync(pkg install rsync)"
command -v git   >/dev/null 2>&1 || die 1 "未安装 git"

# 日志瘦身
if [ -n "${LOG_FILE:-}" ] && [ -f "$LOG_FILE" ]; then
  if [ "$(wc -l <"$LOG_FILE" 2>/dev/null || echo 0)" -gt 300 ]; then
    tail -n 150 "$LOG_FILE" >"$LOG_FILE.tmp" 2>/dev/null && mv "$LOG_FILE.tmp" "$LOG_FILE" || true
  fi
fi

write_log "START src=$BLOG_SRC repo=$BLOG_REPO branch=$BLOG_BRANCH dry_run=$DRY_RUN"

# ---------- 5. 远端预检(在覆写本地之前) ----------
log "→ 检查远端 $BLOG_REMOTE/$BLOG_BRANCH"
cd "$BLOG_REPO"

if git ls-remote --exit-code --heads "$BLOG_REMOTE" "$BLOG_BRANCH" >/dev/null 2>&1; then
  git fetch --quiet "$BLOG_REMOTE" "$BLOG_BRANCH" 2>/dev/null \
    || die 2 "git fetch 失败,检查网络或凭据"
  remote_ref="$BLOG_REMOTE/$BLOG_BRANCH"
  if git rev-parse --verify --quiet HEAD >/dev/null 2>&1; then
    if ! git merge-base --is-ancestor "$remote_ref" HEAD 2>/dev/null; then
      die 2 "远端有本地没有的提交(可能你在 GitHub 网页上改过)。请先手动 git pull --rebase 后再运行。"
    fi
  fi
  vlog "远端无领先提交"
else
  if git ls-remote "$BLOG_REMOTE" >/dev/null 2>&1; then
    vlog "远端分支 $BLOG_BRANCH 尚不存在,将首次推送"
  else
    die 2 "无法连接远端仓库 $BLOG_REMOTE,检查网络或凭据"
  fi
fi

# ---------- 6. 同步 ----------
rsync_args=(-rlt --delete)
for e in "${PROTECTED_EXCLUDES[@]}" "${JUNK_EXCLUDES[@]}"; do
  rsync_args+=("--exclude=$e")
done
for e in "${EXTRA_EXCLUDES[@]}"; do
  rsync_args+=("--exclude=$e")
done

if [ "$DRY_RUN" -eq 1 ]; then
  rsync_args+=(--dry-run --itemize-changes)
  log "→ 预演同步(不会改动任何文件)"
  rsync "${rsync_args[@]}" "$BLOG_SRC/" "$BLOG_REPO/" || die 1 "rsync 预演失败"
  log ""
  log "→ 当前仓库未提交的变更:"
  if [ -n "$(git status --porcelain)" ]; then
    git -c core.quotepath=false status --short
  else
    log "  (无)"
  fi
  log ""
  log "预演结束:未修改仓库、未提交、未推送。"
  write_log "DRY-RUN 完成"
  exit 0
fi

log "→ 同步 $BLOG_SRC → $BLOG_REPO"
rsync "${rsync_args[@]}" "$BLOG_SRC/" "$BLOG_REPO/" || die 1 "rsync 同步失败"

git add -A
if [ -z "$(git status --porcelain)" ]; then
  log "没有变更"
  write_log "NO-CHANGE"
  notify "博客推送" "没有变更"
  exit 3
fi

# ---------- 7. 生成提交信息 ----------
trim() {
  local s="${1-}"
  s="${s#"${s%%[![:space:]]*}"}"
  s="${s%"${s##*[![:space:]]}"}"
  printf '%s' "$s"
}

html_unescape() {
  local s="${1-}" q="'"
  s=${s//&amp;/&}
  s=${s//&lt;/<}
  s=${s//&gt;/>}
  s=${s//&quot;/\"}
  s=${s//&#39;/$q}
  printf '%s' "$s"
}

# 从仓库内的 html 提取标题:<title> 优先,其次第一个 <h1>,最后退回文件名
post_title() {
  local path="$1" t="" f="$BLOG_REPO/$1"
  if [ -f "$f" ]; then
    t=$(sed -n 's|.*<title[^>]*>\([^<]*\)</title>.*|\1|p' "$f" 2>/dev/null | head -n 1) || t=""
    if [ -z "$(trim "$t")" ]; then
      t=$(sed -n 's|.*<h1[^>]*>\([^<]*\)</h1>.*|\1|p' "$f" 2>/dev/null | head -n 1) || t=""
    fi
    t=$(html_unescape "$t")
    t=$(trim "$t")
    if [ -n "$TITLE_SUFFIX" ] && [ "${#t}" -ge "${#TITLE_SUFFIX}" ] && [ "${t%"$TITLE_SUFFIX"}" != "$t" ]; then
      t="${t%"$TITLE_SUFFIX"}"
      t=$(trim "$t")
    fi
  fi
  [ -n "$t" ] || t="$(basename "$path" .html)"
  printf '%s' "$t"
}

# 拼接名称:超过 max 个就折叠成「A、B等N个」
join_names() {
  local max="$1" unit="${2:-个}"; shift 2
  local out="" n=0 total=$#
  for x in "$@"; do
    n=$((n + 1))
    if [ "$n" -gt "$max" ]; then
      out="${out}…等${total}${unit}"
      break
    fi
    if [ -n "$out" ]; then out="${out}、"; fi
    out="${out}${x}"
  done
  printf '%s' "$out"
}

build_message() {
  local st path
  local -a ps_a=() ps_m=() ps_d=() pg_a=() pg_m=() pg_d=()
  local -a st_a=() st_m=() st_d=() as_a=() as_m=() as_d=()
  local -a ot_a=() ot_m=() ot_d=()
  local -a body=()

  while IFS=$'\t' read -r st path; do
    [ -n "${path:-}" ] || continue
    case "$path" in
      posts/*)
        case "$st" in
          A|C) ps_a+=("$(post_title "$path")") ;;
          D)   ps_d+=("$(post_title "$path")") ;;
          *)   ps_m+=("$(post_title "$path")") ;;
        esac ;;
      assets/*)
        case "$st" in
          A|C) as_a+=("$(basename "$path")") ;;
          D)   as_d+=("$(basename "$path")") ;;
          *)   as_m+=("$(basename "$path")") ;;
        esac ;;
      css/*|js/*)
        case "$st" in
          A|C) st_a+=("$path") ;;
          D)   st_d+=("$path") ;;
          *)   st_m+=("$path") ;;
        esac ;;
      *.html)
        case "$st" in
          A|C) pg_a+=("$path") ;;
          D)   pg_d+=("$path") ;;
          *)   pg_m+=("$path") ;;
        esac ;;
      *)
        case "$st" in
          A|C) ot_a+=("$path") ;;
          D)   ot_d+=("$path") ;;
          *)   ot_m+=("$path") ;;
        esac ;;
    esac
  done < <(git -c core.quotepath=false diff --cached --name-status --no-renames)

  local total=$(( ${#ps_a[@]} + ${#ps_m[@]} + ${#ps_d[@]}
                + ${#pg_a[@]} + ${#pg_m[@]} + ${#pg_d[@]}
                + ${#st_a[@]} + ${#st_m[@]} + ${#st_d[@]}
                + ${#as_a[@]} + ${#as_m[@]} + ${#as_d[@]}
                + ${#ot_a[@]} + ${#ot_m[@]} + ${#ot_d[@]} ))
  [ "$total" -gt 0 ] || { printf '更新文件\n'; return 0; }

  local subject="" counted=0
  if [ "${#ps_a[@]}" -gt 0 ]; then
    if [ "${#ps_a[@]}" -eq 1 ]; then
      subject="新增《${ps_a[0]}》"
    else
      subject="新增 ${#ps_a[@]} 篇文章:$(join_names 3 篇 "${ps_a[@]}")"; counted=1
    fi
  elif [ "${#ps_d[@]}" -gt 0 ] && [ "$total" -eq "${#ps_d[@]}" ]; then
    if [ "${#ps_d[@]}" -eq 1 ]; then
      subject="删除《${ps_d[0]}》"
    else
      subject="删除 ${#ps_d[@]} 篇文章:$(join_names 3 篇 "${ps_d[@]}")"; counted=1
    fi
  elif [ "${#ps_m[@]}" -gt 0 ]; then
    if [ "${#ps_m[@]}" -eq 1 ]; then
      subject="更新《${ps_m[0]}》"
    else
      subject="更新 ${#ps_m[@]} 篇文章:$(join_names 3 篇 "${ps_m[@]}")"; counted=1
    fi
  elif [ "${#st_a[@]}" -eq 0 ] && [ "${#st_m[@]}" -eq 0 ] && [ "${#st_d[@]}" -eq 0 ] \
    && [ "${#pg_a[@]}" -eq 0 ] && [ "${#pg_m[@]}" -eq 0 ] && [ "${#pg_d[@]}" -eq 0 ] \
    && [ "${#as_a[@]}" -eq 0 ] && [ "${#as_m[@]}" -eq 0 ] && [ "${#as_d[@]}" -eq 0 ] \
    && [ "${#ot_a[@]}" -eq 0 ] && [ "${#ot_m[@]}" -eq 0 ] && [ "${#ot_d[@]}" -eq 0 ]; then
    subject="更新文件"
  elif [ "${#pg_a[@]}" -eq 0 ] && [ "${#pg_m[@]}" -eq 0 ] && [ "${#pg_d[@]}" -eq 0 ] \
    && [ "${#as_a[@]}" -eq 0 ] && [ "${#as_m[@]}" -eq 0 ] && [ "${#as_d[@]}" -eq 0 ] \
    && [ "${#ot_a[@]}" -eq 0 ] && [ "${#ot_m[@]}" -eq 0 ] && [ "${#ot_d[@]}" -eq 0 ]; then
    subject="调整样式与脚本"
  elif [ "${#pg_a[@]}" -eq 0 ] && [ "${#pg_m[@]}" -eq 0 ] && [ "${#pg_d[@]}" -eq 0 ] \
    && [ "${#ot_a[@]}" -eq 0 ] && [ "${#ot_m[@]}" -eq 0 ] && [ "${#ot_d[@]}" -eq 0 ] \
    && [ "${#ps_a[@]}" -eq 0 ] && [ "${#ps_m[@]}" -eq 0 ] && [ "${#ps_d[@]}" -eq 0 ]; then
    if [ "${#as_m[@]}" -eq 0 ] && [ "${#as_d[@]}" -eq 0 ]; then
      subject="新增图片资源 ${total} 个"
    elif [ "${#as_a[@]}" -eq 0 ] && [ "${#as_m[@]}" -eq 0 ]; then
      subject="删除图片资源 ${total} 个"
    else
      subject="更新图片资源 ${total} 个"
    fi
    counted=1
  elif [ "$total" -eq 1 ] && [ "${#pg_m[@]}" -eq 1 ]; then
    subject="更新页面 ${pg_m[0]}"
  else
    subject="更新 ${total} 个文件"; counted=1
  fi

  if [ "$total" -gt 1 ] && [ "$counted" -eq 0 ]; then
    subject="${subject},共 ${total} 个文件"
  fi

  # 正文:按分类列明细
  if [ "$total" -gt 1 ]; then
    if [ "${#ps_a[@]}" -gt 0 ]; then body+=("文章:新增 ${#ps_a[@]} 篇 · $(join_names 4 篇 "${ps_a[@]}")"); fi
    if [ "${#ps_m[@]}" -gt 0 ]; then body+=("文章:修改 ${#ps_m[@]} 篇 · $(join_names 4 篇 "${ps_m[@]}")"); fi
    if [ "${#ps_d[@]}" -gt 0 ]; then body+=("文章:删除 ${#ps_d[@]} 篇 · $(join_names 4 篇 "${ps_d[@]}")"); fi
    if [ "${#pg_a[@]}" -gt 0 ]; then body+=("页面:新增 $(join_names 4 个 "${pg_a[@]}")"); fi
    if [ "${#pg_m[@]}" -gt 0 ]; then body+=("页面:修改 $(join_names 4 个 "${pg_m[@]}")"); fi
    if [ "${#pg_d[@]}" -gt 0 ]; then body+=("页面:删除 $(join_names 4 个 "${pg_d[@]}")"); fi
    if [ "${#st_a[@]}" -gt 0 ]; then body+=("样式脚本:新增 $(join_names 4 个 "${st_a[@]}")"); fi
    if [ "${#st_m[@]}" -gt 0 ]; then body+=("样式脚本:修改 $(join_names 4 个 "${st_m[@]}")"); fi
    if [ "${#st_d[@]}" -gt 0 ]; then body+=("样式脚本:删除 $(join_names 4 个 "${st_d[@]}")"); fi
    if [ "${#as_a[@]}" -gt 0 ]; then body+=("资源:新增 ${#as_a[@]} 个 · $(join_names 4 个 "${as_a[@]}")"); fi
    if [ "${#as_m[@]}" -gt 0 ]; then body+=("资源:修改 ${#as_m[@]} 个 · $(join_names 4 个 "${as_m[@]}")"); fi
    if [ "${#as_d[@]}" -gt 0 ]; then body+=("资源:删除 ${#as_d[@]} 个 · $(join_names 4 个 "${as_d[@]}")"); fi
    if [ "${#ot_a[@]}" -gt 0 ]; then body+=("其他:新增 $(join_names 4 个 "${ot_a[@]}")"); fi
    if [ "${#ot_m[@]}" -gt 0 ]; then body+=("其他:修改 $(join_names 4 个 "${ot_m[@]}")"); fi
    if [ "${#ot_d[@]}" -gt 0 ]; then body+=("其他:删除 $(join_names 4 个 "${ot_d[@]}")"); fi
    if [ "${#body[@]}" -gt 6 ]; then
      local keep=("${body[@]:0:6}")
      body=("${keep[@]}" "…等 $(( ${#body[@]} - 6 )) 项")
    fi
  fi

  printf '%s\n' "$subject"
  if [ "${#body[@]}" -gt 0 ]; then
    printf '\n'
    local line
    for line in "${body[@]}"; do printf '%s\n' "$line"; done
  fi
}

if [ -n "$CUSTOM_MSG" ]; then
  msg="$CUSTOM_MSG"
else
  msg="$(build_message)"
fi

subject="$(printf '%s\n' "$msg" | head -n 1)"
body="$(printf '%s\n' "$msg" | tail -n +2)"

# ---------- 8. 提交与推送 ----------
log ""
log "提交信息:"
printf '%s\n' "$msg" | sed 's/^/    /'
log ""

if [ -n "$(printf '%s' "$body" | tr -d '[:space:]')" ]; then
  git commit -m "$subject" -m "$body" >/dev/null || die 1 "git commit 失败"
else
  git commit -m "$subject" >/dev/null || die 1 "git commit 失败"
fi

local_sha="$(git rev-parse --short HEAD)"

if [ "$NO_PUSH" -eq 1 ]; then
  log "✓ 已提交 $local_sha(--no-push,未推送)"
  write_log "COMMIT $local_sha subject=$subject (no-push)"
  notify "博客推送" "已本地提交(未推送)"
  exit 0
fi

log "→ 推送到 $BLOG_REMOTE/$BLOG_BRANCH"
if ! git push "$BLOG_REMOTE" "HEAD:$BLOG_BRANCH" 2>&1 | sed 's/^/    /'; then
  write_log "PUSH-FAIL $local_sha"
  die 2 "推送失败,提交 $local_sha 已保存在本地"
fi

log "✓ 已推送 $local_sha,Cloudflare Pages 将开始构建"
write_log "OK $local_sha subject=$subject"
notify "博客已推送" "$subject"
exit 0