| import os |
| import requests |
| from flask import Flask, request, jsonify, render_template_string |
| from datetime import datetime |
| import time |
|
|
| app = Flask(__name__) |
| app.secret_key = os.environ.get("SECRET_KEY", "quark-offline-secret-key") |
|
|
| |
| DEFAULT_COOKIE = os.environ.get("QUARK_COOKIE") |
| if not DEFAULT_COOKIE: |
| raise RuntimeError("请在 Hugging Face Secrets 中配置 QUARK_COOKIE") |
|
|
| current_cookie = DEFAULT_COOKIE |
| rate_limit_records = {} |
|
|
|
|
| def get_client_ip(): |
| if request.headers.get('X-Forwarded-For'): |
| return request.headers.get('X-Forwarded-For').split(',')[0] |
| if request.headers.get('X-Real-IP'): |
| return request.headers.get('X-Real-IP') |
| return request.remote_addr or "unknown" |
|
|
|
|
| def rate_limit(min_interval=10, max_calls_per_minute=2): |
| def decorator(f): |
| def decorated_function(*args, **kwargs): |
| identifier = get_client_ip() + "_" + f.__name__ |
| now = time.time() |
| record = rate_limit_records.get(identifier, {"last_call": 0, "calls": []}) |
| record["calls"] = [t for t in record["calls"] if now - t < 60] |
| if len(record["calls"]) >= max_calls_per_minute: |
| oldest = record["calls"][0] |
| wait_time = 60 - (now - oldest) |
| return jsonify({"success": False, "error": f"操作过于频繁,请等待 {int(wait_time)} 秒"}), 429 |
| if now - record["last_call"] < min_interval: |
| wait_time = min_interval - (now - record["last_call"]) |
| return jsonify({"success": False, "error": f"请等待 {int(wait_time)} 秒后再试"}), 429 |
| record["last_call"] = now |
| record["calls"].append(now) |
| rate_limit_records[identifier] = record |
| return f(*args, **kwargs) |
| decorated_function.__name__ = f.__name__ |
| return decorated_function |
| return decorator |
|
|
|
|
| def get_headers(): |
| return { |
| "Cookie": current_cookie, |
| "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) quark-cloud-drive/2.5.20 Chrome/100.0.4896.160 Electron/18.3.5.4-b478491100 Safari/537.36", |
| "Accept": "application/json, text/plain, */*", |
| "Referer": "https://pan.quark.cn", |
| "Origin": "https://pan.quark.cn", |
| "Content-Type": "application/json" |
| } |
|
|
|
|
| def mask_cookie(cookie): |
| if not cookie: |
| return "未设置" |
| if len(cookie) <= 20: |
| return cookie[:8] + "***" |
| return cookie[:15] + "..." + cookie[-10:] |
|
|
|
|
| def safe_format_timestamp(timestamp): |
| if not timestamp: |
| return "暂无" |
| try: |
| ts = int(timestamp) |
| if ts <= 0 or ts > 4102444800: |
| return "暂无" |
| return datetime.fromtimestamp(ts).strftime("%Y-%m-%d") |
| except: |
| return "暂无" |
|
|
|
|
| def get_space_info(): |
| try: |
| url = "https://drive.quark.cn/1/clouddrive/member" |
| params = {"pr": "ucpro", "fr": "pc", "fetch_subscribe": "false", "_ch": "home", "fetch_identity": "false"} |
| resp = requests.get(url, headers=get_headers(), params=params, timeout=10) |
| data = resp.json() |
| if data.get("status") == 200 or data.get("code") == 0: |
| info = data.get("data", {}) |
| total_bytes = info.get("total_capacity", 1) |
| used_bytes = info.get("use_capacity", 0) |
| total_gb = total_bytes / (1024**3) |
| used_gb = used_bytes / (1024**3) |
| free_gb = max(total_gb - used_gb, 0) |
| percent = min(used_gb / total_gb * 100, 100) if total_gb > 0 else 0 |
| |
| member_status = info.get("member_status", {}) |
| vip_level = "普通用户" |
| is_88vip = False |
| |
| if member_status.get("SUPER_VIP"): |
| vip_level = "超级会员(SVIP)" |
| is_88vip = True |
| elif member_status.get("VIP"): |
| vip_level = "VIP会员" |
| elif member_status.get("Z_VIP"): |
| vip_level = "Z会员" |
| elif member_status.get("MINI_VIP"): |
| vip_level = "迷你会员" |
| |
| expire_ts = info.get("expired_at", 0) or info.get("created_at", 0) |
| return { |
| "success": True, |
| "total": round(total_gb, 2), |
| "used": round(used_gb, 2), |
| "free": round(free_gb, 2), |
| "percent": round(percent, 1), |
| "vip": vip_level, |
| "is_88vip": is_88vip, |
| "expire_date": safe_format_timestamp(expire_ts) |
| } |
| return {"success": False, "error": data.get("message", "获取失败")} |
| except Exception as e: |
| return {"success": False, "error": str(e)} |
|
|
|
|
| HTML = """ |
| <!DOCTYPE html> |
| <html> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes"> |
| <title>夸克网盘信息查看器</title> |
| <style> |
| * { margin: 0; padding: 0; box-sizing: border-box; } |
| body { |
| font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; |
| background: #f0f2f5; |
| padding: 20px; |
| } |
| .container { |
| max-width: 600px; |
| margin: 0 auto; |
| } |
| .card { |
| background: white; |
| border-radius: 16px; |
| padding: 20px; |
| margin-bottom: 16px; |
| box-shadow: 0 1px 3px rgba(0,0,0,0.1); |
| } |
| .card-header { |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| margin-bottom: 16px; |
| padding-bottom: 12px; |
| border-bottom: 1px solid #eee; |
| } |
| .card-header h2 { |
| font-size: 18px; |
| font-weight: 600; |
| color: #1a1a2e; |
| } |
| .badge { |
| background: #e8f4ff; |
| color: #0066cc; |
| padding: 4px 10px; |
| border-radius: 20px; |
| font-size: 12px; |
| } |
| button { |
| background: #0066cc; |
| color: white; |
| border: none; |
| padding: 10px 16px; |
| border-radius: 10px; |
| font-size: 14px; |
| font-weight: 500; |
| cursor: pointer; |
| } |
| button:hover { background: #0052a3; } |
| button:disabled { background: #ccc; cursor: not-allowed; } |
| .btn-sm { |
| padding: 6px 12px; |
| font-size: 12px; |
| } |
| textarea { |
| width: 100%; |
| padding: 12px; |
| border: 1px solid #ddd; |
| border-radius: 10px; |
| font-size: 14px; |
| resize: vertical; |
| margin-bottom: 12px; |
| font-family: monospace; |
| } |
| textarea:focus { |
| outline: none; |
| border-color: #0066cc; |
| } |
| .info-row { |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| padding: 12px 0; |
| border-bottom: 1px solid #f0f0f0; |
| } |
| .info-label { |
| color: #666; |
| font-size: 14px; |
| } |
| .info-value { |
| font-weight: 500; |
| color: #1a1a2e; |
| display: flex; |
| align-items: center; |
| gap: 8px; |
| } |
| .progress-bar { |
| background: #e0e0e0; |
| border-radius: 10px; |
| height: 8px; |
| overflow: hidden; |
| margin-top: 8px; |
| } |
| .progress-fill { |
| background: #28a745; |
| width: 0%; |
| height: 100%; |
| border-radius: 10px; |
| } |
| .alert { |
| padding: 12px; |
| border-radius: 10px; |
| margin-bottom: 16px; |
| font-size: 13px; |
| } |
| .alert-warning { |
| background: #fff3cd; |
| border-left: 4px solid #ffc107; |
| color: #856404; |
| } |
| .alert-info { |
| background: #e7f3ff; |
| border-left: 4px solid #17a2b8; |
| color: #0c5460; |
| } |
| .result { |
| margin-top: 12px; |
| padding: 10px; |
| border-radius: 10px; |
| display: none; |
| } |
| .result.success { background: #d4edda; color: #155724; display: block; } |
| .result.error { background: #f8d7da; color: #721c24; display: block; } |
| .vip-tag { |
| background: #e8a020; |
| color: white; |
| padding: 2px 8px; |
| border-radius: 12px; |
| font-size: 11px; |
| } |
| .vip-88 { |
| background: #ff6b6b; |
| } |
| .empty-text { |
| text-align: center; |
| padding: 30px; |
| color: #999; |
| } |
| </style> |
| </head> |
| <body> |
| <div class="container"> |
| <div class="alert alert-warning"> |
| ⚠️ 重要提示:手动点击刷新,建议每小时1-2次,避免Cookie失效 |
| </div> |
| |
| <!-- 会员信息 --> |
| <div class="card"> |
| <div class="card-header"> |
| <h2>📊 会员信息 & 💾 空间情况</h2> |
| <button class="btn-sm" onclick="loadSpaceInfo()">🔄 刷新</button> |
| </div> |
| <div id="spaceInfo"> |
| <div class="empty-text">点击刷新按钮获取会员信息</div> |
| </div> |
| </div> |
| |
| <!-- Cookie 管理 --> |
| <div class="card"> |
| <div class="card-header"> |
| <h2>🔧 Cookie 管理</h2> |
| <span class="badge" id="cookieDisplay">加载中...</span> |
| </div> |
| <textarea id="newCookie" rows="3" placeholder="粘贴完整的 Cookie 字符串 格式如: __quark_di=xxx; __quark_uid=xxx; ..."></textarea> |
| <button onclick="updateCookie()">🔄 更换 Cookie</button> |
| <div id="cookieResult" class="result"></div> |
| <div class="alert-info" style="margin-top: 12px; padding: 10px; font-size: 12px;"> |
| 💡 如何获取Cookie?浏览器登录 pan.quark.cn → F12 → Application → Cookies → 复制所有键值对 |
| </div> |
| </div> |
| </div> |
| |
| <script> |
| let lastSpaceCall = 0; |
| const SPACE_MIN_INTERVAL = 10000; |
| |
| async function loadSpaceInfo() { |
| const now = Date.now(); |
| if (now - lastSpaceCall < SPACE_MIN_INTERVAL) { |
| const waitSeconds = Math.ceil((SPACE_MIN_INTERVAL - (now - lastSpaceCall)) / 1000); |
| const div = document.getElementById("spaceInfo"); |
| div.innerHTML = `<div class="empty-text" style="color: #856404;">⏰ 请等待 ${waitSeconds} 秒后再试</div>`; |
| setTimeout(() => { |
| const currentDiv = document.getElementById("spaceInfo"); |
| if (currentDiv && currentDiv.innerHTML.includes("请等待")) { |
| currentDiv.innerHTML = '<div class="empty-text">点击刷新按钮获取会员信息</div>'; |
| } |
| }, 3000); |
| return; |
| } |
| |
| const div = document.getElementById("spaceInfo"); |
| div.innerHTML = '<div class="empty-text">加载中...</div>'; |
| |
| try { |
| const resp = await fetch("/api/space_info"); |
| const data = await resp.json(); |
| |
| if (data.success) { |
| lastSpaceCall = now; |
| |
| let vipHtml = data.vip; |
| if (data.is_88vip) { |
| vipHtml = `${data.vip} <span class="vip-tag vip-88">88VIP</span>`; |
| } else if (data.vip !== '普通用户') { |
| vipHtml = `${data.vip} <span class="vip-tag">VIP</span>`; |
| } |
| |
| const percent = data.percent; |
| div.innerHTML = ` |
| <div class="info-row"><span class="info-label">会员类型</span><span class="info-value">${vipHtml}</span></div> |
| <div class="info-row"><span class="info-label">到期时间</span><span class="info-value">${data.expire_date}</span></div> |
| <div class="info-row"><span class="info-label">总空间</span><span class="info-value">${data.total} GB</span></div> |
| <div class="info-row"><span class="info-label">已用空间</span><span class="info-value">${data.used} GB</span></div> |
| <div class="info-row"><span class="info-label">剩余空间</span><span class="info-value">${data.free} GB</span></div> |
| <div class="info-row"><span class="info-label">使用率</span><span class="info-value">${percent}%</span></div> |
| <div class="progress-bar"><div class="progress-fill" style="width: ${percent}%;"></div></div> |
| <div class="alert-info" style="margin-top: 12px; padding: 8px; font-size: 12px;">✅ 获取成功,建议1小时内不要超过2次</div> |
| `; |
| } else { |
| div.innerHTML = `<div class="empty-text" style="color: #dc3545;">❌ ${data.error}</div>`; |
| } |
| } catch(e) { |
| div.innerHTML = `<div class="empty-text" style="color: #dc3545;">❌ 获取失败</div>`; |
| } |
| } |
| |
| async function updateCookie() { |
| const newCookie = document.getElementById("newCookie").value.trim(); |
| if (!newCookie) { |
| showResult("cookieResult", "请输入新 Cookie", "error"); |
| return; |
| } |
| |
| const btn = event.target; |
| btn.disabled = true; |
| btn.textContent = "更换中..."; |
| |
| try { |
| const resp = await fetch("/api/update_cookie", { |
| method: "POST", |
| headers: { "Content-Type": "application/json" }, |
| body: JSON.stringify({ cookie: newCookie }) |
| }); |
| const data = await resp.json(); |
| if (resp.ok) { |
| showResult("cookieResult", "✅ Cookie已更新", "success"); |
| document.getElementById("newCookie").value = ""; |
| loadCurrentCookie(); |
| } else { |
| showResult("cookieResult", "❌ " + (data.error || "更换失败"), "error"); |
| } |
| } catch(e) { |
| showResult("cookieResult", "❌ 网络错误", "error"); |
| } finally { |
| btn.disabled = false; |
| btn.textContent = "🔄 更换 Cookie"; |
| } |
| } |
| |
| async function loadCurrentCookie() { |
| try { |
| const resp = await fetch("/api/current_cookie"); |
| const data = await resp.json(); |
| document.getElementById("cookieDisplay").textContent = data.masked_cookie; |
| } catch(e) { |
| document.getElementById("cookieDisplay").textContent = "获取失败"; |
| } |
| } |
| |
| function showResult(elementId, msg, type) { |
| const div = document.getElementById(elementId); |
| if (!div) return; |
| div.className = "result " + type; |
| div.textContent = msg; |
| setTimeout(() => { if (div) div.className = "result"; }, 5000); |
| } |
| |
| loadCurrentCookie(); |
| </script> |
| </body> |
| </html> |
| """ |
|
|
|
|
| @app.route("/", methods=["GET"]) |
| def index(): |
| return render_template_string(HTML) |
|
|
|
|
| @app.route("/api/space_info", methods=["GET"]) |
| @rate_limit(min_interval=10, max_calls_per_minute=2) |
| def api_space_info(): |
| return jsonify(get_space_info()), 200 |
|
|
|
|
| @app.route("/api/update_cookie", methods=["POST"]) |
| def api_update_cookie(): |
| global current_cookie |
| data = request.get_json() |
| if not data or "cookie" not in data: |
| return jsonify({"error": "缺少 cookie"}), 400 |
| new_cookie = data["cookie"].strip() |
| if not new_cookie: |
| return jsonify({"error": "Cookie 不能为空"}), 400 |
| current_cookie = new_cookie |
| return jsonify({"status": "success", "message": "Cookie已更新"}), 200 |
|
|
|
|
| @app.route("/api/current_cookie", methods=["GET"]) |
| def api_current_cookie(): |
| return jsonify({"masked_cookie": mask_cookie(current_cookie)}), 200 |
|
|
|
|
| if __name__ == "__main__": |
| app.run(host="0.0.0.0", port=7860) |