2api / cleanup_tokens.sh
ohmyapi's picture
feat: add automatic token cleanup with crond + fixed /tmp/crontabs for HF read-only fs
9727ff0 verified
#!/bin/sh
#
# cleanup_tokens.sh - Remove invalid/unavailable auth tokens via CLIProxyAPI Management API
#
# Environment variables:
# MANAGEMENT_PASSWORD CLIProxyAPI management API password (required)
# FEISHU_WEBHOOK_URL Feishu bot webhook URL for notifications (optional)
# CLEANUP_CONCURRENCY Max parallel DELETE requests (default: 20)
#
API_BASE="http://localhost:7860/v0/management"
TIMESTAMP="$(date '+%Y-%m-%d %H:%M:%S')"
LOG_PREFIX="[cleanup_tokens] ${TIMESTAMP}"
TMP_FILE="/tmp/auth_files_$$.json"
TMP_NAMES="/tmp/auth_names_$$.txt"
TMP_DIR="/tmp/cleanup_$$"
CONCURRENCY="${CLEANUP_CONCURRENCY:-20}"
cleanup_tmp() {
rm -f "$TMP_FILE" "$TMP_NAMES"
rm -rf "$TMP_DIR"
}
trap cleanup_tmp EXIT
# ---------------------------------------------------------------------------
# notify_feishu TITLE BODY
# ---------------------------------------------------------------------------
notify_feishu() {
[ -z "$FEISHU_WEBHOOK_URL" ] && return 0
PAYLOAD=$(jq -n --arg title "$1" --arg body "$2" '{
msg_type: "post",
content: { post: { zh_cn: {
title: $title,
content: [[{"tag": "text", "text": $body}]]
}}}
}')
curl -sf -X POST -H "Content-Type: application/json" \
-d "$PAYLOAD" "$FEISHU_WEBHOOK_URL" > /dev/null 2>&1 \
&& echo "${LOG_PREFIX} Feishu notification sent." \
|| echo "${LOG_PREFIX} WARNING: Failed to send Feishu notification."
}
# ---------------------------------------------------------------------------
# delete_one NAME — writes result to $TMP_DIR/<name>.result
# Format: "OK <name>" or "ERR <name> <http_status>"
# ---------------------------------------------------------------------------
delete_one() {
NAME="$1"
ENCODED=$(printf '%s' "$NAME" | sed 's/@/%40/g; s/ /%20/g')
RESULT_FILE="${TMP_DIR}/$(printf '%s' "$NAME" | md5sum | cut -d' ' -f1).result"
HTTP_ST=$(curl -s -o /dev/null -w "%{http_code}" \
-X DELETE \
-H "Authorization: Bearer ${MANAGEMENT_PASSWORD}" \
"${API_BASE}/auth-files?name=${ENCODED}" 2>/dev/null)
if [ "$HTTP_ST" = "200" ]; then
printf 'OK %s\n' "$NAME" > "$RESULT_FILE"
else
printf 'ERR %s %s\n' "$NAME" "$HTTP_ST" > "$RESULT_FILE"
fi
}
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
if [ -z "$MANAGEMENT_PASSWORD" ]; then
echo "${LOG_PREFIX} ERROR: MANAGEMENT_PASSWORD not set, skipping cleanup"
exit 1
fi
echo "${LOG_PREFIX} Starting invalid token cleanup... (concurrency=${CONCURRENCY})"
# --- Fetch auth file list ---------------------------------------------------
HTTP_STATUS=$(curl -s -o "$TMP_FILE" -w "%{http_code}" \
-H "Authorization: Bearer ${MANAGEMENT_PASSWORD}" \
"${API_BASE}/auth-files" 2>/dev/null)
if [ "$HTTP_STATUS" != "200" ]; then
MSG="API 返回 HTTP ${HTTP_STATUS},服务可能尚未就绪。"
echo "${LOG_PREFIX} ERROR: ${MSG}"
notify_feishu "❌ Token 清理失败" "[TIME] ${TIMESTAMP}
[ERROR] ${MSG}"
exit 1
fi
if ! jq empty "$TMP_FILE" 2>/dev/null; then
echo "${LOG_PREFIX} ERROR: Invalid JSON response"
notify_feishu "❌ Token 清理失败" "[TIME] ${TIMESTAMP}
[ERROR] API 返回了无效的 JSON 响应"
exit 1
fi
TOTAL=$(jq '.files | length' "$TMP_FILE")
echo "${LOG_PREFIX} Total auth files: ${TOTAL}"
[ "$TOTAL" -eq 0 ] && { echo "${LOG_PREFIX} No auth files found."; exit 0; }
# --- Identify tokens to delete ---------------------------------------------
jq -r '
.files[] |
select(.runtime_only != true) |
select(
.unavailable == true or
(.status != null and (
.status == "error" or .status == "expired" or
.status == "invalid" or .status == "failed" or
.status == "unauthorized" or .status == "quota_exceeded"
))
) |
.name
' "$TMP_FILE" > "$TMP_NAMES"
TO_DELETE=$(grep -c '' "$TMP_NAMES" 2>/dev/null || echo 0)
echo "${LOG_PREFIX} Tokens to delete: ${TO_DELETE}"
if [ "$TO_DELETE" -eq 0 ]; then
echo "${LOG_PREFIX} All tokens healthy, nothing to clean up."
notify_feishu "✅ Token 状态检查" "[TIME] ${TIMESTAMP}
[STATS] 共 ${TOTAL} 个 token,全部健康
[RESULT] 无需清理"
exit 0
fi
# Log the plan
while IFS= read -r NAME; do
STATUS=$(jq -r --arg n "$NAME" '.files[] | select(.name==$n) | .status // "unknown"' "$TMP_FILE")
echo "${LOG_PREFIX} - ${NAME} (${STATUS})"
done < "$TMP_NAMES"
# --- Parallel DELETE with concurrency control ------------------------------
mkdir -p "$TMP_DIR"
ACTIVE=0
while IFS= read -r NAME; do
[ -z "$NAME" ] && continue
# Launch delete in background
delete_one "$NAME" &
ACTIVE=$((ACTIVE + 1))
# When we hit the concurrency limit, wait for all current batch to finish
if [ "$ACTIVE" -ge "$CONCURRENCY" ]; then
wait
ACTIVE=0
fi
done < "$TMP_NAMES"
# Wait for any remaining background jobs
wait
# --- Collect results -------------------------------------------------------
DELETED=0
ERRORS=0
DETAIL_LINES=""
ERROR_LINES=""
# Re-read the names file to preserve status info for the notification
while IFS= read -r NAME; do
[ -z "$NAME" ] && continue
RESULT_FILE="${TMP_DIR}/$(printf '%s' "$NAME" | md5sum | cut -d' ' -f1).result"
RESULT=$(cat "$RESULT_FILE" 2>/dev/null)
case "$RESULT" in
OK*)
STATUS=$(jq -r --arg n "$NAME" '.files[] | select(.name==$n) | .status // "unknown"' "$TMP_FILE")
echo "${LOG_PREFIX} DELETED: ${NAME}"
DELETED=$((DELETED + 1))
DETAIL_LINES="${DETAIL_LINES}${NAME} [${STATUS}]\n"
;;
ERR*)
HTTP_ST=$(echo "$RESULT" | awk '{print $3}')
echo "${LOG_PREFIX} ERROR deleting ${NAME} (HTTP ${HTTP_ST})"
ERRORS=$((ERRORS + 1))
ERROR_LINES="${ERROR_LINES}${NAME} (HTTP ${HTTP_ST})\n"
;;
*)
echo "${LOG_PREFIX} WARNING: No result for ${NAME}"
ERRORS=$((ERRORS + 1))
ERROR_LINES="${ERROR_LINES}${NAME} (no result)\n"
;;
esac
done < "$TMP_NAMES"
echo "${LOG_PREFIX} Done. Deleted: ${DELETED}, Errors: ${ERRORS}"
# --- Feishu notification ---------------------------------------------------
# printf to interpret \n in the accumulated lines
DETAIL_LINES=$(printf '%b' "$DETAIL_LINES")
ERROR_LINES=$(printf '%b' "$ERROR_LINES")
if [ "$ERRORS" -gt 0 ]; then
notify_feishu "⚠️ Token 清理完成(有错误)" "[TIME] ${TIMESTAMP}
[STATS] 共 ${TOTAL} 个 token,发现 ${TO_DELETE} 个失效
────────────────────
[SUCCESS] 已删除 ${DELETED} 个:
${DETAIL_LINES}
[FAIL] 删除失败 ${ERRORS} 个:
${ERROR_LINES}────────────────────"
else
notify_feishu "🧹 Token 清理完成" "[TIME] ${TIMESTAMP}
[STATS] 共 ${TOTAL} 个 token,清理 ${DELETED} 个失效
────────────────────
[REMOVED] 已删除:
${DETAIL_LINES}────────────────────
[RESULT] Success: ${DELETED}/${TO_DELETE} | Failed: 0/${TO_DELETE}"
fi