epii-1 commited on
Commit
6a2845d
·
1 Parent(s): d65a9cb
.idea/.gitignore ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ # 默认忽略的文件
2
+ /shelf/
3
+ /workspace.xml
4
+ # 基于编辑器的 HTTP 客户端请求
5
+ /httpRequests/
6
+ # Datasource local storage ignored files
7
+ /dataSources/
8
+ /dataSources.local.xml
.idea/inspectionProfiles/Project_Default.xml ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <component name="InspectionProjectProfileManager">
2
+ <profile version="1.0">
3
+ <option name="myName" value="Project Default" />
4
+ <inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
5
+ <inspection_tool class="PyPackageRequirementsInspection" enabled="true" level="WARNING" enabled_by_default="true">
6
+ <option name="ignoredPackages">
7
+ <value>
8
+ <list size="20">
9
+ <item index="0" class="java.lang.String" itemvalue="httpx" />
10
+ <item index="1" class="java.lang.String" itemvalue="inquirer" />
11
+ <item index="2" class="java.lang.String" itemvalue="better_proxy" />
12
+ <item index="3" class="java.lang.String" itemvalue="curl_cffi" />
13
+ <item index="4" class="java.lang.String" itemvalue="art" />
14
+ <item index="5" class="java.lang.String" itemvalue="pydantic" />
15
+ <item index="6" class="java.lang.String" itemvalue="paddleocr" />
16
+ <item index="7" class="java.lang.String" itemvalue="PyYAML" />
17
+ <item index="8" class="java.lang.String" itemvalue="aiofiles" />
18
+ <item index="9" class="java.lang.String" itemvalue="rich" />
19
+ <item index="10" class="java.lang.String" itemvalue="numpy" />
20
+ <item index="11" class="java.lang.String" itemvalue="loguru" />
21
+ <item index="12" class="java.lang.String" itemvalue="imap_tools" />
22
+ <item index="13" class="java.lang.String" itemvalue="paddlepaddle" />
23
+ <item index="14" class="java.lang.String" itemvalue="names" />
24
+ <item index="15" class="java.lang.String" itemvalue="tortoise-orm" />
25
+ <item index="16" class="java.lang.String" itemvalue="colorama" />
26
+ <item index="17" class="java.lang.String" itemvalue="pytz" />
27
+ <item index="18" class="java.lang.String" itemvalue="urllib3" />
28
+ <item index="19" class="java.lang.String" itemvalue="aiocsv" />
29
+ </list>
30
+ </value>
31
+ </option>
32
+ </inspection_tool>
33
+ </profile>
34
+ </component>
.idea/inspectionProfiles/profiles_settings.xml ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ <component name="InspectionProjectProfileManager">
2
+ <settings>
3
+ <option name="USE_PROJECT_PROFILE" value="false" />
4
+ <version value="1.0" />
5
+ </settings>
6
+ </component>
.idea/misc.xml ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="ProjectRootManager" version="2" project-jdk-name="PPX" project-jdk-type="Python SDK" />
4
+ </project>
.idea/modules.xml ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="ProjectModuleManager">
4
+ <modules>
5
+ <module fileurl="file://$PROJECT_DIR$/.idea/panel.iml" filepath="$PROJECT_DIR$/.idea/panel.iml" />
6
+ </modules>
7
+ </component>
8
+ </project>
.idea/panel.iml ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <module type="PYTHON_MODULE" version="4">
3
+ <component name="Flask">
4
+ <option name="enabled" value="true" />
5
+ </component>
6
+ <component name="NewModuleRootManager">
7
+ <content url="file://$MODULE_DIR$" />
8
+ <orderEntry type="inheritedJdk" />
9
+ <orderEntry type="sourceFolder" forTests="false" />
10
+ </component>
11
+ <component name="PyDocumentationSettings">
12
+ <option name="format" value="PLAIN" />
13
+ <option name="myDocStringFormat" value="Plain" />
14
+ </component>
15
+ <component name="TemplatesService">
16
+ <option name="TEMPLATE_CONFIGURATION" value="Jinja2" />
17
+ <option name="TEMPLATE_FOLDERS">
18
+ <list>
19
+ <option value="$MODULE_DIR$/templates" />
20
+ </list>
21
+ </option>
22
+ </component>
23
+ </module>
.idea/vcs.xml ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="VcsDirectoryMappings">
4
+ <mapping directory="" vcs="Git" />
5
+ </component>
6
+ </project>
Dockerfile ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 使用官方 Python 镜像作为基础镜像
2
+ FROM python:3.9-slim
3
+
4
+ # 设置工作目录
5
+ WORKDIR /app
6
+
7
+ # 将当前目录下的所有文件复制到工作目录
8
+ COPY . .
9
+
10
+ # 安装项目依赖
11
+ RUN pip install Flask python-dotenv huggingface_hub requests gunicorn flask-socketio python-engineio python-socketio eventlet
12
+
13
+ # 开放应用程序的端口
14
+ EXPOSE 5000
15
+
16
+ # 设置环境变量(可选,如果需要传递 Docker 环境中的环境变量)
17
+ # ENV USERNAME=your_username
18
+ # ENV PASSWORD=your_password
19
+ # ENV HF_TOKENS=token1,token2,token3
20
+ # ENV API_KEY=your_apikey
21
+
22
+ # 定义启动命令
23
+ CMD ["gunicorn", "--worker-class", "eventlet", "--bind", "0.0.0.0:5000", "app:app"]
api.py ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Blueprint, request, jsonify
2
+ from functools import wraps
3
+ from huggingface_hub import HfApi
4
+ from config import API_KEY
5
+
6
+ api = Blueprint('api', __name__, url_prefix='/api/v1')
7
+
8
+ def require_api_key(f):
9
+ @wraps(f)
10
+ def decorated(*args, **kwargs):
11
+ auth_header = request.headers.get('Authorization')
12
+ if not auth_header:
13
+ return jsonify({'error': 'No Authorization header'}), 401
14
+
15
+ try:
16
+ scheme, token = auth_header.split()
17
+ if scheme.lower() != 'bearer':
18
+ return jsonify({'error': 'Invalid authorization scheme'}), 401
19
+ if token != API_KEY:
20
+ return jsonify({'error': 'Invalid API key'}), 401
21
+ except ValueError:
22
+ return jsonify({'error': 'Invalid Authorization header format'}), 401
23
+
24
+ return f(*args, **kwargs)
25
+ return decorated
26
+
27
+ @api.route('/info/<token>', methods=['GET'])
28
+ @require_api_key
29
+ def list_spaces(token):
30
+ """列出所有空间"""
31
+ try:
32
+ hf_api = HfApi(token=token)
33
+ # 验证token
34
+ try:
35
+ user_info = hf_api.whoami()
36
+ username = user_info["name"]
37
+ except Exception:
38
+ return jsonify({'error': 'Invalid HuggingFace token'}), 401
39
+
40
+ spaces = list(hf_api.list_spaces(author=username))
41
+ space_list = []
42
+
43
+ for space in spaces:
44
+ try:
45
+ space_info = hf_api.space_info(repo_id=space.id)
46
+ space_list.append(space_info.id)
47
+ except Exception as e:
48
+ continue
49
+
50
+ return jsonify({
51
+ 'spaces': space_list,
52
+ 'total': len(space_list)
53
+ })
54
+
55
+ except Exception as e:
56
+ return jsonify({'error': str(e)}), 500
57
+
58
+ @api.route('/info/<token>/<path:space_id>', methods=['GET'])
59
+ @require_api_key
60
+ def get_space_info(token, space_id):
61
+ """获取特定空间信息"""
62
+ try:
63
+ hf_api = HfApi(token=token)
64
+ try:
65
+ space_info = hf_api.space_info(repo_id=space_id)
66
+ except Exception:
67
+ return jsonify({'error': 'Space not found'}), 404
68
+
69
+ # 获取运行状态
70
+ status = "未知状态"
71
+ if space_info.runtime:
72
+ status = space_info.runtime.stage if hasattr(space_info.runtime, 'stage') else "未知状态"
73
+
74
+ return jsonify({
75
+ 'id': space_info.id,
76
+ 'status': status,
77
+ 'last_modified': space_info.lastModified.isoformat() if space_info.lastModified else None,
78
+ 'created_at': space_info.created_at.isoformat() if space_info.created_at else None,
79
+ 'sdk': space_info.sdk,
80
+ 'tags': space_info.tags,
81
+ 'private': space_info.private
82
+ })
83
+
84
+ except Exception as e:
85
+ return jsonify({'error': str(e)}), 500
86
+
87
+ @api.route('/action/<token>/<path:space_id>/restart', methods=['POST'])
88
+ @require_api_key
89
+ def restart_space(token, space_id):
90
+ """重启空间"""
91
+ try:
92
+ hf_api = HfApi(token=token)
93
+ try:
94
+ hf_api.restart_space(repo_id=space_id)
95
+ return jsonify({
96
+ 'success': True,
97
+ 'message': f'Space {space_id} restart initiated successfully'
98
+ })
99
+ except Exception as e:
100
+ return jsonify({
101
+ 'success': False,
102
+ 'error': str(e)
103
+ }), 400
104
+
105
+ except Exception as e:
106
+ return jsonify({
107
+ 'success': False,
108
+ 'error': str(e)
109
+ }), 500
110
+
111
+ @api.route('/action/<token>/<path:space_id>/rebuild', methods=['POST'])
112
+ @require_api_key
113
+ def rebuild_space(token, space_id):
114
+ """重建空间"""
115
+ try:
116
+ hf_api = HfApi(token=token)
117
+ try:
118
+ hf_api.restart_space(repo_id=space_id, factory_reboot=True)
119
+ return jsonify({
120
+ 'success': True,
121
+ 'message': f'Space {space_id} rebuild initiated successfully'
122
+ })
123
+ except Exception as e:
124
+ return jsonify({
125
+ 'success': False,
126
+ 'error': str(e)
127
+ }), 400
128
+
129
+ except Exception as e:
130
+ return jsonify({
131
+ 'success': False,
132
+ 'error': str(e)
133
+ }), 500
app.py ADDED
@@ -0,0 +1,252 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, render_template, request, redirect, url_for, session, jsonify
2
+ from flask_socketio import SocketIO, emit, disconnect
3
+ from dotenv import load_dotenv
4
+ from huggingface_hub import HfApi
5
+ from functools import wraps
6
+ from datetime import datetime, timedelta
7
+ from concurrent.futures import ThreadPoolExecutor
8
+ from config import USERNAME, PASSWORD, HF_TOKENS
9
+ from api import api as api_blueprint
10
+ import concurrent.futures
11
+ import threading
12
+ import logging
13
+ import time
14
+ import os
15
+
16
+ # 配置日志
17
+ logging.basicConfig(level=logging.INFO)
18
+ logger = logging.getLogger(__name__)
19
+
20
+ # 加载环境变量
21
+ load_dotenv()
22
+
23
+ app = Flask(__name__)
24
+ app.secret_key = os.urandom(24)
25
+ app.register_blueprint(api_blueprint)
26
+ socketio = SocketIO(app)
27
+
28
+ # 缓存管理
29
+ class SpaceCache:
30
+ def __init__(self):
31
+ self.spaces = {}
32
+ self.last_update = None
33
+ self.lock = threading.Lock()
34
+ self.active_clients = set() # 跟踪活动的客户端
35
+
36
+ def update_all(self, spaces_data):
37
+ with self.lock:
38
+ self.spaces = {space['repo_id']: space for space in spaces_data}
39
+ self.last_update = datetime.now()
40
+
41
+ def get_all(self):
42
+ with self.lock:
43
+ return list(self.spaces.values()) if self.spaces else []
44
+
45
+ def is_expired(self, expire_minutes=5):
46
+ if not self.last_update:
47
+ return True
48
+ return datetime.now() - self.last_update > timedelta(minutes=expire_minutes)
49
+
50
+ def add_client(self, client_id):
51
+ with self.lock:
52
+ self.active_clients.add(client_id)
53
+
54
+ def remove_client(self, client_id):
55
+ with self.lock:
56
+ self.active_clients.discard(client_id)
57
+
58
+ def has_active_clients(self):
59
+ with self.lock:
60
+ return len(self.active_clients) > 0
61
+
62
+ space_cache = SpaceCache()
63
+
64
+ # WebSocket 事件处理
65
+ @socketio.on('connect')
66
+ def handle_connect():
67
+ if 'authenticated' not in session or not session['authenticated']:
68
+ disconnect()
69
+ return
70
+
71
+ client_id = request.sid
72
+ space_cache.add_client(client_id)
73
+ logger.info(f"Client connected: {client_id}")
74
+
75
+ @socketio.on('disconnect')
76
+ def handle_disconnect():
77
+ client_id = request.sid
78
+ space_cache.remove_client(client_id)
79
+ logger.info(f"Client disconnected: {client_id}")
80
+
81
+ # 登录验证装饰器
82
+ def login_required(f):
83
+ @wraps(f)
84
+ def decorated_function(*args, **kwargs):
85
+ if 'authenticated' not in session or not session['authenticated']:
86
+ return redirect(url_for('login'))
87
+ return f(*args, **kwargs)
88
+ return decorated_function
89
+
90
+ def process_single_space(space, hf_api, username, token):
91
+ try:
92
+ space_info = hf_api.space_info(repo_id=space.id)
93
+ space_runtime = space_info.runtime
94
+
95
+ status = "未知状态"
96
+ if space_runtime:
97
+ status = space_runtime.stage if hasattr(space_runtime, 'stage') else "未知状态"
98
+
99
+ return {
100
+ "repo_id": space_info.id,
101
+ "name": space_info.cardData.get('title') or space_info.id.split('/')[-1],
102
+ "owner": space_info.author,
103
+ "username": username,
104
+ "token": token,
105
+ "url": f"https://{space_info.author}-{space_info.id.split('/')[-1]}.hf.space",
106
+ "status": status,
107
+ "last_modified": space_info.lastModified.strftime("%Y-%m-%d %H:%M:%S") if space_info.lastModified else "未知",
108
+ "created_at": space_info.created_at.strftime("%Y-%m-%d %H:%M:%S") if space_info.created_at else "未知",
109
+ "sdk": space_info.sdk,
110
+ "tags": space_info.tags,
111
+ "private": space_info.private,
112
+ "app_port": space_info.cardData.get('app_port', '未知')
113
+ }
114
+ except Exception as e:
115
+ logger.error(f"处理 Space {space.id} 时出错: {e}")
116
+ return None
117
+
118
+ def get_all_user_spaces():
119
+ # 检查缓存是否有效
120
+ if not space_cache.is_expired():
121
+ logger.info("从缓存获取 Spaces 数据")
122
+ return space_cache.get_all()
123
+
124
+ all_spaces = []
125
+ with ThreadPoolExecutor(max_workers=10) as executor:
126
+ for token in HF_TOKENS:
127
+ try:
128
+ hf_api = HfApi(token=token)
129
+ user_info = hf_api.whoami()
130
+ username = user_info["name"]
131
+ logger.info(f"获取到用户信息: {username}")
132
+
133
+ spaces = list(hf_api.list_spaces(author=username))
134
+ logger.info(f"获取到 {len(spaces)} 个 Spaces")
135
+
136
+ # 并行处理每个space
137
+ future_to_space = {
138
+ executor.submit(process_single_space, space, hf_api, username, token): space
139
+ for space in spaces
140
+ }
141
+
142
+ for future in concurrent.futures.as_completed(future_to_space):
143
+ space_data = future.result()
144
+ if space_data:
145
+ all_spaces.append(space_data)
146
+
147
+ except Exception as e:
148
+ logger.error(f"获取 Spaces 列表失败 (token: {token[:5]}...): {e}")
149
+ import traceback
150
+ traceback.print_exc()
151
+
152
+ # 按名称排序
153
+ all_spaces.sort(key=lambda x: x['name'].lower())
154
+
155
+ # 更新缓存
156
+ space_cache.update_all(all_spaces)
157
+
158
+ logger.info(f"总共获取到 {len(all_spaces)} 个 Spaces")
159
+ return all_spaces
160
+
161
+ # 后台更新缓存的函数
162
+ def update_cache_if_needed():
163
+ """在有活动客户端时更新缓存"""
164
+ while True:
165
+ try:
166
+ if space_cache.has_active_clients() and space_cache.is_expired():
167
+ logger.info("Updating cache due to active clients")
168
+ spaces = get_all_user_spaces()
169
+ space_cache.update_all(spaces)
170
+ socketio.emit('spaces_updated', {'timestamp': time.time()})
171
+ except Exception as e:
172
+ logger.error(f"Cache update failed: {e}")
173
+ time.sleep(60) # 每分钟检查一次
174
+
175
+ # 启动缓存更新线程
176
+ update_thread = threading.Thread(target=update_cache_if_needed, daemon=True)
177
+ update_thread.start()
178
+
179
+ @app.route("/", methods=["GET", "POST"])
180
+ def login():
181
+ if 'authenticated' in session and session['authenticated']:
182
+ return redirect(url_for('dashboard'))
183
+
184
+ if request.method == "POST":
185
+ username = request.form.get("username")
186
+ password = request.form.get("password")
187
+ if username == USERNAME and password == PASSWORD:
188
+ session['authenticated'] = True
189
+ return redirect(url_for("dashboard"))
190
+ else:
191
+ return render_template("index.html", error="用户名或密码错误")
192
+ return render_template("index.html", error=None)
193
+
194
+ @app.route("/logout")
195
+ def logout():
196
+ session.clear()
197
+ return redirect(url_for('login'))
198
+
199
+ @app.route("/dashboard")
200
+ @login_required
201
+ def dashboard():
202
+ spaces = get_all_user_spaces()
203
+ logger.info(f"Dashboard 显示 {len(spaces)} 个 Spaces")
204
+ return render_template("dashboard.html", spaces=spaces)
205
+
206
+ @app.route("/api/space/<path:repo_id>/status")
207
+ @login_required
208
+ def get_space_status(repo_id):
209
+ spaces = get_all_user_spaces()
210
+ space = next((s for s in spaces if s["repo_id"] == repo_id), None)
211
+ if not space:
212
+ return jsonify({"error": "Space not found"}), 404
213
+ return jsonify({
214
+ "id": repo_id,
215
+ "status": space["status"]
216
+ })
217
+
218
+ def restart_space(repo_id, token):
219
+ try:
220
+ hf_api = HfApi(token=token)
221
+ hf_api.restart_space(repo_id=repo_id)
222
+ return f"成功重启 Space: {repo_id}"
223
+ except Exception as e:
224
+ return f"重启 Space {repo_id} 失败: {e}"
225
+
226
+ def rebuild_space(repo_id, token):
227
+ try:
228
+ hf_api = HfApi(token=token)
229
+ hf_api.restart_space(repo_id=repo_id, factory_reboot=True)
230
+ return f"成功重建 Space: {repo_id}"
231
+ except Exception as e:
232
+ return f"重建 Space {repo_id} 失败: {e}"
233
+
234
+ @app.route("/action/<action_type>/<path:repo_id>")
235
+ @login_required
236
+ def space_action(action_type, repo_id):
237
+ spaces = get_all_user_spaces()
238
+ space = next((s for s in spaces if s["repo_id"] == repo_id), None)
239
+
240
+ if not space:
241
+ return "Space not found", 404
242
+
243
+ if action_type == "restart":
244
+ message = restart_space(repo_id, space["token"])
245
+ elif action_type == "rebuild":
246
+ message = rebuild_space(repo_id, space["token"])
247
+ else:
248
+ message = "未知操作"
249
+ return render_template("action_result.html", message=message)
250
+
251
+ if __name__ == "__main__":
252
+ socketio.run(app, host='0.0.0.0', port=5000)
config.py ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from dotenv import load_dotenv
3
+
4
+ load_dotenv()
5
+
6
+ USERNAME = os.getenv("USERNAME")
7
+ PASSWORD = os.getenv("PASSWORD")
8
+ HF_TOKENS = os.getenv("HF_TOKENS", "").split(",")
9
+ API_KEY = os.getenv('API_KEY')
10
+
11
+ if not USERNAME or not PASSWORD or not HF_TOKENS or not API_KEY:
12
+ raise Exception("请在 .env 文件中配置 USERNAME、PASSWORD、HF_TOKENS 和 API_KEY")
templates/action_result.html ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>操作结果 - HF Space Manager</title>
7
+ <style>
8
+ * {
9
+ margin: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
12
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
13
+ }
14
+
15
+ body {
16
+ background-color: #f5f5f7;
17
+ min-height: 100vh;
18
+ display: flex;
19
+ flex-direction: column;
20
+ }
21
+
22
+ .container {
23
+ flex: 1;
24
+ display: flex;
25
+ flex-direction: column;
26
+ justify-content: center;
27
+ align-items: center;
28
+ padding: 2rem;
29
+ }
30
+
31
+ .result-card {
32
+ background: white;
33
+ padding: 2rem;
34
+ border-radius: 18px;
35
+ box-shadow: 0 4px 24px rgba(0, 0, 0, 0.1);
36
+ text-align: center;
37
+ max-width: 600px;
38
+ width: 100%;
39
+ }
40
+
41
+ .message {
42
+ color: #1d1d1f;
43
+ margin-bottom: 2rem;
44
+ font-size: 1.1rem;
45
+ line-height: 1.5;
46
+ }
47
+
48
+ .back-button {
49
+ display: inline-block;
50
+ padding: 0.75rem 1.5rem;
51
+ background: #0071e3;
52
+ color: white;
53
+ text-decoration: none;
54
+ border-radius: 8px;
55
+ transition: all 0.3s ease;
56
+ }
57
+
58
+ .back-button:hover {
59
+ background: #0077ED;
60
+ }
61
+
62
+ .footer {
63
+ text-align: center;
64
+ padding: 2rem;
65
+ color: #86868b;
66
+ font-size: 0.9rem;
67
+ background: white;
68
+ border-top: 1px solid #e5e5e7;
69
+ }
70
+
71
+ .footer a {
72
+ color: #0071e3;
73
+ text-decoration: none;
74
+ transition: color 0.3s ease;
75
+ }
76
+
77
+ .footer a:hover {
78
+ color: #0077ED;
79
+ text-decoration: underline;
80
+ }
81
+ </style>
82
+ </head>
83
+ <body>
84
+ <div class="container">
85
+ <div class="result-card">
86
+ <div class="message">{{ message }}</div>
87
+ <a href="/dashboard" class="back-button">返回控制面板</a>
88
+ </div>
89
+ </div>
90
+
91
+ <footer class="footer">
92
+ <a href="https://github.com/ssfun/hf-space-manager">HF Space Manager</a> 由
93
+ <a href="https://github.com/ssfun">ssfun</a> 构建,源代码遵循
94
+ <a href="https://opensource.org/license/mit">MIT 协议</a>
95
+ </footer>
96
+ </body>
97
+ </html>
templates/dashboard.html ADDED
@@ -0,0 +1,411 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>HF Space Manager -控制面板</title>
7
+ <style>
8
+ * {
9
+ margin: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
12
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
13
+ }
14
+
15
+ body {
16
+ background-color: #f5f5f7;
17
+ min-height: 100vh;
18
+ display: flex;
19
+ flex-direction: column;
20
+ }
21
+
22
+ .header {
23
+ background: rgba(255, 255, 255, 0.85);
24
+ backdrop-filter: blur(20px);
25
+ position: fixed;
26
+ top: 0;
27
+ left: 0;
28
+ right: 0;
29
+ padding: 1rem 2rem;
30
+ display: flex;
31
+ justify-content: space-between;
32
+ align-items: center;
33
+ box-shadow: 0 1px 0 rgba(0, 0, 0, 0.1);z-index: 1000;
34
+ }
35
+
36
+ .header h1 {
37
+ font-size: 1.5rem;
38
+ color: #1d1d1f;
39
+ font-weight: 600;
40
+ }
41
+
42
+ .logout {
43
+ background: none;
44
+ border: none;
45
+ color: #0071e3;
46
+ font-size: 1rem;
47
+ cursor: pointer;
48
+ padding: 8px 16px;
49
+ border-radius: 8px;
50
+ transition: all 0.3s ease;text-decoration: none;
51
+ }
52
+
53
+ .logout:hover {
54
+ background: rgba(0, 113, 227, 0.1);
55
+ }
56
+
57
+ .container {
58
+ flex: 1;
59
+ width: 100%;
60
+ max-width: 1200px;
61
+ margin: 100px auto 0;
62
+ padding: 0 2rem 2rem;
63
+ }
64
+
65
+ .loading-overlay {
66
+ position: fixed;
67
+ top: 0;
68
+ left: 0;
69
+ right: 0;
70
+ bottom: 0;
71
+ background: rgba(255, 255, 255, 0.8);
72
+ display: flex;
73
+ justify-content: center;
74
+ align-items: center;
75
+ z-index: 9999;
76
+ }
77
+
78
+ .loading-spinner {
79
+ width: 50px;
80
+ height: 50px;
81
+ border: 5px solid #f3f3f3;
82
+ border-top: 5px solid #0071e3;
83
+ border-radius: 50%;
84
+ animation: spin 1s linear infinite;
85
+ }
86
+
87
+ @keyframes spin {
88
+ 0% { transform: rotate(0deg); }
89
+ 100% { transform: rotate(360deg); }
90
+ }
91
+
92
+ .owner-section {
93
+ margin-bottom: 2rem;background: white;
94
+ border-radius: 18px;
95
+ padding: 1.5rem;
96
+ box-shadow: 0 4px 24px rgba(0, 0, 0, 0.1);
97
+ }
98
+
99
+ .owner-name {
100
+ font-size: 1.3rem;
101
+ font-weight: 600;
102
+ color: #1d1d1f;
103
+ margin-bottom: 1rem;
104
+ padding-bottom: 0.5rem;
105
+ border-bottom: 1px solid #e5e5e7;
106
+ display: flex;
107
+ justify-content: space-between;align-items: center;
108
+ }
109
+
110
+ .space-count {
111
+ font-size: 0.9rem;
112
+ color: #86868b;}
113
+
114
+ .space-status-count {
115
+ margin-left: 10px;
116
+ }
117
+
118
+ .space-grid {
119
+ display: grid;
120
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
121
+ gap: 1.5rem;
122
+ width: 100%;
123
+ }
124
+
125
+ .space-card {
126
+ background: white;
127
+ border-radius: 12px;
128
+ padding: 1.5rem;
129
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
130
+ transition: all 0.3s ease;
131
+ display: flex;
132
+ flex-direction: column;}
133
+
134
+ .space-card:hover {
135
+ transform: translateY(-3px);
136
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
137
+ }
138
+
139
+ .space-name {
140
+ font-size: 1.1rem;
141
+ font-weight: 500;
142
+ color: #1d1d1f;
143
+ margin-bottom: 1rem;
144
+ }
145
+
146
+ .space-info {
147
+ font-size: 0.9rem;
148
+ color: #86868b;
149
+ margin-bottom: 1rem;
150
+ flex-grow: 1;
151
+ }
152
+
153
+ .space-info p {
154
+ margin-bottom: 0.5rem;
155
+ }
156
+
157
+ .status-badge {
158
+ display: inline-block;
159
+ padding: 2px 8px;
160
+ border-radius: 4px;
161
+ font-weight: 500;}
162
+
163
+ .status-BUILDING {
164
+ background-color: #ff9500;
165
+ color: white;
166
+ }
167
+
168
+ .status-RUNNING {
169
+ background-color: #34c759;
170
+ color: white;
171
+ }
172
+
173
+ .status-SLEEPING {
174
+ background-color: #007aff;
175
+ color: white;
176
+ }
177
+
178
+ .status-STOPPED {
179
+ background-color: #8e8e93;
180
+ color: white;
181
+ }
182
+
183
+ .status-FAILED {
184
+ background-color: #ff3b30;
185
+ color: white;
186
+ }
187
+
188
+ .status-BUILD_ERROR {
189
+ background-color: #ff3b30;
190
+ color: white;
191
+ }
192
+
193
+ .status-UNKNOWN {
194
+ background-color: #8e8e93;
195
+ color: white;
196
+ }
197
+
198
+ .action-buttons {
199
+ display: grid;
200
+ grid-template-columns: repeat(3, 1fr);
201
+ gap: 0.5rem;
202
+ margin-top: auto;
203
+ }
204
+
205
+ .action-button {
206
+ padding: 8px 12px;
207
+ border-radius: 8px;
208
+ border: none;
209
+ font-size: 0.9rem;
210
+ cursor: pointer;
211
+ transition: all 0.3s ease;text-align: center;
212
+ text-decoration: none;
213
+ }
214
+
215
+ .view {
216
+ background: #e5e5e7;
217
+ color: #1d1d1f;
218
+ }
219
+
220
+ .view:hover {
221
+ background: #d5d5d7;
222
+ }
223
+
224
+ .restart {
225
+ background: #0071e3;
226
+ color: white;
227
+ }
228
+
229
+ .restart:hover {
230
+ background: #0077ED;
231
+ }
232
+
233
+ .rebuild {
234
+ background: #f5f5f7;
235
+ color: #1d1d1f;
236
+ }
237
+
238
+ .rebuild:hover {
239
+ background: #e5e5e7;
240
+ }
241
+
242
+ .footer {
243
+ text-align: center;
244
+ padding: 2rem;
245
+ color: #86868b;
246
+ font-size: 0.9rem;
247
+ background: white;
248
+ border-top: 1px solid #e5e5e7;
249
+ }
250
+
251
+ .footer a {
252
+ color: #0071e3;
253
+ text-decoration: none;transition: color 0.3s ease;
254
+ }
255
+
256
+ .footer a:hover {
257
+ color: #0077ED;
258
+ text-decoration: underline;
259
+ }
260
+
261
+ @media (max-width: 768px) {
262
+ .container {
263
+ padding: 0 1rem;
264
+ }
265
+
266
+ .owner-name {
267
+ flex-direction: column;
268
+ align-items: flex-start;
269
+ }
270
+
271
+ .space-count {
272
+ margin-top: 0.5rem;
273
+ }
274
+ }
275
+ </style>
276
+ </head>
277
+ <body>
278
+ <div id="loading" class="loading-overlay">
279
+ <div class="loading-spinner"></div>
280
+ </div>
281
+
282
+ <header class="header">
283
+ <h1>HF Space Manager</h1>
284
+ <a href="/logout" class="logout">退出登录</a>
285
+ </header>
286
+
287
+ <div class="container">
288
+ {% if spaces %}
289
+ {% set grouped_spaces = {} %}
290
+ {% for space in spaces %}
291
+ {% if space.owner not in grouped_spaces %}
292
+ {% set _ = grouped_spaces.update({space.owner: []}) %}
293
+ {% endif %}
294
+ {% set _ = grouped_spaces[space.owner].append(space) %}
295
+ {% endfor %}
296
+
297
+ {% for owner, owner_spaces in grouped_spaces.items() %}
298
+ {% set sorted_spaces = owner_spaces %}
299
+ {% set running_count = sorted_spaces | selectattr('status','equalto','RUNNING') | list | length %}
300
+ {% set building_count = sorted_spaces | selectattr('status','equalto','BUILDING') | list | length %}
301
+ {% set sleeping_count = sorted_spaces | selectattr('status','equalto','SLEEPING') | list | length %}
302
+ {% set stopped_count = sorted_spaces | selectattr('status','equalto','STOPPED') | list | length %}
303
+ {% set failed_count = sorted_spaces | selectattr('status','equalto','BUILD_ERROR') | list | length %}
304
+ <div class="owner-section">
305
+ <div class="owner-name">
306
+ <span>{{ owner }}</span>
307
+ <span class="space-count">
308
+ 总数: {{ sorted_spaces | length }}
309
+ <span class="space-status-count">运行:{{ running_count }}</span>
310
+ <span class="space-status-count">休眠:{{ sleeping_count }}</span>
311
+ <span class="space-status-count">停止:{{ stopped_count }}</span>
312
+ <span class="space-status-count">失败:{{ failed_count }}</span>
313
+ </span>
314
+ </div><div class="space-grid">
315
+ {% for space in sorted_spaces %}
316
+ <div class="space-card" data-space-id="{{ space.repo_id }}"><div class="space-name">{{ space.name }}</div>
317
+ <div class="space-info">
318
+ <p>ID: {{ space.repo_id }}</p>
319
+ <p>状态: <span class="status-badge status-{{ space.status }}">{{ space.status }}</span></p><p>创建时间: {{ space.created_at }}</p>
320
+ <p>最后修改: {{ space.last_modified }}</p>
321
+ <p>SDK: {{ space.sdk }}</p><p>App端口: {{ space.app_port }}</p>
322
+ {% if space.tags %}
323
+ <p>标签:{% for tag in space.tags %}<span class="tag">{{ tag }}</span>
324
+ {% endfor %}
325
+ </p>
326
+ {% endif %}<p>私有: {{ '是' if space.private else '否' }}</p>
327
+ </div><div class="action-buttons">
328
+ <a href="{{ space.url }}" target="_blank" class="action-button view">查看</a>
329
+ <button onclick="confirmAction('restart', '{{ space.repo_id }}')" class="action-button restart">重启</button>
330
+ <button onclick="confirmAction('rebuild', '{{ space.repo_id }}')" class="action-button rebuild">重建</button>
331
+ </div>
332
+ </div>
333
+ {% endfor %}
334
+ </div>
335
+ </div>
336
+ {% endfor %}
337
+ {% else %}
338
+ <div class="owner-section">
339
+ <p style="text-align: center; color: #86868b;">没有找到任何 Spaces。请确保你的账户中有创建的Spaces,并且提供的 token 有正确的权限。</p>
340
+ </div>
341
+ {% endif %}
342
+ </div>
343
+
344
+ <footer class="footer">
345
+ <a href="https://github.com/ssfun/hf-space-manager">HF Space Manager</a> 由
346
+ <a href="https://github.com/ssfun">ssfun</a> 构建,源代码遵循
347
+ <a href="https://opensource.org/license/mit">MIT 协议</a>
348
+ </footer>
349
+
350
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.js"></script>
351
+ <script>
352
+ // 连接 WebSocket
353
+ const socket = io();
354
+
355
+ socket.on('connect', () => {
356
+ console.log('Connected to server');
357
+ });
358
+
359
+ socket.on('disconnect', () => {
360
+ console.log('Disconnected from server');
361
+ });
362
+
363
+ // 当收到缓存更新通知时刷新数据
364
+ socket.on('spaces_updated', (data) => {
365
+ updateSpaceStatuses();
366
+ });
367
+
368
+ // 页面可见性变化处理
369
+ document.addEventListener('visibilitychange', function() {
370
+ if (document.hidden) {
371
+ // 页面隐藏时断开连接
372
+ socket.disconnect();
373
+ } else {
374
+ // 页面可见时重新连接
375
+ socket.connect();
376
+ }
377
+ });
378
+
379
+ // 页面加载完成后隐藏加载动画
380
+ window.addEventListener('load', function() {
381
+ document.getElementById('loading').style.display = 'none';
382
+ });
383
+
384
+ // 定期更新状态
385
+ function updateSpaceStatuses() {document.querySelectorAll('.space-card').forEach(card => {
386
+ const spaceId = card.dataset.spaceId;
387
+ fetch(`/api/space/${spaceId}/status`)
388
+ .then(response => response.json())
389
+ .then(data => {
390
+ const statusElement = card.querySelector('.status-badge');
391
+ if (statusElement && data.status) {
392
+ statusElement.className = `status-badge status-${data.status}`;statusElement.textContent = data.status;
393
+ }
394
+ })
395
+ .catch(error => console.error('Error updating status:', error));
396
+ });
397
+ }
398
+
399
+ // 在进行操作时显示加载动画
400
+ function confirmAction(action, spaceId) {
401
+ var actionText = action === 'restart' ? '重启' : '重建';
402
+ if (confirm(`确定要${actionText} "${spaceId}" 吗?`)) {document.getElementById('loading').style.display = 'flex';
403
+ window.location.href = `/action/${action}/${spaceId}`;
404
+ }
405
+ }
406
+
407
+ // 每30秒更新一次状态
408
+ setInterval(updateSpaceStatuses, 30000);
409
+ </script>
410
+ </body>
411
+ </html>
templates/index.html ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>登录 - HF Space Manager</title>
7
+ <style>
8
+ * {
9
+ margin: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
12
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
13
+ }
14
+
15
+ body {
16
+ background-color: #f5f5f7;
17
+ min-height: 100vh;
18
+ display: flex;
19
+ flex-direction: column;
20
+ }
21
+
22
+ .login-container {
23
+ flex: 1;
24
+ display: flex;
25
+ flex-direction: column;
26
+ justify-content: center;
27
+ align-items: center;
28
+ padding: 2rem;
29
+ }
30
+
31
+ .logo {
32
+ font-size: 2rem;
33
+ font-weight: 600;
34
+ color: #1d1d1f;
35
+ margin-bottom: 2rem;
36
+ }
37
+
38
+ form {
39
+ background: white;
40
+ padding: 2rem;
41
+ border-radius: 18px;
42
+ box-shadow: 0 4px 24px rgba(0, 0, 0, 0.1);
43
+ width: 100%;
44
+ max-width: 400px;}
45
+
46
+ .form-group {
47
+ margin-bottom: 1.5rem;
48
+ }
49
+
50
+ label {
51
+ display: block;
52
+ margin-bottom: 0.5rem;
53
+ color: #1d1d1f;
54
+ font-weight: 500;
55
+ }
56
+
57
+ input {
58
+ width: 100%;
59
+ padding: 0.75rem;
60
+ border: 1px solid #e5e5e7;
61
+ border-radius: 8px;
62
+ font-size: 1rem;
63
+ transition: all 0.3s ease;
64
+ }
65
+
66
+ input:focus {
67
+ outline: none;
68
+ border-color: #0071e3;
69
+ box-shadow: 0 0 0 3px rgba(0, 113, 227, 0.1);
70
+ }
71
+
72
+ button {
73
+ width: 100%;
74
+ padding: 0.75rem;
75
+ background: #0071e3;
76
+ color: white;
77
+ border: none;
78
+ border-radius: 8px;
79
+ font-size: 1rem;
80
+ cursor: pointer;
81
+ transition: all 0.3s ease;
82
+ }
83
+
84
+ button:hover {
85
+ background: #0077ED;
86
+ }
87
+
88
+ .error-message {
89
+ color: #ff3b30;
90
+ margin-bottom: 1rem;text-align: center;
91
+ }
92
+
93
+ .footer {
94
+ text-align: center;
95
+ padding: 2rem;
96
+ color: #86868b;font-size: 0.9rem;
97
+ background: white;
98
+ border-top: 1px solid #e5e5e7;
99
+ }
100
+
101
+ .footer a {
102
+ color: #0071e3;
103
+ text-decoration: none;transition: color 0.3s ease;
104
+ }
105
+
106
+ .footer a:hover {
107
+ color: #0077ED;text-decoration: underline;
108
+ }
109
+ </style>
110
+ </head>
111
+ <body>
112
+ <div class="login-container">
113
+ <div class="logo">HF Space Manager</div>
114
+ <form method="post">
115
+ <div class="form-group">
116
+ <label for="username">用户名</label>
117
+ <input type="text" id="username" name="username" required>
118
+ </div>
119
+ <div class="form-group">
120
+ <label for="password">密码</label>
121
+ <input type="password" id="password" name="password" required>
122
+ </div>
123
+ {% if error %}
124
+ <div class="error-message">{{ error }}</div>
125
+ {% endif %}
126
+ <button type="submit">登录</button>
127
+ </form>
128
+ </div>
129
+
130
+ <footer class="footer">
131
+ <a href="https://github.com/ssfun/hf-space-manager">HF Space Manager</a> 由<a href="https://github.com/ssfun">ssfun</a> 构建,源代码遵循
132
+ <a href="https://opensource.org/license/mit">MIT 协议</a>
133
+ </footer>
134
+ </body>
135
+ </html>