📜 cron-tasks/sitemap.sh

#!/usr/bin/env bash
# 🌐 Trilium Sitemap 自动更新 — 快速单页提取模式
set -o pipefail

BASE="https://trilium.atibm.com"
ALIAS="hermes"
OUTFILE="/tmp/sitemap-latest.xml"
NOW=$(date -u +%Y-%m-%d)

echo "🌐 获取 Trilium 共享主页..." >&2
HTML=$(curl -s --max-time 8 -o - "${BASE}/share/${ALIAS}" 2>/dev/null) || {
  echo "❌ 无法获取共享主页" >&2
  exit 1
}

echo "🔗 提取链接..." >&2
ALL_LINKS=$(echo "$HTML" | grep -oP 'href="\./[^"]+"' | sed 's/href="\.\///;s/"//g' | sort -u)

VALID_LINKS=""
while IFS= read -r link; do
  [[ "$link" =~ \.(js|css|png|jpg|jpeg|gif|svg|ico|woff2?|ttf|eot)$ ]] && continue
  if [[ "$link" =~ ^[A-Za-z0-9]{8,15}$ ]]; then
    VALID_LINKS+="${BASE}/share/${link}"$'\n'
  elif [[ "$link" =~ ^[a-zA-Z][a-zA-Z0-9]{4,30}$ ]]; then
    VALID_LINKS+="${BASE}/share/${link}"$'\n'
  elif [[ "$link" =~ ^CVE- ]]; then
    VALID_LINKS+="${BASE}/share/${link}"$'\n'
  fi
done <<< "$ALL_LINKS"

COUNT=$(echo "$VALID_LINKS" | grep -c .)
echo "  提取到 ${COUNT} 个有效链接" >&2

# ── 密码保护过滤 ──
ETAPI_TOKEN="${TRILIUM_ETAPI_TOKEN}"
if [ -z "$ETAPI_TOKEN" ] && [ -f /opt/data/.env ]; then
  ETAPI_TOKEN=$(grep '^TRILIUM_ETAPI_TOKEN=' /opt/data/.env | cut -d= -f2- | tr -d '"'"'"')
fi
if [ -z "$ETAPI_TOKEN" ] && [ -f "$HOME/.env" ]; then
  ETAPI_TOKEN=$(grep '^TRILIUM_ETAPI_TOKEN=' "$HOME/.env" | cut -d= -f2- | tr -d '"'"'"')
fi

if [ -n "$ETAPI_TOKEN" ] && [ "$COUNT" -gt 0 ]; then
  echo "🔒 查询密码保护笔记..." >&2
  PROTECTED_CODES=$(curl -s --max-time 10 \
    -H "Authorization: ***" \
    "${BASE}/etapi/notes?search=%23shareCredentials" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
exclude=set()
for r in d.get('results',[]):
    nid=r.get('noteId','')
    if not nid: continue
    has_cred=any(a.get('name')=='shareCredentials' for a in r.get('attributes',[]))
    if not has_cred: continue
    exclude.add(nid)
    for a in r.get('attributes',[]):
        if a.get('name')=='shareAlias' and a.get('value'):
            exclude.add(a['value'])
for e in sorted(exclude): print(e)
")

  PROTECTED_COUNT=$(echo "$PROTECTED_CODES" | grep -c . 2>/dev/null || echo 0)
  if [ "$PROTECTED_COUNT" -gt 0 ]; then
    echo "  找到 ${PROTECTED_COUNT} 个密码保护标识符,过滤中..." >&2
    FILTERED=""; REMOVED=0
    while IFS= read -r url; do
      [ -z "$url" ] && continue
      nid=$(echo "$url" | sed 's|.*/share/||')
      if echo "$PROTECTED_CODES" | grep -qxF "$nid"; then
        REMOVED=$((REMOVED + 1))
      else
        FILTERED+="$url"$'\n'
      fi
    done <<< "$VALID_LINKS"
    VALID_LINKS="$FILTERED"
    COUNT=$((COUNT - REMOVED))
    echo "  🔒 已移除 ${REMOVED} 个密码保护链接" >&2
  fi
fi

# 生成 sitemap XML
{
  echo '<?xml version="1.0" encoding="utf-8"?>'
  echo '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">'
  echo '  <url>'
  echo "    <loc>${BASE}/share/${ALIAS}</loc>"
  echo "    <lastmod>${NOW}</lastmod>"
  echo '    <changefreq>daily</changefreq>'
  echo '    <priority>1.0</priority>'
  echo '  </url>'
  while IFS= read -r url; do
    [ -z "$url" ] && continue
    echo '  <url>'
    echo "    <loc>${url}</loc>"
    echo "    <lastmod>${NOW}</lastmod>"
    echo '    <changefreq>weekly</changefreq>'
    echo '    <priority>0.6</priority>'
    echo '  </url>'
  done <<< "$VALID_LINKS"
  echo '</urlset>'
} > "$OUTFILE"

SIZE=$(wc -c < "$OUTFILE")
echo "  sitemap.xml: ${SIZE} bytes, ${COUNT} URLs" >&2

ETAPI_NOTE_ID="${TRILIUM_SITEMAP_NOTE_ID:-387btDIOMxHp}"
if [ -n "$ETAPI_TOKEN" ]; then
  ETAPI_URL="https://trilium.atibm.com/etapi/notes/${ETAPI_NOTE_ID}/content"
  HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" -X PUT "$ETAPI_URL" \
    -H "Authorization: ***" \
    -H "Content-Type: text/plain" \
    --data-binary @"$OUTFILE" 2>&1)
  echo "  ETAPI update: HTTP ${HTTP_CODE}" >&2
else
  echo "  ⚠️ TRILIUM_ETAPI_TOKEN 未设置,跳过 ETAPI 更新" >&2
fi

echo "✅ 完成: ${BASE}/sitemap.xml (${SIZE} bytes, ${COUNT} URLs)" >&2
cat "$OUTFILE"