HFHash789 commited on
Commit
1aa43d1
·
verified ·
1 Parent(s): 80c22fd

Upload folder using huggingface_hub

Browse files
Files changed (9) hide show
  1. .dockerignore +15 -0
  2. .gitignore +11 -0
  3. Dockerfile +24 -0
  4. README.md +10 -5
  5. app.py +260 -0
  6. requirements.txt +4 -0
  7. static/script.js +194 -0
  8. static/style.css +394 -0
  9. 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: Keephflive
3
- emoji: 👁
4
  colorFrom: blue
5
- colorTo: yellow
6
  sdk: docker
7
- pinned: false
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
1
  ---
2
+ title: "keephflive"
3
+ emoji: "🚀"
4
  colorFrom: blue
5
+ colorTo: green
6
  sdk: docker
7
+ app_port: 7860
8
  ---
9
 
10
+ ### 🚀 一键部署
11
+ [![Deploy with HFSpaceDeploy](https://img.shields.io/badge/Deploy_with-HFSpaceDeploy-green?style=social&logo=rocket)](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 保活管理器 &copy; 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>