#!/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/.result # Format: "OK " or "ERR " # --------------------------------------------------------------------------- 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