nbugs commited on
Commit
fac8d9c
·
verified ·
1 Parent(s): 2c6e400

Create server.js

Browse files
Files changed (1) hide show
  1. server.js +429 -0
server.js ADDED
@@ -0,0 +1,429 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const express = require('express');
2
+ const path = require('path');
3
+ const axios = require('axios');
4
+ const crypto = require('crypto');
5
+ const app = express();
6
+ const port = process.env.PORT || 8080;
7
+
8
+ // 启用 JSON 和 URL-encoded 请求解析
9
+ app.use(express.json());
10
+ app.use(express.urlencoded({ extended: true }));
11
+
12
+ // 从环境变量获取 HuggingFace 用户名和对应的 API Token 映射
13
+ const userTokenMapping = {};
14
+ const usernames = [];
15
+ const hfUserConfig = process.env.HF_USER || '';
16
+ if (hfUserConfig) {
17
+ hfUserConfig.split(',').forEach(pair => {
18
+ const parts = pair.split(':').map(part => part.trim());
19
+ const username = parts[0];
20
+ const token = parts[1] || '';
21
+ if (username) {
22
+ usernames.push(username);
23
+ if (token) {
24
+ userTokenMapping[username] = token;
25
+ }
26
+ }
27
+ });
28
+ }
29
+
30
+ // 从环境变量获取登录凭据
31
+ const ADMIN_USERNAME = process.env.USER_NAME || 'admin';
32
+ const ADMIN_PASSWORD = process.env.USER_PASSWORD || 'password';
33
+
34
+ // 存储会话 token 的简单内存数据库(生产环境中应使用数据库或 Redis)
35
+ const sessions = new Map();
36
+ const SESSION_TIMEOUT = 24 * 60 * 60 * 1000; // 24小时超时
37
+
38
+ // 缓存管理
39
+ class SpaceCache {
40
+ constructor() {
41
+ this.spaces = {};
42
+ this.lastUpdate = null;
43
+ }
44
+
45
+ updateAll(spacesData) {
46
+ this.spaces = spacesData.reduce((acc, space) => ({ ...acc, [space.repo_id]: space }), {});
47
+ this.lastUpdate = Date.now();
48
+ }
49
+
50
+ getAll() {
51
+ return Object.values(this.spaces);
52
+ }
53
+
54
+ isExpired(expireMinutes = 5) {
55
+ if (!this.lastUpdate) return true;
56
+ return (Date.now() - this.lastUpdate) > (expireMinutes * 60 * 1000);
57
+ }
58
+ }
59
+
60
+ const spaceCache = new SpaceCache();
61
+
62
+ // 提供静态文件(前端文件)
63
+ app.use(express.static(path.join(__dirname, 'public')));
64
+
65
+ // 提供配置信息的 API 接口
66
+ app.get('/api/config', (req, res) => {
67
+ res.json({ usernames: usernames.join(',') });
68
+ });
69
+
70
+ // 登录 API 接口
71
+ app.post('/api/login', (req, res) => {
72
+ const { username, password } = req.body;
73
+ if (username === ADMIN_USERNAME && password === ADMIN_PASSWORD) {
74
+ // 生成一个随机 token 作为会话标识
75
+ const token = crypto.randomBytes(16).toString('hex');
76
+ const expiresAt = Date.now() + SESSION_TIMEOUT;
77
+ sessions.set(token, { username, expiresAt });
78
+ console.log(`用户 ${username} 登录成功,生成 token: ${token.slice(0, 8)}...`);
79
+ res.json({ success: true, token });
80
+ } else {
81
+ console.log(`用户 ${username} 登录失败,凭据无效`);
82
+ res.status(401).json({ success: false, message: '用户名或密码错误' });
83
+ }
84
+ });
85
+
86
+ // 验证登录状态 API 接口
87
+ app.post('/api/verify-token', (req, res) => {
88
+ const { token } = req.body;
89
+ const session = sessions.get(token);
90
+ if (session && session.expiresAt > Date.now()) {
91
+ res.json({ success: true, message: 'Token 有效' });
92
+ } else {
93
+ if (session) {
94
+ sessions.delete(token); // 删除过期的 token
95
+ console.log(`Token ${token.slice(0, 8)}... 已过期,已删除`);
96
+ }
97
+ res.status(401).json({ success: false, message: 'Token 无效或已过期' });
98
+ }
99
+ });
100
+
101
+ // 登出 API 接口
102
+ app.post('/api/logout', (req, res) => {
103
+ const { token } = req.body;
104
+ sessions.delete(token);
105
+ console.log(`Token ${token.slice(0, 8)}... 已手动登出`);
106
+ res.json({ success: true, message: '登出成功' });
107
+ });
108
+
109
+ // 中间件:验证请求中的 token
110
+ const authenticateToken = (req, res, next) => {
111
+ const authHeader = req.headers['authorization'];
112
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
113
+ return res.status(401).json({ error: '未提供有效的认证令牌' });
114
+ }
115
+ const token = authHeader.split(' ')[1];
116
+ const session = sessions.get(token);
117
+ if (session && session.expiresAt > Date.now()) {
118
+ req.session = session;
119
+ next();
120
+ } else {
121
+ if (session) {
122
+ sessions.delete(token); // 删除过期的 token
123
+ console.log(`Token ${token.slice(0, 8)}... 已过期,拒绝访问`);
124
+ }
125
+ return res.status(401).json({ error: '认证令牌无效或已过期' });
126
+ }
127
+ };
128
+
129
+ // 获取所有 spaces 列表(包括私有)
130
+ app.get('/api/proxy/spaces', async (req, res) => {
131
+ try {
132
+ if (!spaceCache.isExpired()) {
133
+ console.log('从缓存获取 Spaces 数据');
134
+ return res.json(spaceCache.getAll());
135
+ }
136
+
137
+ const allSpaces = [];
138
+ for (const username of usernames) {
139
+ const token = userTokenMapping[username];
140
+ if (!token) {
141
+ console.warn(`用户 ${username} 没有配置 API Token,将尝试无认证访问公开数据`);
142
+ }
143
+
144
+ try {
145
+ // 调用 HuggingFace API 获取 Spaces 列表
146
+ const headers = token ? { 'Authorization': `Bearer ${token}` } : {};
147
+ const response = await axios.get(`https://huggingface.co/api/spaces?author=${username}`, { headers });
148
+ const spaces = response.data;
149
+ console.log(`获取到 ${spaces.length} 个 Spaces for ${username}`);
150
+
151
+ for (const space of spaces) {
152
+ try {
153
+ // 获取 Space 详细信息
154
+ const spaceInfoResponse = await axios.get(`https://huggingface.co/api/spaces/${space.id}`, { headers });
155
+ const spaceInfo = spaceInfoResponse.data;
156
+ const spaceRuntime = spaceInfo.runtime || {};
157
+
158
+ allSpaces.push({
159
+ repo_id: spaceInfo.id,
160
+ name: spaceInfo.cardData?.title || spaceInfo.id.split('/')[1],
161
+ owner: spaceInfo.author,
162
+ username: username,
163
+ token: token || '',
164
+ url: `https://${spaceInfo.author}-${spaceInfo.id.split('/')[1]}.hf.space`,
165
+ status: spaceRuntime.stage || 'unknown',
166
+ last_modified: spaceInfo.lastModified || 'unknown',
167
+ created_at: spaceInfo.createdAt || 'unknown',
168
+ sdk: spaceInfo.sdk || 'unknown',
169
+ tags: spaceInfo.tags || [],
170
+ private: spaceInfo.private || false,
171
+ app_port: spaceInfo.cardData?.app_port || 'unknown'
172
+ });
173
+ } catch (error) {
174
+ console.error(`处理 Space ${space.id} 失败:`, error.message);
175
+ }
176
+ }
177
+ } catch (error) {
178
+ console.error(`获取 Spaces 列表失败 for ${username}:`, error.message);
179
+ }
180
+ }
181
+
182
+ allSpaces.sort((a, b) => a.name.localeCompare(b.name));
183
+ spaceCache.updateAll(allSpaces);
184
+ console.log(`总共获取到 ${allSpaces.length} 个 Spaces`);
185
+ res.json(allSpaces);
186
+ } catch (error) {
187
+ console.error(`代理获取 spaces 列表失败:`, error.message);
188
+ res.status(500).json({ error: '获取 spaces 列表失败', details: error.message });
189
+ }
190
+ });
191
+
192
+ // 代理重启 Space(需要认证)
193
+ app.post('/api/proxy/restart/:repoId(*)', authenticateToken, async (req, res) => {
194
+ try {
195
+ const { repoId } = req.params;
196
+ console.log(`尝试重启 Space: ${repoId}`);
197
+ const spaces = spaceCache.getAll();
198
+ const space = spaces.find(s => s.repo_id === repoId);
199
+ if (!space || !space.token) {
200
+ console.error(`Space ${repoId} 未找到或无 Token 配置`);
201
+ return res.status(404).json({ error: 'Space 未找到或无 Token 配置' });
202
+ }
203
+
204
+ const headers = { 'Authorization': `Bearer ${space.token}`, 'Content-Type': 'application/json' };
205
+ const response = await axios.post(`https://huggingface.co/api/spaces/${repoId}/restart`, {}, { headers });
206
+ console.log(`重启 Space ${repoId} 成功,状态码: ${response.status}`);
207
+ res.json({ success: true, message: `Space ${repoId} 重启成功` });
208
+ } catch (error) {
209
+ console.error(`重启 space 失败 (${req.params.repoId}):`, error.message);
210
+ if (error.response) {
211
+ console.error(`状态码: ${error.response.status}, 响应数据:`, error.response.data);
212
+ res.status(error.response.status || 500).json({ error: '重启 space 失败', details: error.response.data?.message || error.message });
213
+ } else {
214
+ res.status(500).json({ error: '重启 space 失败', details: error.message });
215
+ }
216
+ }
217
+ });
218
+
219
+ // 代理重建 Space(需要认证)
220
+ app.post('/api/proxy/rebuild/:repoId(*)', authenticateToken, async (req, res) => {
221
+ try {
222
+ const { repoId } = req.params;
223
+ console.log(`尝试重建 Space: ${repoId}`);
224
+ const spaces = spaceCache.getAll();
225
+ const space = spaces.find(s => s.repo_id === repoId);
226
+ if (!space || !space.token) {
227
+ console.error(`Space ${repoId} 未找到或无 Token 配置`);
228
+ return res.status(404).json({ error: 'Space 未找到或无 Token 配置' });
229
+ }
230
+
231
+ const headers = { 'Authorization': `Bearer ${space.token}`, 'Content-Type': 'application/json' };
232
+ // 将 factory_reboot 参数作为查询参数传递,而非请求体
233
+ const response = await axios.post(
234
+ `https://huggingface.co/api/spaces/${repoId}/restart?factory=true`,
235
+ {},
236
+ { headers }
237
+ );
238
+ console.log(`重建 Space ${repoId} 成功,状态码: ${response.status}`);
239
+ res.json({ success: true, message: `Space ${repoId} 重建成功` });
240
+ } catch (error) {
241
+ console.error(`重建 space 失败 (${req.params.repoId}):`, error.message);
242
+ if (error.response) {
243
+ console.error(`状态码: ${error.response.status}, 响应数据:`, error.response.data);
244
+ res.status(error.response.status || 500).json({ error: '重建 space 失败', details: error.response.data?.message || error.message });
245
+ } else {
246
+ res.status(500).json({ error: '重建 space 失败', details: error.message });
247
+ }
248
+ }
249
+ });
250
+
251
+ // 外部 API 服务(类似于 Flask 的 /api/v1)
252
+ app.get('/api/v1/info/:token', async (req, res) => {
253
+ try {
254
+ const { token } = req.params;
255
+ const authHeader = req.headers.authorization;
256
+ if (!authHeader || !authHeader.startsWith('Bearer ') || authHeader.split(' ')[1] !== process.env.API_KEY) {
257
+ return res.status(401).json({ error: '无效的 API 密钥' });
258
+ }
259
+
260
+ const headers = { 'Authorization': `Bearer ${token}` };
261
+ const userInfoResponse = await axios.get('https://huggingface.co/api/whoami-v2', { headers });
262
+ const username = userInfoResponse.data.name;
263
+ const spacesResponse = await axios.get(`https://huggingface.co/api/spaces?author=${username}`, { headers });
264
+ const spaces = spacesResponse.data;
265
+ const spaceList = [];
266
+
267
+ for (const space of spaces) {
268
+ try {
269
+ const spaceInfoResponse = await axios.get(`https://huggingface.co/api/spaces/${space.id}`, { headers });
270
+ spaceList.push(spaceInfoResponse.data.id);
271
+ } catch (error) {
272
+ console.error(`获取 Space 信息失败 (${space.id}):`, error.message);
273
+ }
274
+ }
275
+
276
+ res.json({ spaces: spaceList, total: spaceList.length });
277
+ } catch (error) {
278
+ console.error(`获取 spaces 列表失败 (外部 API):`, error.message);
279
+ res.status(500).json({ error: error.message });
280
+ }
281
+ });
282
+
283
+ app.get('/api/v1/info/:token/:spaceId(*)', async (req, res) => {
284
+ try {
285
+ const { token, spaceId } = req.params;
286
+ const authHeader = req.headers.authorization;
287
+ if (!authHeader || !authHeader.startsWith('Bearer ') || authHeader.split(' ')[1] !== process.env.API_KEY) {
288
+ return res.status(401).json({ error: '无效的 API 密钥' });
289
+ }
290
+
291
+ const headers = { 'Authorization': `Bearer ${token}` };
292
+ const spaceInfoResponse = await axios.get(`https://huggingface.co/api/spaces/${spaceId}`, { headers });
293
+ const spaceInfo = spaceInfoResponse.data;
294
+ const spaceRuntime = spaceInfo.runtime || {};
295
+
296
+ res.json({
297
+ id: spaceInfo.id,
298
+ status: spaceRuntime.stage || 'unknown',
299
+ last_modified: spaceInfo.lastModified || null,
300
+ created_at: spaceInfo.createdAt || null,
301
+ sdk: spaceInfo.sdk || 'unknown',
302
+ tags: spaceInfo.tags || [],
303
+ private: spaceInfo.private || false
304
+ });
305
+ } catch (error) {
306
+ console.error(`获取 space 信息失败 (外部 API):`, error.message);
307
+ res.status(error.response?.status || 404).json({ error: error.message });
308
+ }
309
+ });
310
+
311
+ app.post('/api/v1/action/:token/:spaceId(*)/restart', async (req, res) => {
312
+ try {
313
+ const { token, spaceId } = req.params;
314
+ const authHeader = req.headers.authorization;
315
+ if (!authHeader || !authHeader.startsWith('Bearer ') || authHeader.split(' ')[1] !== process.env.API_KEY) {
316
+ return res.status(401).json({ error: '无效的 API 密钥' });
317
+ }
318
+
319
+ const headers = { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' };
320
+ await axios.post(`https://huggingface.co/api/spaces/${spaceId}/restart`, {}, { headers });
321
+ res.json({ success: true, message: `Space ${spaceId} 重启成功` });
322
+ } catch (error) {
323
+ console.error(`重启 space 失败 (外部 API):`, error.message);
324
+ res.status(error.response?.status || 500).json({ success: false, error: error.message });
325
+ }
326
+ });
327
+
328
+ app.post('/api/v1/action/:token/:spaceId(*)/rebuild', async (req, res) => {
329
+ try {
330
+ const { token, spaceId } = req.params;
331
+ const authHeader = req.headers.authorization;
332
+ if (!authHeader || !authHeader.startsWith('Bearer ') || authHeader.split(' ')[1] !== process.env.API_KEY) {
333
+ return res.status(401).json({ error: '无效的 API 密钥' });
334
+ }
335
+
336
+ const headers = { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' };
337
+ console.log(`外部 API 发送重建请求,spaceId: ${spaceId}`);
338
+ // 将 factory_reboot 参数作为查询参数传递,而非请求体
339
+ const response = await axios.post(
340
+ `https://huggingface.co/api/spaces/${spaceId}/restart?factory=true`,
341
+ {},
342
+ { headers }
343
+ );
344
+ console.log(`外部 API 重建 Space ${spaceId} 成功,状态码: ${response.status}`);
345
+ res.json({ success: true, message: `Space ${spaceId} 重建成功` });
346
+ } catch (error) {
347
+ console.error(`重建 space 失败 (外部 API):`, error.message);
348
+ if (error.response) {
349
+ console.error(`状态码: ${error.response.status}, 响应数据:`, error.response.data);
350
+ res.status(error.response.status || 500).json({ success: false, error: error.response.data?.message || error.message });
351
+ } else {
352
+ res.status(500).json({ success: false, error: error.message });
353
+ }
354
+ }
355
+ });
356
+
357
+ // 代理 HuggingFace API:获取实时监控数据(SSE)
358
+ app.get('/api/proxy/live-metrics/:username/:instanceId', async (req, res) => {
359
+ try {
360
+ const { username, instanceId } = req.params;
361
+ const url = `https://api.hf.space/v1/${username}/${instanceId}/live-metrics/sse`;
362
+
363
+ // 检查实例状态,决定是否继续请求
364
+ const spaces = spaceCache.getAll();
365
+ const space = spaces.find(s => s.repo_id === `${username}/${instanceId}`);
366
+ if (!space) {
367
+ console.log(`实例 ${username}/${instanceId} 未找到,不尝试获取监控数据`);
368
+ return res.status(404).json({ error: '实例未找到,无法获取监控数据' });
369
+ }
370
+ if (space.status.toLowerCase() !== 'running') {
371
+ console.log(`实例 ${username}/${instanceId} 状态为 ${space.status},不尝试获取监控数据`);
372
+ return res.status(400).json({ error: '实例未运行,无法���取监控数据' });
373
+ }
374
+
375
+ const token = userTokenMapping[username];
376
+ let headers = {
377
+ 'Accept': 'text/event-stream',
378
+ 'Cache-Control': 'no-cache',
379
+ 'Connection': 'keep-alive'
380
+ };
381
+ if (token) {
382
+ headers['Authorization'] = `Bearer ${token}`;
383
+ }
384
+
385
+ const response = await axios({
386
+ method: 'get',
387
+ url,
388
+ headers,
389
+ responseType: 'stream',
390
+ timeout: 10000
391
+ });
392
+
393
+ res.set({
394
+ 'Content-Type': 'text/event-stream',
395
+ 'Cache-Control': 'no-cache',
396
+ 'Connection': 'keep-alive'
397
+ });
398
+ response.data.pipe(res);
399
+
400
+ req.on('close', () => {
401
+ response.data.destroy();
402
+ });
403
+ } catch (error) {
404
+ console.error(`代理获取直播监控数据失败 (${req.params.username}/${req.params.instanceId}):`, error.message);
405
+ res.status(error.response?.status || 500).json({ error: '获取监控数据失败', details: error.message });
406
+ }
407
+ });
408
+
409
+ // 处理其他请求,重定向到 index.html
410
+ app.get('*', (req, res) => {
411
+ res.sendFile(path.join(__dirname, 'public', 'index.html'));
412
+ });
413
+
414
+ // 定期清理过期的会话
415
+ setInterval(() => {
416
+ const now = Date.now();
417
+ for (const [token, session] of sessions.entries()) {
418
+ if (session.expiresAt < now) {
419
+ sessions.delete(token);
420
+ console.log(`Token ${token.slice(0, 8)}... 已过期,自动清理`);
421
+ }
422
+ }
423
+ }, 60 * 60 * 1000); // 每小时清理一次
424
+
425
+ app.listen(port, () => {
426
+ console.log(`Server running on port ${port}`);
427
+ console.log(`User configurations:`, usernames.map(user => `${user}: ${userTokenMapping[user] ? 'Token Configured' : 'No Token'}`).join(', ') || 'None');
428
+ console.log(`Admin login enabled: Username=${ADMIN_USERNAME}, Password=${ADMIN_PASSWORD ? 'Configured' : 'Not Configured'}`);
429
+ });