Spaces:
Running
Running
Upload folder using huggingface_hub
Browse files- .dockerignore +15 -0
- .gitignore +11 -0
- Dockerfile +24 -0
- README.md +10 -5
- app.py +260 -0
- requirements.txt +4 -0
- static/script.js +194 -0
- static/style.css +394 -0
- templates/index.html +214 -0
.dockerignore
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
__pycache__/
|
| 2 |
+
*.py[cod]
|
| 3 |
+
*$py.class
|
| 4 |
+
*.so
|
| 5 |
+
.Python
|
| 6 |
+
.env
|
| 7 |
+
.venv
|
| 8 |
+
venv/
|
| 9 |
+
ENV/
|
| 10 |
+
.git
|
| 11 |
+
.gitignore
|
| 12 |
+
*.log
|
| 13 |
+
.DS_Store
|
| 14 |
+
urls.json
|
| 15 |
+
logs.json
|
.gitignore
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
__pycache__/
|
| 2 |
+
*.py[cod]
|
| 3 |
+
*$py.class
|
| 4 |
+
*.so
|
| 5 |
+
.Python
|
| 6 |
+
.env
|
| 7 |
+
.venv
|
| 8 |
+
venv/
|
| 9 |
+
ENV/
|
| 10 |
+
*.log
|
| 11 |
+
.DS_Store
|
Dockerfile
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.10-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
# 复制依赖文件
|
| 6 |
+
COPY requirements.txt .
|
| 7 |
+
|
| 8 |
+
# 安装依赖
|
| 9 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 10 |
+
|
| 11 |
+
# 复制应用代码
|
| 12 |
+
COPY . .
|
| 13 |
+
|
| 14 |
+
# 创建数据目录
|
| 15 |
+
RUN mkdir -p /app/data
|
| 16 |
+
|
| 17 |
+
# 暴露端口
|
| 18 |
+
EXPOSE 7860
|
| 19 |
+
|
| 20 |
+
# 设置环境变量
|
| 21 |
+
ENV PYTHONUNBUFFERED=1
|
| 22 |
+
|
| 23 |
+
# 启动命令
|
| 24 |
+
CMD ["python", "app.py"]
|
README.md
CHANGED
|
@@ -1,10 +1,15 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
colorFrom: blue
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
| 7 |
-
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: "keephflive"
|
| 3 |
+
emoji: "🚀"
|
| 4 |
colorFrom: blue
|
| 5 |
+
colorTo: green
|
| 6 |
sdk: docker
|
| 7 |
+
app_port: 7860
|
| 8 |
---
|
| 9 |
|
| 10 |
+
### 🚀 一键部署
|
| 11 |
+
[](https://github.com/kfcx/HFSpaceDeploy)
|
| 12 |
+
|
| 13 |
+
本项目由[HFSpaceDeploy](https://github.com/kfcx/HFSpaceDeploy)一键部署
|
| 14 |
+
|
| 15 |
+
|
app.py
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Hugging Face Space 保活管理器
|
| 3 |
+
每隔5小时自动向配置的URL发送GET请求,防止空间休眠
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
import json
|
| 8 |
+
import time
|
| 9 |
+
import asyncio
|
| 10 |
+
import aiohttp
|
| 11 |
+
import threading
|
| 12 |
+
from datetime import datetime, timedelta
|
| 13 |
+
from flask import Flask, render_template, request, jsonify, redirect, url_for
|
| 14 |
+
from apscheduler.schedulers.background import BackgroundScheduler
|
| 15 |
+
from apscheduler.triggers.interval import IntervalTrigger
|
| 16 |
+
|
| 17 |
+
app = Flask(__name__)
|
| 18 |
+
|
| 19 |
+
# 数据存储文件
|
| 20 |
+
DATA_FILE = "urls.json"
|
| 21 |
+
LOG_FILE = "logs.json"
|
| 22 |
+
|
| 23 |
+
# 保活间隔(小时)
|
| 24 |
+
KEEPALIVE_INTERVAL_HOURS = 5
|
| 25 |
+
|
| 26 |
+
def load_urls():
|
| 27 |
+
"""加载已保存的URL列表"""
|
| 28 |
+
if os.path.exists(DATA_FILE):
|
| 29 |
+
try:
|
| 30 |
+
with open(DATA_FILE, 'r', encoding='utf-8') as f:
|
| 31 |
+
return json.load(f)
|
| 32 |
+
except:
|
| 33 |
+
return []
|
| 34 |
+
return []
|
| 35 |
+
|
| 36 |
+
def save_urls(urls):
|
| 37 |
+
"""保存URL列表"""
|
| 38 |
+
with open(DATA_FILE, 'w', encoding='utf-8') as f:
|
| 39 |
+
json.dump(urls, f, ensure_ascii=False, indent=2)
|
| 40 |
+
|
| 41 |
+
def load_logs():
|
| 42 |
+
"""加载日志"""
|
| 43 |
+
if os.path.exists(LOG_FILE):
|
| 44 |
+
try:
|
| 45 |
+
with open(LOG_FILE, 'r', encoding='utf-8') as f:
|
| 46 |
+
logs = json.load(f)
|
| 47 |
+
# 只保留最近100条日志
|
| 48 |
+
return logs[-100:]
|
| 49 |
+
except:
|
| 50 |
+
return []
|
| 51 |
+
return []
|
| 52 |
+
|
| 53 |
+
def save_logs(logs):
|
| 54 |
+
"""保存日志"""
|
| 55 |
+
# 只保留最近100条
|
| 56 |
+
logs = logs[-100:]
|
| 57 |
+
with open(LOG_FILE, 'w', encoding='utf-8') as f:
|
| 58 |
+
json.dump(logs, f, ensure_ascii=False, indent=2)
|
| 59 |
+
|
| 60 |
+
def add_log(url, status, message):
|
| 61 |
+
"""添加一条日志"""
|
| 62 |
+
logs = load_logs()
|
| 63 |
+
logs.append({
|
| 64 |
+
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
| 65 |
+
"url": url,
|
| 66 |
+
"status": status,
|
| 67 |
+
"message": message
|
| 68 |
+
})
|
| 69 |
+
save_logs(logs)
|
| 70 |
+
|
| 71 |
+
async def ping_url(session, url_data):
|
| 72 |
+
"""异步发送GET请求到指定URL"""
|
| 73 |
+
url = url_data.get("url", "")
|
| 74 |
+
name = url_data.get("name", url)
|
| 75 |
+
|
| 76 |
+
try:
|
| 77 |
+
async with session.get(url, timeout=aiohttp.ClientTimeout(total=30)) as response:
|
| 78 |
+
status = response.status
|
| 79 |
+
if status == 200:
|
| 80 |
+
add_log(name, "success", f"状态码: {status}")
|
| 81 |
+
return {"url": url, "name": name, "status": "success", "code": status}
|
| 82 |
+
else:
|
| 83 |
+
add_log(name, "warning", f"状态码: {status}")
|
| 84 |
+
return {"url": url, "name": name, "status": "warning", "code": status}
|
| 85 |
+
except asyncio.TimeoutError:
|
| 86 |
+
add_log(name, "error", "请求超时")
|
| 87 |
+
return {"url": url, "name": name, "status": "error", "message": "请求超时"}
|
| 88 |
+
except Exception as e:
|
| 89 |
+
add_log(name, "error", str(e)[:100])
|
| 90 |
+
return {"url": url, "name": name, "status": "error", "message": str(e)[:100]}
|
| 91 |
+
|
| 92 |
+
async def ping_all_urls():
|
| 93 |
+
"""异步ping所有URL"""
|
| 94 |
+
urls = load_urls()
|
| 95 |
+
if not urls:
|
| 96 |
+
add_log("系统", "info", "没有配置需要保活的URL")
|
| 97 |
+
return []
|
| 98 |
+
|
| 99 |
+
add_log("系统", "info", f"开始保活任务,共 {len(urls)} 个URL")
|
| 100 |
+
|
| 101 |
+
async with aiohttp.ClientSession() as session:
|
| 102 |
+
tasks = [ping_url(session, url_data) for url_data in urls]
|
| 103 |
+
results = await asyncio.gather(*tasks)
|
| 104 |
+
|
| 105 |
+
success_count = sum(1 for r in results if r.get("status") == "success")
|
| 106 |
+
add_log("系统", "info", f"保活任务完成,成功: {success_count}/{len(urls)}")
|
| 107 |
+
|
| 108 |
+
return results
|
| 109 |
+
|
| 110 |
+
def run_keepalive_job():
|
| 111 |
+
"""运行保活任务的同步包装器"""
|
| 112 |
+
loop = asyncio.new_event_loop()
|
| 113 |
+
asyncio.set_event_loop(loop)
|
| 114 |
+
try:
|
| 115 |
+
loop.run_until_complete(ping_all_urls())
|
| 116 |
+
finally:
|
| 117 |
+
loop.close()
|
| 118 |
+
|
| 119 |
+
# 初始化调度器
|
| 120 |
+
scheduler = BackgroundScheduler()
|
| 121 |
+
scheduler.add_job(
|
| 122 |
+
run_keepalive_job,
|
| 123 |
+
trigger=IntervalTrigger(hours=KEEPALIVE_INTERVAL_HOURS),
|
| 124 |
+
id='keepalive_job',
|
| 125 |
+
name='URL保活任务',
|
| 126 |
+
replace_existing=True
|
| 127 |
+
)
|
| 128 |
+
|
| 129 |
+
@app.route('/')
|
| 130 |
+
def index():
|
| 131 |
+
"""主页"""
|
| 132 |
+
urls = load_urls()
|
| 133 |
+
logs = load_logs()
|
| 134 |
+
# 获取下次执行时间
|
| 135 |
+
job = scheduler.get_job('keepalive_job')
|
| 136 |
+
next_run = job.next_run_time.strftime("%Y-%m-%d %H:%M:%S") if job and job.next_run_time else "未调度"
|
| 137 |
+
|
| 138 |
+
return render_template('index.html',
|
| 139 |
+
urls=urls,
|
| 140 |
+
logs=reversed(logs), # 最新的在前
|
| 141 |
+
next_run=next_run,
|
| 142 |
+
interval=KEEPALIVE_INTERVAL_HOURS)
|
| 143 |
+
|
| 144 |
+
@app.route('/api/urls', methods=['GET'])
|
| 145 |
+
def get_urls():
|
| 146 |
+
"""获取所有URL"""
|
| 147 |
+
return jsonify(load_urls())
|
| 148 |
+
|
| 149 |
+
@app.route('/api/urls', methods=['POST'])
|
| 150 |
+
def add_url():
|
| 151 |
+
"""添加新URL"""
|
| 152 |
+
data = request.json
|
| 153 |
+
url = data.get('url', '').strip()
|
| 154 |
+
name = data.get('name', '').strip()
|
| 155 |
+
|
| 156 |
+
if not url:
|
| 157 |
+
return jsonify({"error": "URL不能为空"}), 400
|
| 158 |
+
|
| 159 |
+
# 确保URL有协议前缀
|
| 160 |
+
if not url.startswith(('http://', 'https://')):
|
| 161 |
+
url = 'https://' + url
|
| 162 |
+
|
| 163 |
+
urls = load_urls()
|
| 164 |
+
|
| 165 |
+
# 检查是否已存在
|
| 166 |
+
for u in urls:
|
| 167 |
+
if u.get('url') == url:
|
| 168 |
+
return jsonify({"error": "URL已存在"}), 400
|
| 169 |
+
|
| 170 |
+
# 使用URL作为默认名称
|
| 171 |
+
if not name:
|
| 172 |
+
name = url.split('//')[1].split('/')[0] if '//' in url else url
|
| 173 |
+
|
| 174 |
+
urls.append({
|
| 175 |
+
"url": url,
|
| 176 |
+
"name": name,
|
| 177 |
+
"added_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 178 |
+
})
|
| 179 |
+
|
| 180 |
+
save_urls(urls)
|
| 181 |
+
add_log(name, "info", "添加到保活列表")
|
| 182 |
+
|
| 183 |
+
return jsonify({"success": True, "message": "URL添加成功"})
|
| 184 |
+
|
| 185 |
+
@app.route('/api/urls/<int:index>', methods=['DELETE'])
|
| 186 |
+
def delete_url(index):
|
| 187 |
+
"""删除URL"""
|
| 188 |
+
urls = load_urls()
|
| 189 |
+
|
| 190 |
+
if 0 <= index < len(urls):
|
| 191 |
+
removed = urls.pop(index)
|
| 192 |
+
save_urls(urls)
|
| 193 |
+
add_log(removed.get('name', removed.get('url')), "info", "从保活列表移除")
|
| 194 |
+
return jsonify({"success": True})
|
| 195 |
+
|
| 196 |
+
return jsonify({"error": "索引无效"}), 400
|
| 197 |
+
|
| 198 |
+
@app.route('/api/ping', methods=['POST'])
|
| 199 |
+
def manual_ping():
|
| 200 |
+
"""手动触发保活"""
|
| 201 |
+
loop = asyncio.new_event_loop()
|
| 202 |
+
asyncio.set_event_loop(loop)
|
| 203 |
+
try:
|
| 204 |
+
results = loop.run_until_complete(ping_all_urls())
|
| 205 |
+
finally:
|
| 206 |
+
loop.close()
|
| 207 |
+
|
| 208 |
+
return jsonify({"success": True, "results": results})
|
| 209 |
+
|
| 210 |
+
@app.route('/api/ping/<int:index>', methods=['POST'])
|
| 211 |
+
def ping_single(index):
|
| 212 |
+
"""ping单个URL"""
|
| 213 |
+
urls = load_urls()
|
| 214 |
+
|
| 215 |
+
if 0 <= index < len(urls):
|
| 216 |
+
loop = asyncio.new_event_loop()
|
| 217 |
+
asyncio.set_event_loop(loop)
|
| 218 |
+
try:
|
| 219 |
+
async def ping_one():
|
| 220 |
+
async with aiohttp.ClientSession() as session:
|
| 221 |
+
return await ping_url(session, urls[index])
|
| 222 |
+
result = loop.run_until_complete(ping_one())
|
| 223 |
+
finally:
|
| 224 |
+
loop.close()
|
| 225 |
+
|
| 226 |
+
return jsonify({"success": True, "result": result})
|
| 227 |
+
|
| 228 |
+
return jsonify({"error": "索引无效"}), 400
|
| 229 |
+
|
| 230 |
+
@app.route('/api/logs', methods=['GET'])
|
| 231 |
+
def get_logs():
|
| 232 |
+
"""获取日志"""
|
| 233 |
+
return jsonify(load_logs())
|
| 234 |
+
|
| 235 |
+
@app.route('/api/logs', methods=['DELETE'])
|
| 236 |
+
def clear_logs():
|
| 237 |
+
"""清空日志"""
|
| 238 |
+
save_logs([])
|
| 239 |
+
return jsonify({"success": True})
|
| 240 |
+
|
| 241 |
+
@app.route('/api/status', methods=['GET'])
|
| 242 |
+
def get_status():
|
| 243 |
+
"""获取系统状态"""
|
| 244 |
+
job = scheduler.get_job('keepalive_job')
|
| 245 |
+
urls = load_urls()
|
| 246 |
+
|
| 247 |
+
return jsonify({
|
| 248 |
+
"url_count": len(urls),
|
| 249 |
+
"interval_hours": KEEPALIVE_INTERVAL_HOURS,
|
| 250 |
+
"next_run": job.next_run_time.strftime("%Y-%m-%d %H:%M:%S") if job and job.next_run_time else None,
|
| 251 |
+
"scheduler_running": scheduler.running
|
| 252 |
+
})
|
| 253 |
+
|
| 254 |
+
if __name__ == '__main__':
|
| 255 |
+
# 启动调度器
|
| 256 |
+
scheduler.start()
|
| 257 |
+
add_log("系统", "info", "保活管理器启动")
|
| 258 |
+
|
| 259 |
+
# 运行Flask应用
|
| 260 |
+
app.run(host='0.0.0.0', port=7860, debug=False)
|
requirements.txt
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
flask>=2.3.0
|
| 2 |
+
apscheduler>=3.10.0
|
| 3 |
+
aiohttp>=3.8.0
|
| 4 |
+
gunicorn>=21.0.0
|
static/script.js
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* HuggingFace 保活管理器 - 前端交互脚本
|
| 3 |
+
*/
|
| 4 |
+
|
| 5 |
+
// Toast 通知函数
|
| 6 |
+
function showToast(message, type = 'info') {
|
| 7 |
+
const container = document.getElementById('toast-container');
|
| 8 |
+
const toast = document.createElement('div');
|
| 9 |
+
toast.className = `toast toast-${type}`;
|
| 10 |
+
|
| 11 |
+
const icons = {
|
| 12 |
+
success: '<svg viewBox="0 0 24 24" fill="none"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" stroke="currentColor" stroke-width="2"/><polyline points="22,4 12,14.01 9,11.01" stroke="currentColor" stroke-width="2"/></svg>',
|
| 13 |
+
error: '<svg viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/><line x1="15" y1="9" x2="9" y2="15" stroke="currentColor" stroke-width="2"/><line x1="9" y1="9" x2="15" y2="15" stroke="currentColor" stroke-width="2"/></svg>',
|
| 14 |
+
warning: '<svg viewBox="0 0 24 24" fill="none"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" stroke="currentColor" stroke-width="2"/><line x1="12" y1="9" x2="12" y2="13" stroke="currentColor" stroke-width="2"/><line x1="12" y1="17" x2="12.01" y2="17" stroke="currentColor" stroke-width="2"/></svg>',
|
| 15 |
+
info: '<svg viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/><line x1="12" y1="16" x2="12" y2="12" stroke="currentColor" stroke-width="2"/><line x1="12" y1="8" x2="12.01" y2="8" stroke="currentColor" stroke-width="2"/></svg>'
|
| 16 |
+
};
|
| 17 |
+
|
| 18 |
+
toast.innerHTML = `
|
| 19 |
+
<span class="toast-icon">${icons[type] || icons.info}</span>
|
| 20 |
+
<span class="toast-message">${message}</span>
|
| 21 |
+
<button class="toast-close" onclick="this.parentElement.remove()">
|
| 22 |
+
<svg viewBox="0 0 24 24" fill="none"><line x1="18" y1="6" x2="6" y2="18" stroke="currentColor" stroke-width="2"/><line x1="6" y1="6" x2="18" y2="18" stroke="currentColor" stroke-width="2"/></svg>
|
| 23 |
+
</button>
|
| 24 |
+
`;
|
| 25 |
+
|
| 26 |
+
container.appendChild(toast);
|
| 27 |
+
|
| 28 |
+
// 自动移除
|
| 29 |
+
setTimeout(() => {
|
| 30 |
+
toast.classList.add('fade-out');
|
| 31 |
+
setTimeout(() => toast.remove(), 300);
|
| 32 |
+
}, 4000);
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
// 刷新页面
|
| 36 |
+
function refreshPage() {
|
| 37 |
+
location.reload();
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
// 添加URL
|
| 41 |
+
document.getElementById('add-url-form').addEventListener('submit', async (e) => {
|
| 42 |
+
e.preventDefault();
|
| 43 |
+
|
| 44 |
+
const urlInput = document.getElementById('url-input');
|
| 45 |
+
const nameInput = document.getElementById('name-input');
|
| 46 |
+
const btn = e.target.querySelector('button[type="submit"]');
|
| 47 |
+
|
| 48 |
+
const url = urlInput.value.trim();
|
| 49 |
+
const name = nameInput.value.trim();
|
| 50 |
+
|
| 51 |
+
if (!url) {
|
| 52 |
+
showToast('请输入URL地址', 'error');
|
| 53 |
+
return;
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
btn.disabled = true;
|
| 57 |
+
btn.innerHTML = '<span class="loading"></span> 添加中...';
|
| 58 |
+
|
| 59 |
+
try {
|
| 60 |
+
const response = await fetch('/api/urls', {
|
| 61 |
+
method: 'POST',
|
| 62 |
+
headers: { 'Content-Type': 'application/json' },
|
| 63 |
+
body: JSON.stringify({ url, name })
|
| 64 |
+
});
|
| 65 |
+
|
| 66 |
+
const data = await response.json();
|
| 67 |
+
|
| 68 |
+
if (response.ok) {
|
| 69 |
+
showToast('URL添加成功', 'success');
|
| 70 |
+
setTimeout(refreshPage, 500);
|
| 71 |
+
} else {
|
| 72 |
+
showToast(data.error || '添加失败', 'error');
|
| 73 |
+
}
|
| 74 |
+
} catch (error) {
|
| 75 |
+
showToast('网络错误,请重试', 'error');
|
| 76 |
+
} finally {
|
| 77 |
+
btn.disabled = false;
|
| 78 |
+
btn.innerHTML = '<svg viewBox="0 0 24 24" fill="none"><line x1="12" y1="5" x2="12" y2="19" stroke="currentColor" stroke-width="2"/><line x1="5" y1="12" x2="19" y2="12" stroke="currentColor" stroke-width="2"/></svg> 添加';
|
| 79 |
+
}
|
| 80 |
+
});
|
| 81 |
+
|
| 82 |
+
// 删除URL
|
| 83 |
+
document.querySelectorAll('.delete-btn').forEach(btn => {
|
| 84 |
+
btn.addEventListener('click', async () => {
|
| 85 |
+
const index = btn.dataset.index;
|
| 86 |
+
|
| 87 |
+
if (!confirm('确定要删除这个URL吗?')) return;
|
| 88 |
+
|
| 89 |
+
btn.disabled = true;
|
| 90 |
+
|
| 91 |
+
try {
|
| 92 |
+
const response = await fetch(`/api/urls/${index}`, {
|
| 93 |
+
method: 'DELETE'
|
| 94 |
+
});
|
| 95 |
+
|
| 96 |
+
if (response.ok) {
|
| 97 |
+
showToast('删除成功', 'success');
|
| 98 |
+
setTimeout(refreshPage, 500);
|
| 99 |
+
} else {
|
| 100 |
+
showToast('删除失败', 'error');
|
| 101 |
+
}
|
| 102 |
+
} catch (error) {
|
| 103 |
+
showToast('网络错误', 'error');
|
| 104 |
+
} finally {
|
| 105 |
+
btn.disabled = false;
|
| 106 |
+
}
|
| 107 |
+
});
|
| 108 |
+
});
|
| 109 |
+
|
| 110 |
+
// Ping单个URL
|
| 111 |
+
document.querySelectorAll('.ping-btn').forEach(btn => {
|
| 112 |
+
btn.addEventListener('click', async () => {
|
| 113 |
+
const index = btn.dataset.index;
|
| 114 |
+
const urlItem = btn.closest('.url-item');
|
| 115 |
+
const urlName = urlItem.querySelector('.url-name').textContent;
|
| 116 |
+
|
| 117 |
+
btn.disabled = true;
|
| 118 |
+
btn.innerHTML = '<span class="loading"></span>';
|
| 119 |
+
|
| 120 |
+
try {
|
| 121 |
+
const response = await fetch(`/api/ping/${index}`, {
|
| 122 |
+
method: 'POST'
|
| 123 |
+
});
|
| 124 |
+
|
| 125 |
+
const data = await response.json();
|
| 126 |
+
|
| 127 |
+
if (data.result && data.result.status === 'success') {
|
| 128 |
+
showToast(`${urlName} 连接成功 (${data.result.code})`, 'success');
|
| 129 |
+
} else if (data.result && data.result.status === 'warning') {
|
| 130 |
+
showToast(`${urlName} 返回状态码: ${data.result.code}`, 'warning');
|
| 131 |
+
} else {
|
| 132 |
+
showToast(`${urlName} 连接失败: ${data.result?.message || '未知错误'}`, 'error');
|
| 133 |
+
}
|
| 134 |
+
} catch (error) {
|
| 135 |
+
showToast('网络错误', 'error');
|
| 136 |
+
} finally {
|
| 137 |
+
btn.disabled = false;
|
| 138 |
+
btn.innerHTML = '<svg viewBox="0 0 24 24" fill="none"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" stroke="currentColor" stroke-width="2"/><polyline points="22,4 12,14.01 9,11.01" stroke="currentColor" stroke-width="2"/></svg>';
|
| 139 |
+
}
|
| 140 |
+
});
|
| 141 |
+
});
|
| 142 |
+
|
| 143 |
+
// 全部保活
|
| 144 |
+
document.getElementById('ping-all-btn')?.addEventListener('click', async () => {
|
| 145 |
+
const btn = document.getElementById('ping-all-btn');
|
| 146 |
+
|
| 147 |
+
btn.disabled = true;
|
| 148 |
+
btn.innerHTML = '<span class="loading"></span> 执行中...';
|
| 149 |
+
|
| 150 |
+
showToast('正在执行保活任务...', 'info');
|
| 151 |
+
|
| 152 |
+
try {
|
| 153 |
+
const response = await fetch('/api/ping', {
|
| 154 |
+
method: 'POST'
|
| 155 |
+
});
|
| 156 |
+
|
| 157 |
+
const data = await response.json();
|
| 158 |
+
|
| 159 |
+
if (data.results) {
|
| 160 |
+
const successCount = data.results.filter(r => r.status === 'success').length;
|
| 161 |
+
showToast(`保活完成: ${successCount}/${data.results.length} 成功`,
|
| 162 |
+
successCount === data.results.length ? 'success' : 'warning');
|
| 163 |
+
setTimeout(refreshPage, 1000);
|
| 164 |
+
}
|
| 165 |
+
} catch (error) {
|
| 166 |
+
showToast('执行失败', 'error');
|
| 167 |
+
} finally {
|
| 168 |
+
btn.disabled = false;
|
| 169 |
+
btn.innerHTML = '<svg viewBox="0 0 24 24" fill="none"><path d="M23 4v6h-6M1 20v-6h6" stroke="currentColor" stroke-width="2"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15" stroke="currentColor" stroke-width="2"/></svg> 立即全部保活';
|
| 170 |
+
}
|
| 171 |
+
});
|
| 172 |
+
|
| 173 |
+
// 清空日志
|
| 174 |
+
document.getElementById('clear-logs-btn')?.addEventListener('click', async () => {
|
| 175 |
+
if (!confirm('确定要清空所有日志吗?')) return;
|
| 176 |
+
|
| 177 |
+
try {
|
| 178 |
+
const response = await fetch('/api/logs', {
|
| 179 |
+
method: 'DELETE'
|
| 180 |
+
});
|
| 181 |
+
|
| 182 |
+
if (response.ok) {
|
| 183 |
+
showToast('日志已清空', 'success');
|
| 184 |
+
setTimeout(refreshPage, 500);
|
| 185 |
+
}
|
| 186 |
+
} catch (error) {
|
| 187 |
+
showToast('清空失败', 'error');
|
| 188 |
+
}
|
| 189 |
+
});
|
| 190 |
+
|
| 191 |
+
// 页面加载完成
|
| 192 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 193 |
+
console.log('HuggingFace 保活管理器已加载');
|
| 194 |
+
});
|
static/style.css
ADDED
|
@@ -0,0 +1,394 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* HuggingFace 保活管理器 - 深色主题 */
|
| 2 |
+
|
| 3 |
+
:root {
|
| 4 |
+
--primary-hue: 260;
|
| 5 |
+
--primary: hsl(var(--primary-hue), 100%, 65%);
|
| 6 |
+
--primary-light: hsl(var(--primary-hue), 100%, 75%);
|
| 7 |
+
--primary-dark: hsl(var(--primary-hue), 80%, 50%);
|
| 8 |
+
--primary-glow: hsla(var(--primary-hue), 100%, 65%, 0.4);
|
| 9 |
+
--accent: hsl(180, 100%, 50%);
|
| 10 |
+
--success: #10b981;
|
| 11 |
+
--success-bg: rgba(16, 185, 129, 0.15);
|
| 12 |
+
--warning: #f59e0b;
|
| 13 |
+
--warning-bg: rgba(245, 158, 11, 0.15);
|
| 14 |
+
--error: #ef4444;
|
| 15 |
+
--error-bg: rgba(239, 68, 68, 0.15);
|
| 16 |
+
--info: #3b82f6;
|
| 17 |
+
--info-bg: rgba(59, 130, 246, 0.15);
|
| 18 |
+
--bg-primary: #0a0a0f;
|
| 19 |
+
--bg-secondary: #12121a;
|
| 20 |
+
--bg-tertiary: #1a1a25;
|
| 21 |
+
--bg-card: rgba(26, 26, 40, 0.7);
|
| 22 |
+
--text-primary: #f8fafc;
|
| 23 |
+
--text-secondary: #94a3b8;
|
| 24 |
+
--text-tertiary: #64748b;
|
| 25 |
+
--border-color: rgba(148, 163, 184, 0.1);
|
| 26 |
+
--border-glow: rgba(148, 163, 184, 0.2);
|
| 27 |
+
--shadow-md: 0 4px 20px rgba(0, 0, 0, 0.4);
|
| 28 |
+
--shadow-lg: 0 8px 40px rgba(0, 0, 0, 0.5);
|
| 29 |
+
--shadow-glow: 0 0 30px var(--primary-glow);
|
| 30 |
+
--radius-sm: 8px;
|
| 31 |
+
--radius-md: 12px;
|
| 32 |
+
--radius-lg: 16px;
|
| 33 |
+
--transition-fast: 150ms ease;
|
| 34 |
+
--transition-normal: 250ms ease;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
| 38 |
+
html { font-size: 16px; scroll-behavior: smooth; }
|
| 39 |
+
|
| 40 |
+
body {
|
| 41 |
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
| 42 |
+
background: var(--bg-primary);
|
| 43 |
+
color: var(--text-primary);
|
| 44 |
+
line-height: 1.6;
|
| 45 |
+
min-height: 100vh;
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
body::before {
|
| 49 |
+
content: '';
|
| 50 |
+
position: fixed;
|
| 51 |
+
inset: 0;
|
| 52 |
+
background:
|
| 53 |
+
radial-gradient(ellipse 80% 50% at 50% -20%, hsla(var(--primary-hue), 100%, 50%, 0.15), transparent),
|
| 54 |
+
radial-gradient(ellipse 60% 40% at 100% 0%, hsla(180, 100%, 50%, 0.1), transparent);
|
| 55 |
+
pointer-events: none;
|
| 56 |
+
z-index: -1;
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
a { color: var(--primary-light); text-decoration: none; transition: color var(--transition-fast); }
|
| 60 |
+
a:hover { color: var(--accent); }
|
| 61 |
+
|
| 62 |
+
.app-container {
|
| 63 |
+
max-width: 1200px;
|
| 64 |
+
margin: 0 auto;
|
| 65 |
+
padding: 0 24px;
|
| 66 |
+
min-height: 100vh;
|
| 67 |
+
display: flex;
|
| 68 |
+
flex-direction: column;
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
.header {
|
| 72 |
+
padding: 24px 0;
|
| 73 |
+
border-bottom: 1px solid var(--border-color);
|
| 74 |
+
margin-bottom: 32px;
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
.header-content {
|
| 78 |
+
display: flex;
|
| 79 |
+
justify-content: space-between;
|
| 80 |
+
align-items: center;
|
| 81 |
+
flex-wrap: wrap;
|
| 82 |
+
gap: 16px;
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
.logo { display: flex; align-items: center; gap: 16px; }
|
| 86 |
+
|
| 87 |
+
.logo-icon {
|
| 88 |
+
width: 48px;
|
| 89 |
+
height: 48px;
|
| 90 |
+
background: linear-gradient(135deg, var(--primary), var(--accent));
|
| 91 |
+
border-radius: var(--radius-md);
|
| 92 |
+
display: flex;
|
| 93 |
+
align-items: center;
|
| 94 |
+
justify-content: center;
|
| 95 |
+
box-shadow: var(--shadow-glow);
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
.logo-icon svg { width: 28px; height: 28px; color: white; }
|
| 99 |
+
|
| 100 |
+
.logo-text h1 {
|
| 101 |
+
font-size: 1.5rem;
|
| 102 |
+
font-weight: 700;
|
| 103 |
+
background: linear-gradient(135deg, var(--text-primary), var(--primary-light));
|
| 104 |
+
-webkit-background-clip: text;
|
| 105 |
+
-webkit-text-fill-color: transparent;
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
.logo-text p { font-size: 0.85rem; color: var(--text-tertiary); }
|
| 109 |
+
|
| 110 |
+
.status-badge {
|
| 111 |
+
display: flex;
|
| 112 |
+
align-items: center;
|
| 113 |
+
gap: 8px;
|
| 114 |
+
padding: 8px 16px;
|
| 115 |
+
background: var(--success-bg);
|
| 116 |
+
border: 1px solid rgba(16, 185, 129, 0.3);
|
| 117 |
+
border-radius: 100px;
|
| 118 |
+
font-size: 0.875rem;
|
| 119 |
+
color: var(--success);
|
| 120 |
+
font-weight: 500;
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
.status-dot {
|
| 124 |
+
width: 8px;
|
| 125 |
+
height: 8px;
|
| 126 |
+
background: var(--success);
|
| 127 |
+
border-radius: 50%;
|
| 128 |
+
animation: pulse 2s infinite;
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
@keyframes pulse {
|
| 132 |
+
0%, 100% { opacity: 1; transform: scale(1); }
|
| 133 |
+
50% { opacity: 0.6; transform: scale(1.1); }
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
.main-content { flex: 1; display: flex; flex-direction: column; gap: 24px; }
|
| 137 |
+
|
| 138 |
+
.status-section {
|
| 139 |
+
display: grid;
|
| 140 |
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
| 141 |
+
gap: 16px;
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
.status-card {
|
| 145 |
+
background: var(--bg-card);
|
| 146 |
+
backdrop-filter: blur(20px);
|
| 147 |
+
border: 1px solid var(--border-color);
|
| 148 |
+
border-radius: var(--radius-lg);
|
| 149 |
+
padding: 24px;
|
| 150 |
+
display: flex;
|
| 151 |
+
align-items: center;
|
| 152 |
+
gap: 16px;
|
| 153 |
+
transition: all var(--transition-normal);
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
.status-card:hover {
|
| 157 |
+
border-color: var(--border-glow);
|
| 158 |
+
transform: translateY(-2px);
|
| 159 |
+
box-shadow: var(--shadow-md);
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
.status-icon {
|
| 163 |
+
width: 48px;
|
| 164 |
+
height: 48px;
|
| 165 |
+
border-radius: var(--radius-md);
|
| 166 |
+
display: flex;
|
| 167 |
+
align-items: center;
|
| 168 |
+
justify-content: center;
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
.status-icon svg { width: 24px; height: 24px; }
|
| 172 |
+
.urls-icon { background: linear-gradient(135deg, hsla(260, 100%, 65%, 0.2), hsla(260, 100%, 65%, 0.1)); color: var(--primary-light); }
|
| 173 |
+
.interval-icon { background: linear-gradient(135deg, hsla(180, 100%, 50%, 0.2), hsla(180, 100%, 50%, 0.1)); color: var(--accent); }
|
| 174 |
+
.next-run-icon { background: linear-gradient(135deg, hsla(45, 100%, 50%, 0.2), hsla(45, 100%, 50%, 0.1)); color: #fbbf24; }
|
| 175 |
+
|
| 176 |
+
.status-info { display: flex; flex-direction: column; }
|
| 177 |
+
.status-value { font-size: 1.5rem; font-weight: 700; color: var(--text-primary); }
|
| 178 |
+
.next-run-time { font-size: 0.95rem; }
|
| 179 |
+
.status-label { font-size: 0.85rem; color: var(--text-tertiary); }
|
| 180 |
+
|
| 181 |
+
.glass-card {
|
| 182 |
+
background: var(--bg-card);
|
| 183 |
+
backdrop-filter: blur(20px);
|
| 184 |
+
border: 1px solid var(--border-color);
|
| 185 |
+
border-radius: var(--radius-lg);
|
| 186 |
+
padding: 24px;
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
.glass-card h2 {
|
| 190 |
+
display: flex;
|
| 191 |
+
align-items: center;
|
| 192 |
+
gap: 12px;
|
| 193 |
+
font-size: 1.125rem;
|
| 194 |
+
font-weight: 600;
|
| 195 |
+
margin-bottom: 20px;
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
.glass-card h2 svg { width: 22px; height: 22px; color: var(--primary-light); }
|
| 199 |
+
|
| 200 |
+
.add-form .form-row { display: flex; gap: 16px; flex-wrap: wrap; }
|
| 201 |
+
.form-group { flex: 1; min-width: 200px; display: flex; flex-direction: column; gap: 8px; }
|
| 202 |
+
.form-group.name-group { flex: 0.5; min-width: 150px; }
|
| 203 |
+
.form-group label { font-size: 0.875rem; font-weight: 500; color: var(--text-secondary); }
|
| 204 |
+
|
| 205 |
+
.form-group input {
|
| 206 |
+
padding: 12px 16px;
|
| 207 |
+
background: var(--bg-secondary);
|
| 208 |
+
border: 1px solid var(--border-color);
|
| 209 |
+
border-radius: var(--radius-sm);
|
| 210 |
+
color: var(--text-primary);
|
| 211 |
+
font-size: 0.95rem;
|
| 212 |
+
transition: all var(--transition-fast);
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
.form-group input::placeholder { color: var(--text-tertiary); }
|
| 216 |
+
.form-group input:focus { outline: none; border-color: var(--primary); box-shadow: 0 0 0 3px var(--primary-glow); }
|
| 217 |
+
|
| 218 |
+
.btn {
|
| 219 |
+
display: inline-flex;
|
| 220 |
+
align-items: center;
|
| 221 |
+
justify-content: center;
|
| 222 |
+
gap: 8px;
|
| 223 |
+
padding: 12px 24px;
|
| 224 |
+
font-size: 0.95rem;
|
| 225 |
+
font-weight: 500;
|
| 226 |
+
border: none;
|
| 227 |
+
border-radius: var(--radius-sm);
|
| 228 |
+
cursor: pointer;
|
| 229 |
+
transition: all var(--transition-fast);
|
| 230 |
+
white-space: nowrap;
|
| 231 |
+
}
|
| 232 |
+
|
| 233 |
+
.btn svg { width: 18px; height: 18px; }
|
| 234 |
+
|
| 235 |
+
.btn-primary {
|
| 236 |
+
background: linear-gradient(135deg, var(--primary), var(--primary-dark));
|
| 237 |
+
color: white;
|
| 238 |
+
box-shadow: 0 4px 15px var(--primary-glow);
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
.btn-primary:hover { transform: translateY(-2px); box-shadow: 0 6px 20px var(--primary-glow); }
|
| 242 |
+
.btn-secondary { background: var(--bg-tertiary); color: var(--text-primary); border: 1px solid var(--border-color); }
|
| 243 |
+
.btn-secondary:hover { background: var(--bg-secondary); border-color: var(--border-glow); }
|
| 244 |
+
.btn-small { padding: 8px 16px; font-size: 0.85rem; }
|
| 245 |
+
|
| 246 |
+
.btn-icon {
|
| 247 |
+
width: 40px;
|
| 248 |
+
height: 40px;
|
| 249 |
+
padding: 0;
|
| 250 |
+
background: var(--bg-tertiary);
|
| 251 |
+
border: 1px solid var(--border-color);
|
| 252 |
+
border-radius: var(--radius-sm);
|
| 253 |
+
color: var(--text-secondary);
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
.btn-icon:hover { background: var(--bg-secondary); color: var(--text-primary); }
|
| 257 |
+
.btn-icon.btn-danger:hover { background: var(--error-bg); color: var(--error); }
|
| 258 |
+
.btn:disabled { opacity: 0.6; cursor: not-allowed; }
|
| 259 |
+
|
| 260 |
+
.section-header {
|
| 261 |
+
display: flex;
|
| 262 |
+
justify-content: space-between;
|
| 263 |
+
align-items: center;
|
| 264 |
+
flex-wrap: wrap;
|
| 265 |
+
gap: 16px;
|
| 266 |
+
margin-bottom: 20px;
|
| 267 |
+
}
|
| 268 |
+
|
| 269 |
+
.section-header h2 { margin-bottom: 0; }
|
| 270 |
+
.urls-list { display: flex; flex-direction: column; gap: 12px; }
|
| 271 |
+
|
| 272 |
+
.url-item {
|
| 273 |
+
display: flex;
|
| 274 |
+
justify-content: space-between;
|
| 275 |
+
align-items: center;
|
| 276 |
+
padding: 16px 20px;
|
| 277 |
+
background: var(--bg-secondary);
|
| 278 |
+
border: 1px solid var(--border-color);
|
| 279 |
+
border-radius: var(--radius-md);
|
| 280 |
+
transition: all var(--transition-fast);
|
| 281 |
+
}
|
| 282 |
+
|
| 283 |
+
.url-item:hover { border-color: var(--border-glow); background: var(--bg-tertiary); }
|
| 284 |
+
.url-info { display: flex; flex-direction: column; gap: 4px; min-width: 0; flex: 1; }
|
| 285 |
+
.url-name { font-weight: 600; color: var(--text-primary); }
|
| 286 |
+
.url-link { font-size: 0.875rem; color: var(--primary-light); word-break: break-all; }
|
| 287 |
+
.url-added { font-size: 0.75rem; color: var(--text-tertiary); }
|
| 288 |
+
.url-actions { display: flex; gap: 8px; margin-left: 16px; }
|
| 289 |
+
|
| 290 |
+
.empty-state {
|
| 291 |
+
display: flex;
|
| 292 |
+
flex-direction: column;
|
| 293 |
+
align-items: center;
|
| 294 |
+
padding: 60px 20px;
|
| 295 |
+
text-align: center;
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
.empty-state svg { width: 64px; height: 64px; color: var(--text-tertiary); margin-bottom: 20px; opacity: 0.5; }
|
| 299 |
+
.empty-state p { font-size: 1.125rem; color: var(--text-secondary); margin-bottom: 8px; }
|
| 300 |
+
.empty-state span { font-size: 0.875rem; color: var(--text-tertiary); }
|
| 301 |
+
|
| 302 |
+
.logs-container {
|
| 303 |
+
max-height: 400px;
|
| 304 |
+
overflow-y: auto;
|
| 305 |
+
display: flex;
|
| 306 |
+
flex-direction: column;
|
| 307 |
+
gap: 8px;
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
+
.logs-container::-webkit-scrollbar { width: 6px; }
|
| 311 |
+
.logs-container::-webkit-scrollbar-track { background: var(--bg-secondary); border-radius: 3px; }
|
| 312 |
+
.logs-container::-webkit-scrollbar-thumb { background: var(--border-glow); border-radius: 3px; }
|
| 313 |
+
|
| 314 |
+
.log-item {
|
| 315 |
+
display: flex;
|
| 316 |
+
align-items: center;
|
| 317 |
+
gap: 12px;
|
| 318 |
+
padding: 12px 16px;
|
| 319 |
+
background: var(--bg-secondary);
|
| 320 |
+
border-radius: var(--radius-sm);
|
| 321 |
+
font-size: 0.85rem;
|
| 322 |
+
border-left: 3px solid transparent;
|
| 323 |
+
}
|
| 324 |
+
|
| 325 |
+
.log-time { color: var(--text-tertiary); font-family: monospace; white-space: nowrap; }
|
| 326 |
+
.log-url { color: var(--text-secondary); font-weight: 500; min-width: 120px; }
|
| 327 |
+
.log-message { color: var(--text-primary); flex: 1; }
|
| 328 |
+
|
| 329 |
+
.log-success { border-left-color: var(--success); background: var(--success-bg); }
|
| 330 |
+
.log-success .log-message { color: var(--success); }
|
| 331 |
+
.log-warning { border-left-color: var(--warning); background: var(--warning-bg); }
|
| 332 |
+
.log-warning .log-message { color: var(--warning); }
|
| 333 |
+
.log-error { border-left-color: var(--error); background: var(--error-bg); }
|
| 334 |
+
.log-error .log-message { color: var(--error); }
|
| 335 |
+
.log-info { border-left-color: var(--info); background: var(--info-bg); }
|
| 336 |
+
.log-info .log-message { color: var(--info); }
|
| 337 |
+
|
| 338 |
+
.empty-logs { display: flex; align-items: center; justify-content: center; padding: 40px; color: var(--text-tertiary); }
|
| 339 |
+
|
| 340 |
+
.footer { padding: 24px 0; margin-top: 32px; border-top: 1px solid var(--border-color); text-align: center; }
|
| 341 |
+
.footer p { color: var(--text-tertiary); font-size: 0.875rem; }
|
| 342 |
+
.footer-tip { margin-top: 8px; color: var(--text-secondary) !important; }
|
| 343 |
+
|
| 344 |
+
#toast-container { position: fixed; bottom: 24px; right: 24px; display: flex; flex-direction: column; gap: 12px; z-index: 1000; }
|
| 345 |
+
|
| 346 |
+
.toast {
|
| 347 |
+
display: flex;
|
| 348 |
+
align-items: center;
|
| 349 |
+
gap: 12px;
|
| 350 |
+
padding: 16px 20px;
|
| 351 |
+
background: var(--bg-card);
|
| 352 |
+
backdrop-filter: blur(20px);
|
| 353 |
+
border: 1px solid var(--border-color);
|
| 354 |
+
border-radius: var(--radius-md);
|
| 355 |
+
box-shadow: var(--shadow-lg);
|
| 356 |
+
animation: slideIn 0.3s ease;
|
| 357 |
+
min-width: 280px;
|
| 358 |
+
}
|
| 359 |
+
|
| 360 |
+
.toast.toast-success { border-left: 4px solid var(--success); }
|
| 361 |
+
.toast.toast-error { border-left: 4px solid var(--error); }
|
| 362 |
+
.toast-icon { width: 24px; height: 24px; }
|
| 363 |
+
.toast-success .toast-icon { color: var(--success); }
|
| 364 |
+
.toast-error .toast-icon { color: var(--error); }
|
| 365 |
+
.toast-message { flex: 1; font-size: 0.95rem; }
|
| 366 |
+
|
| 367 |
+
.toast-close {
|
| 368 |
+
background: none;
|
| 369 |
+
border: none;
|
| 370 |
+
color: var(--text-tertiary);
|
| 371 |
+
cursor: pointer;
|
| 372 |
+
padding: 4px;
|
| 373 |
+
display: flex;
|
| 374 |
+
}
|
| 375 |
+
|
| 376 |
+
.toast-close svg { width: 18px; height: 18px; }
|
| 377 |
+
|
| 378 |
+
@keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
|
| 379 |
+
.toast.fade-out { animation: slideOut 0.3s ease forwards; }
|
| 380 |
+
@keyframes slideOut { from { transform: translateX(0); opacity: 1; } to { transform: translateX(100%); opacity: 0; } }
|
| 381 |
+
|
| 382 |
+
@keyframes spin { to { transform: rotate(360deg); } }
|
| 383 |
+
|
| 384 |
+
@media (max-width: 768px) {
|
| 385 |
+
.header-content { flex-direction: column; align-items: flex-start; }
|
| 386 |
+
.status-section { grid-template-columns: 1fr; }
|
| 387 |
+
.add-form .form-row { flex-direction: column; }
|
| 388 |
+
.form-group, .form-group.name-group { flex: 1; min-width: 100%; }
|
| 389 |
+
.add-form .btn-primary { width: 100%; }
|
| 390 |
+
.section-header { flex-direction: column; align-items: flex-start; }
|
| 391 |
+
.section-header .btn { width: 100%; }
|
| 392 |
+
.url-item { flex-direction: column; align-items: flex-start; gap: 12px; }
|
| 393 |
+
.url-actions { margin-left: 0; width: 100%; justify-content: flex-end; }
|
| 394 |
+
}
|
templates/index.html
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="zh-CN">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>HuggingFace 保活管理器</title>
|
| 7 |
+
<meta name="description" content="管理和保活您的HuggingFace Spaces,防止因长时间不访问而休眠">
|
| 8 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 9 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 10 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
| 11 |
+
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
| 12 |
+
</head>
|
| 13 |
+
<body>
|
| 14 |
+
<div class="app-container">
|
| 15 |
+
<!-- 头部 -->
|
| 16 |
+
<header class="header">
|
| 17 |
+
<div class="header-content">
|
| 18 |
+
<div class="logo">
|
| 19 |
+
<div class="logo-icon">
|
| 20 |
+
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
| 21 |
+
<path d="M12 2L2 7L12 12L22 7L12 2Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
| 22 |
+
<path d="M2 17L12 22L22 17" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
| 23 |
+
<path d="M2 12L12 17L22 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
| 24 |
+
</svg>
|
| 25 |
+
</div>
|
| 26 |
+
<div class="logo-text">
|
| 27 |
+
<h1>HF 保活管理器</h1>
|
| 28 |
+
<p>HuggingFace Space Keepalive Manager</p>
|
| 29 |
+
</div>
|
| 30 |
+
</div>
|
| 31 |
+
<div class="status-badge">
|
| 32 |
+
<span class="status-dot"></span>
|
| 33 |
+
<span>运行中</span>
|
| 34 |
+
</div>
|
| 35 |
+
</div>
|
| 36 |
+
</header>
|
| 37 |
+
|
| 38 |
+
<!-- 主内容区 -->
|
| 39 |
+
<main class="main-content">
|
| 40 |
+
<!-- 状态卡片 -->
|
| 41 |
+
<section class="status-section">
|
| 42 |
+
<div class="status-card">
|
| 43 |
+
<div class="status-icon urls-icon">
|
| 44 |
+
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
| 45 |
+
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
| 46 |
+
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
| 47 |
+
</svg>
|
| 48 |
+
</div>
|
| 49 |
+
<div class="status-info">
|
| 50 |
+
<span class="status-value" id="url-count">{{ urls|length }}</span>
|
| 51 |
+
<span class="status-label">保活URL</span>
|
| 52 |
+
</div>
|
| 53 |
+
</div>
|
| 54 |
+
<div class="status-card">
|
| 55 |
+
<div class="status-icon interval-icon">
|
| 56 |
+
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
| 57 |
+
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/>
|
| 58 |
+
<polyline points="12,6 12,12 16,14" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
| 59 |
+
</svg>
|
| 60 |
+
</div>
|
| 61 |
+
<div class="status-info">
|
| 62 |
+
<span class="status-value">{{ interval }}小时</span>
|
| 63 |
+
<span class="status-label">保活间隔</span>
|
| 64 |
+
</div>
|
| 65 |
+
</div>
|
| 66 |
+
<div class="status-card">
|
| 67 |
+
<div class="status-icon next-run-icon">
|
| 68 |
+
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
| 69 |
+
<path d="M5 12h14M12 5l7 7-7 7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
| 70 |
+
</svg>
|
| 71 |
+
</div>
|
| 72 |
+
<div class="status-info">
|
| 73 |
+
<span class="status-value next-run-time" id="next-run">{{ next_run }}</span>
|
| 74 |
+
<span class="status-label">下次执行</span>
|
| 75 |
+
</div>
|
| 76 |
+
</div>
|
| 77 |
+
</section>
|
| 78 |
+
|
| 79 |
+
<!-- 添加URL表单 -->
|
| 80 |
+
<section class="add-url-section glass-card">
|
| 81 |
+
<h2>
|
| 82 |
+
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
| 83 |
+
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/>
|
| 84 |
+
<line x1="12" y1="8" x2="12" y2="16" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
| 85 |
+
<line x1="8" y1="12" x2="16" y2="12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
| 86 |
+
</svg>
|
| 87 |
+
添加保活URL
|
| 88 |
+
</h2>
|
| 89 |
+
<form id="add-url-form" class="add-form">
|
| 90 |
+
<div class="form-row">
|
| 91 |
+
<div class="form-group">
|
| 92 |
+
<label for="url-input">URL地址</label>
|
| 93 |
+
<input type="url" id="url-input" placeholder="https://your-space.hf.space" required>
|
| 94 |
+
</div>
|
| 95 |
+
<div class="form-group name-group">
|
| 96 |
+
<label for="name-input">名称 (可选)</label>
|
| 97 |
+
<input type="text" id="name-input" placeholder="我的空间">
|
| 98 |
+
</div>
|
| 99 |
+
<button type="submit" class="btn btn-primary">
|
| 100 |
+
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
| 101 |
+
<line x1="12" y1="5" x2="12" y2="19" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
| 102 |
+
<line x1="5" y1="12" x2="19" y2="12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
| 103 |
+
</svg>
|
| 104 |
+
添加
|
| 105 |
+
</button>
|
| 106 |
+
</div>
|
| 107 |
+
</form>
|
| 108 |
+
</section>
|
| 109 |
+
|
| 110 |
+
<!-- URL列表 -->
|
| 111 |
+
<section class="urls-section glass-card">
|
| 112 |
+
<div class="section-header">
|
| 113 |
+
<h2>
|
| 114 |
+
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
| 115 |
+
<path d="M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
| 116 |
+
</svg>
|
| 117 |
+
保活列表
|
| 118 |
+
</h2>
|
| 119 |
+
<button id="ping-all-btn" class="btn btn-secondary">
|
| 120 |
+
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
| 121 |
+
<path d="M23 4v6h-6M1 20v-6h6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
| 122 |
+
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
| 123 |
+
</svg>
|
| 124 |
+
立即全部保活
|
| 125 |
+
</button>
|
| 126 |
+
</div>
|
| 127 |
+
<div id="urls-list" class="urls-list">
|
| 128 |
+
{% if urls %}
|
| 129 |
+
{% for url in urls %}
|
| 130 |
+
<div class="url-item" data-index="{{ loop.index0 }}">
|
| 131 |
+
<div class="url-info">
|
| 132 |
+
<span class="url-name">{{ url.name }}</span>
|
| 133 |
+
<a href="{{ url.url }}" target="_blank" class="url-link">{{ url.url }}</a>
|
| 134 |
+
<span class="url-added">添加于: {{ url.added_at }}</span>
|
| 135 |
+
</div>
|
| 136 |
+
<div class="url-actions">
|
| 137 |
+
<button class="btn btn-icon ping-btn" title="测试连接" data-index="{{ loop.index0 }}">
|
| 138 |
+
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
| 139 |
+
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
| 140 |
+
<polyline points="22,4 12,14.01 9,11.01" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
| 141 |
+
</svg>
|
| 142 |
+
</button>
|
| 143 |
+
<button class="btn btn-icon btn-danger delete-btn" title="删除" data-index="{{ loop.index0 }}">
|
| 144 |
+
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
| 145 |
+
<polyline points="3,6 5,6 21,6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
| 146 |
+
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
| 147 |
+
</svg>
|
| 148 |
+
</button>
|
| 149 |
+
</div>
|
| 150 |
+
</div>
|
| 151 |
+
{% endfor %}
|
| 152 |
+
{% else %}
|
| 153 |
+
<div class="empty-state">
|
| 154 |
+
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
| 155 |
+
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
| 156 |
+
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
| 157 |
+
</svg>
|
| 158 |
+
<p>还没有添加任何保活URL</p>
|
| 159 |
+
<span>在上方输入框中添加需要保活的HuggingFace Space URL</span>
|
| 160 |
+
</div>
|
| 161 |
+
{% endif %}
|
| 162 |
+
</div>
|
| 163 |
+
</section>
|
| 164 |
+
|
| 165 |
+
<!-- 日志区域 -->
|
| 166 |
+
<section class="logs-section glass-card">
|
| 167 |
+
<div class="section-header">
|
| 168 |
+
<h2>
|
| 169 |
+
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
| 170 |
+
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
| 171 |
+
<polyline points="14,2 14,8 20,8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
| 172 |
+
<line x1="16" y1="13" x2="8" y2="13" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
| 173 |
+
<line x1="16" y1="17" x2="8" y2="17" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
| 174 |
+
<polyline points="10,9 9,9 8,9" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
| 175 |
+
</svg>
|
| 176 |
+
运行日志
|
| 177 |
+
</h2>
|
| 178 |
+
<button id="clear-logs-btn" class="btn btn-secondary btn-small">
|
| 179 |
+
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
| 180 |
+
<polyline points="3,6 5,6 21,6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
| 181 |
+
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
| 182 |
+
</svg>
|
| 183 |
+
清空日志
|
| 184 |
+
</button>
|
| 185 |
+
</div>
|
| 186 |
+
<div id="logs-container" class="logs-container">
|
| 187 |
+
{% for log in logs %}
|
| 188 |
+
<div class="log-item log-{{ log.status }}">
|
| 189 |
+
<span class="log-time">{{ log.timestamp }}</span>
|
| 190 |
+
<span class="log-url">{{ log.url }}</span>
|
| 191 |
+
<span class="log-message">{{ log.message }}</span>
|
| 192 |
+
</div>
|
| 193 |
+
{% else %}
|
| 194 |
+
<div class="empty-logs">
|
| 195 |
+
<p>暂无日志记录</p>
|
| 196 |
+
</div>
|
| 197 |
+
{% endfor %}
|
| 198 |
+
</div>
|
| 199 |
+
</section>
|
| 200 |
+
</main>
|
| 201 |
+
|
| 202 |
+
<!-- 页脚 -->
|
| 203 |
+
<footer class="footer">
|
| 204 |
+
<p>HuggingFace Space 保活管理器 © 2024</p>
|
| 205 |
+
<p class="footer-tip">💡 提示:保持此页面每日访问,即可确保所有配置的Space正常运行</p>
|
| 206 |
+
</footer>
|
| 207 |
+
</div>
|
| 208 |
+
|
| 209 |
+
<!-- Toast 通知 -->
|
| 210 |
+
<div id="toast-container"></div>
|
| 211 |
+
|
| 212 |
+
<script src="{{ url_for('static', filename='script.js') }}"></script>
|
| 213 |
+
</body>
|
| 214 |
+
</html>
|