Spaces:
Sleeping
Sleeping
epii-1
commited on
Commit
·
6a2845d
1
Parent(s):
d65a9cb
12345
Browse files- .idea/.gitignore +8 -0
- .idea/inspectionProfiles/Project_Default.xml +34 -0
- .idea/inspectionProfiles/profiles_settings.xml +6 -0
- .idea/misc.xml +4 -0
- .idea/modules.xml +8 -0
- .idea/panel.iml +23 -0
- .idea/vcs.xml +6 -0
- Dockerfile +23 -0
- api.py +133 -0
- app.py +252 -0
- config.py +12 -0
- templates/action_result.html +97 -0
- templates/dashboard.html +411 -0
- templates/index.html +135 -0
.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>
|