mistpe commited on
Commit
1df5e59
·
verified ·
1 Parent(s): 4d17cfc

Upload 5 files

Browse files
Files changed (5) hide show
  1. Dockerfile +23 -0
  2. README-example.md +152 -0
  3. api.py +133 -0
  4. app.py +252 -0
  5. config.py +12 -0
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 7860
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:7860", "app:app"]
README-example.md ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # HF Space Manager
2
+
3
+ HF Space Manager 是一个用于管理 Hugging Face Spaces 的 Web 应用程序。它提供了直观的界面和API 接口,让你能够方便地查看和管理多个账号下的所有 Spaces。
4
+
5
+ ## 最近更新
6
+
7
+ 2024-12-20 space 信息修改为访问 dashboard 时才会后台自动更新
8
+
9
+ ## 功能特点
10
+
11
+ - 🚀 支持多个 HuggingFace 账号管理
12
+ - 📊 实时显示 Spaces 状态
13
+ - 🔄 支持重启和重建操作
14
+ - 🔑 安全的登录认证
15
+ - 🎯 简洁的用户界面
16
+ - 📱 响应式设计,支持移动端
17
+ - 🔌 RESTful API 支持
18
+ - 💾 数据缓存机制
19
+
20
+ ## 界面预览
21
+ ![index](/preview/index.png "index")
22
+
23
+ ![dashboard](/preview/dashboard.png "dashboard")
24
+
25
+ ## 快速开始
26
+
27
+ ### 环境要求
28
+
29
+ - Python 3.9+
30
+ - Docker (可选)
31
+
32
+ ### 安装
33
+
34
+ 1. 克隆仓库
35
+ ```bash
36
+ git clone https://github.com/ssfun/hf-space-manager.git
37
+ cd hf-space-manager
38
+ ```
39
+
40
+ 2. 安装依赖
41
+ ```bash
42
+ pip install Flask python-dotenv huggingface_hub requests gunicorn
43
+ ```
44
+
45
+ 3. 配置环境变量
46
+ 创建 `.env` 文件:
47
+ ```env
48
+ USERNAME=your_admin_username
49
+ PASSWORD=your_admin_password
50
+ HF_TOKENS=token1,token2,token3
51
+ API_KEY=your_api_key
52
+ ```
53
+
54
+ ### 运行
55
+
56
+ **直接运行:**
57
+ ```bash
58
+ python app.py
59
+ ```
60
+
61
+ **使用 Docker:**
62
+ ```bash
63
+ docker run -d \
64
+ --name hfspace-manager \
65
+ -p 7860:7860 \
66
+ -e USERNAME=your_username \
67
+ -e PASSWORD=your_password \
68
+ -e HF_TOKENS=token1,token2,token3 \
69
+ -e API_KEY=your_api_key \
70
+ sfun/hfspace:latest
71
+ ```
72
+
73
+ ## API 文档
74
+
75
+ ### 认证
76
+ 所有 API 请求都需要在 header 中包含 API key:
77
+ ```
78
+ Authorization: Bearer your_api_key
79
+ ```
80
+
81
+ ### 端点
82
+
83
+ #### 1. 获取所有 Spaces
84
+ ```bash
85
+ curl -X GET "http://localhost:7860/api/v1/info/hf_token_here" \
86
+ -H "Content-Type: application/json" \
87
+ -H "Authorization: Bearer API_KEY"
88
+ ```
89
+
90
+ #### 2. 获取特定 Space 信息
91
+ ```bash
92
+ curl -X GET "http://localhost:7860/api/v1/info/hf_token_here/username/space-name" \
93
+ -H "Content-Type: application/json" \
94
+ -H "Authorization: Bearer API_KEY"
95
+ ```
96
+
97
+ #### 3. 重启 Space
98
+ ```bash
99
+ curl -X POST "http://localhost:7860/api/v1/action/hf_token_here/username/space-name/restart" \
100
+ -H "Content-Type: application/json" \
101
+ -H "Authorization: Bearer API_KEY"
102
+ ```
103
+
104
+ #### 4. 重建 Space
105
+ ```bash
106
+ curl -X POST "http://localhost:7860/api/v1/action/hf_token_here/username/space-name/restart" \
107
+ -H "Content-Type: application/json" \
108
+ -H "Authorization: Bearer API_KEY"
109
+ ```
110
+
111
+ ## 配置说明
112
+
113
+ ### 环境变量
114
+
115
+ | 变量名 | 说明 | 必填 |
116
+ |--------|------|------|
117
+ | USERNAME | 管理员用户名 | 是 |
118
+ | PASSWORD | 管理员密码 | 是 |
119
+ | HF_TOKENS | HuggingFace tokens(逗号分隔) | 是 |
120
+ | API_KEY | API访问密钥 | 是 |
121
+
122
+ ### 缓存配置
123
+
124
+ - 访问 dashboard 时后台自动更新
125
+
126
+ ## 开发
127
+
128
+ ### 本地开发
129
+ ```bash
130
+ # 安装开发依赖
131
+ pip install Flask python-dotenv huggingface_hub requests gunicorn
132
+
133
+ # 运行开发服务器
134
+ python app.py
135
+ ```
136
+
137
+ ## 贡献
138
+
139
+ 欢迎 Star 和 Fork 后自行修改!
140
+
141
+ ## 许可证
142
+
143
+ 本项目采用 [MIT 许可证](https://opensource.org/license/mit)。
144
+
145
+ ## 作者
146
+
147
+ [ssfun](https://github.com/ssfun)
148
+
149
+ ## 致谢
150
+
151
+ - [Hugging Face](https://huggingface.co/) - 提供优秀的 API 和服务
152
+ - [Flask](https://flask.palletsprojects.com/) - Web 框架支持
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=7860)
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")