Spaces:
Sleeping
Sleeping
Update server.js
Browse files
server.js
CHANGED
@@ -1,10 +1,11 @@
|
|
1 |
const express = require('express');
|
2 |
const path = require('path');
|
3 |
const axios = require('axios');
|
|
|
4 |
const app = express();
|
5 |
const port = process.env.PORT || 8080;
|
6 |
|
7 |
-
//
|
8 |
const userTokenMapping = {};
|
9 |
const usernames = [];
|
10 |
const hfUserConfig = process.env.HF_USER || '';
|
@@ -22,6 +23,30 @@ if (hfUserConfig) {
|
|
22 |
});
|
23 |
}
|
24 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
25 |
// 提供静态文件(前端文件)
|
26 |
app.use(express.static(path.join(__dirname, 'public')));
|
27 |
|
@@ -30,36 +55,196 @@ app.get('/api/config', (req, res) => {
|
|
30 |
res.json({ usernames: usernames.join(',') });
|
31 |
});
|
32 |
|
33 |
-
//
|
34 |
app.get('/api/proxy/spaces', async (req, res) => {
|
35 |
try {
|
36 |
-
|
37 |
-
|
38 |
-
return res.
|
39 |
}
|
40 |
|
41 |
-
const
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
51 |
}
|
52 |
-
})
|
|
|
|
|
53 |
}
|
54 |
-
|
55 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
56 |
} catch (error) {
|
57 |
-
console.error(
|
58 |
-
res.status(error.response?.status || 500).json({
|
59 |
}
|
60 |
});
|
61 |
|
62 |
-
// 代理 HuggingFace API:获取实时监控数据(SSE
|
63 |
app.get('/api/proxy/live-metrics/:username/:instanceId', async (req, res) => {
|
64 |
try {
|
65 |
const { username, instanceId } = req.params;
|
@@ -69,12 +254,10 @@ app.get('/api/proxy/live-metrics/:username/:instanceId', async (req, res) => {
|
|
69 |
let headers = {};
|
70 |
if (token) {
|
71 |
headers = { 'Authorization': `Bearer ${token}` };
|
72 |
-
console.log(`用戶 ${username}/${instanceId} 使用 Token 連接監控數據`);
|
73 |
} else {
|
74 |
console.warn(`用户 ${username} 没有配置 API Token,将尝试无认证访问公开数据`);
|
75 |
}
|
76 |
|
77 |
-
// 发起请求并设置流式响应
|
78 |
const response = await axios({
|
79 |
method: 'get',
|
80 |
url,
|
@@ -82,7 +265,6 @@ app.get('/api/proxy/live-metrics/:username/:instanceId', async (req, res) => {
|
|
82 |
responseType: 'stream'
|
83 |
});
|
84 |
|
85 |
-
// 将响应流转发到客户端
|
86 |
res.set({
|
87 |
'Content-Type': 'text/event-stream',
|
88 |
'Cache-Control': 'no-cache',
|
@@ -90,7 +272,6 @@ app.get('/api/proxy/live-metrics/:username/:instanceId', async (req, res) => {
|
|
90 |
});
|
91 |
response.data.pipe(res);
|
92 |
|
93 |
-
// 监听错误
|
94 |
response.data.on('error', (err) => {
|
95 |
console.error(`直播监控数据流错误 (${username}/${instanceId}):`, err);
|
96 |
res.status(500).end();
|
@@ -100,7 +281,7 @@ app.get('/api/proxy/live-metrics/:username/:instanceId', async (req, res) => {
|
|
100 |
response.data.destroy();
|
101 |
});
|
102 |
} catch (error) {
|
103 |
-
console.error(`代理获取直播监控数据失败 (${req.params.username}/${req.params.instanceId}):`, error.message
|
104 |
res.status(error.response?.status || 500).json({ error: '获取监控数据失败', details: error.message });
|
105 |
}
|
106 |
});
|
|
|
1 |
const express = require('express');
|
2 |
const path = require('path');
|
3 |
const axios = require('axios');
|
4 |
+
const { HubApi } = require('@huggingface/hub');
|
5 |
const app = express();
|
6 |
const port = process.env.PORT || 8080;
|
7 |
|
8 |
+
// 从环境变量获取 HuggingFace 用户名和对应的 API Token 映射
|
9 |
const userTokenMapping = {};
|
10 |
const usernames = [];
|
11 |
const hfUserConfig = process.env.HF_USER || '';
|
|
|
23 |
});
|
24 |
}
|
25 |
|
26 |
+
// 缓存管理
|
27 |
+
class SpaceCache {
|
28 |
+
constructor() {
|
29 |
+
this.spaces = {};
|
30 |
+
this.lastUpdate = null;
|
31 |
+
}
|
32 |
+
|
33 |
+
updateAll(spacesData) {
|
34 |
+
this.spaces = spacesData.reduce((acc, space) => ({ ...acc, [space.repo_id]: space }), {});
|
35 |
+
this.lastUpdate = Date.now();
|
36 |
+
}
|
37 |
+
|
38 |
+
getAll() {
|
39 |
+
return Object.values(this.spaces);
|
40 |
+
}
|
41 |
+
|
42 |
+
isExpired(expireMinutes = 5) {
|
43 |
+
if (!this.lastUpdate) return true;
|
44 |
+
return (Date.now() - this.lastUpdate) > (expireMinutes * 60 * 1000);
|
45 |
+
}
|
46 |
+
}
|
47 |
+
|
48 |
+
const spaceCache = new SpaceCache();
|
49 |
+
|
50 |
// 提供静态文件(前端文件)
|
51 |
app.use(express.static(path.join(__dirname, 'public')));
|
52 |
|
|
|
55 |
res.json({ usernames: usernames.join(',') });
|
56 |
});
|
57 |
|
58 |
+
// 获取所有 spaces 列表(包括私有)
|
59 |
app.get('/api/proxy/spaces', async (req, res) => {
|
60 |
try {
|
61 |
+
if (!spaceCache.isExpired()) {
|
62 |
+
console.log('从缓存获取 Spaces 数据');
|
63 |
+
return res.json(spaceCache.getAll());
|
64 |
}
|
65 |
|
66 |
+
const allSpaces = [];
|
67 |
+
for (const username of usernames) {
|
68 |
+
const token = userTokenMapping[username];
|
69 |
+
if (!token) {
|
70 |
+
console.warn(`用户 ${username} 没有配置 API Token,将尝试无认证访问公开数据`);
|
71 |
+
}
|
72 |
+
|
73 |
+
try {
|
74 |
+
const hubApi = new HubApi({ accessToken: token || undefined });
|
75 |
+
const spaces = await hubApi.listSpaces({ author: username });
|
76 |
+
console.log(`获取到 ${spaces.length} 个 Spaces for ${username}`);
|
77 |
+
|
78 |
+
for (const space of spaces) {
|
79 |
+
try {
|
80 |
+
const spaceInfo = await hubApi.getSpaceInfo({ repoId: space.id });
|
81 |
+
const spaceRuntime = spaceInfo.runtime || {};
|
82 |
+
|
83 |
+
allSpaces.push({
|
84 |
+
repo_id: spaceInfo.id,
|
85 |
+
name: spaceInfo.cardData?.title || spaceInfo.id.split('/')[1],
|
86 |
+
owner: spaceInfo.author,
|
87 |
+
username: username,
|
88 |
+
token: token || '',
|
89 |
+
url: `https://${spaceInfo.author}-${spaceInfo.id.split('/')[1]}.hf.space`,
|
90 |
+
status: spaceRuntime.stage || 'unknown',
|
91 |
+
last_modified: spaceInfo.lastModified || 'unknown',
|
92 |
+
created_at: spaceInfo.createdAt || 'unknown',
|
93 |
+
sdk: spaceInfo.sdk || 'unknown',
|
94 |
+
tags: spaceInfo.tags || [],
|
95 |
+
private: spaceInfo.private || false,
|
96 |
+
app_port: spaceInfo.cardData?.app_port || 'unknown'
|
97 |
+
});
|
98 |
+
} catch (error) {
|
99 |
+
console.error(`处理 Space ${space.id} 失败:`, error.message);
|
100 |
+
}
|
101 |
}
|
102 |
+
} catch (error) {
|
103 |
+
console.error(`获取 Spaces 列表失败 for ${username}:`, error.message);
|
104 |
+
}
|
105 |
}
|
106 |
+
|
107 |
+
allSpaces.sort((a, b) => a.name.localeCompare(b.name));
|
108 |
+
spaceCache.updateAll(allSpaces);
|
109 |
+
console.log(`总共获取到 ${allSpaces.length} 个 Spaces`);
|
110 |
+
res.json(allSpaces);
|
111 |
+
} catch (error) {
|
112 |
+
console.error(`代理获取 spaces 列表失败:`, error.message);
|
113 |
+
res.status(500).json({ error: '获取 spaces 列表失败', details: error.message });
|
114 |
+
}
|
115 |
+
});
|
116 |
+
|
117 |
+
// 代理重启 Space
|
118 |
+
app.post('/api/proxy/restart/:repoId', async (req, res) => {
|
119 |
+
try {
|
120 |
+
const { repoId } = req.params;
|
121 |
+
const spaces = spaceCache.getAll();
|
122 |
+
const space = spaces.find(s => s.repo_id === repoId);
|
123 |
+
if (!space || !space.token) {
|
124 |
+
return res.status(404).json({ error: 'Space 未找到或无 Token 配置' });
|
125 |
+
}
|
126 |
+
|
127 |
+
const hubApi = new HubApi({ accessToken: space.token });
|
128 |
+
await hubApi.restartSpace({ repoId });
|
129 |
+
res.json({ success: true, message: `Space ${repoId} 重启成功` });
|
130 |
+
} catch (error) {
|
131 |
+
console.error(`重启 space 失败 (${req.params.repoId}):`, error.message);
|
132 |
+
res.status(error.response?.status || 500).json({ error: '重启 space 失败', details: error.message });
|
133 |
+
}
|
134 |
+
});
|
135 |
+
|
136 |
+
// 代理重建 Space
|
137 |
+
app.post('/api/proxy/rebuild/:repoId', async (req, res) => {
|
138 |
+
try {
|
139 |
+
const { repoId } = req.params;
|
140 |
+
const spaces = spaceCache.getAll();
|
141 |
+
const space = spaces.find(s => s.repo_id === repoId);
|
142 |
+
if (!space || !space.token) {
|
143 |
+
return res.status(404).json({ error: 'Space 未找到或无 Token 配置' });
|
144 |
+
}
|
145 |
+
|
146 |
+
const hubApi = new HubApi({ accessToken: space.token });
|
147 |
+
await hubApi.restartSpace({ repoId, factoryReboot: true });
|
148 |
+
res.json({ success: true, message: `Space ${repoId} 重建成功` });
|
149 |
+
} catch (error) {
|
150 |
+
console.error(`重建 space 失败 (${req.params.repoId}):`, error.message);
|
151 |
+
res.status(error.response?.status || 500).json({ error: '重建 space 失败', details: error.message });
|
152 |
+
}
|
153 |
+
});
|
154 |
+
|
155 |
+
// 外部 API 服务(类似于 Flask 的 /api/v1)
|
156 |
+
app.get('/api/v1/info/:token', async (req, res) => {
|
157 |
+
try {
|
158 |
+
const { token } = req.params;
|
159 |
+
const authHeader = req.headers.authorization;
|
160 |
+
if (!authHeader || !authHeader.startsWith('Bearer ') || authHeader.split(' ')[1] !== process.env.API_KEY) {
|
161 |
+
return res.status(401).json({ error: '无效的 API 密钥' });
|
162 |
+
}
|
163 |
+
|
164 |
+
const hubApi = new HubApi({ accessToken: token });
|
165 |
+
const userInfo = await hubApi.whoami();
|
166 |
+
const username = userInfo.name;
|
167 |
+
const spaces = await hubApi.listSpaces({ author: username });
|
168 |
+
const spaceList = [];
|
169 |
+
|
170 |
+
for (const space of spaces) {
|
171 |
+
try {
|
172 |
+
const spaceInfo = await hubApi.getSpaceInfo({ repoId: space.id });
|
173 |
+
spaceList.push(spaceInfo.id);
|
174 |
+
} catch (error) {
|
175 |
+
console.error(`获取 Space 信息失败 (${space.id}):`, error.message);
|
176 |
+
}
|
177 |
+
}
|
178 |
+
|
179 |
+
res.json({ spaces: spaceList, total: spaceList.length });
|
180 |
+
} catch (error) {
|
181 |
+
console.error(`获取 spaces 列表失败 (外部 API):`, error.message);
|
182 |
+
res.status(500).json({ error: error.message });
|
183 |
+
}
|
184 |
+
});
|
185 |
+
|
186 |
+
app.get('/api/v1/info/:token/:spaceId(*) ', async (req, res) => {
|
187 |
+
try {
|
188 |
+
const { token, spaceId } = req.params;
|
189 |
+
const authHeader = req.headers.authorization;
|
190 |
+
if (!authHeader || !authHeader.startsWith('Bearer ') || authHeader.split(' ')[1] !== process.env.API_KEY) {
|
191 |
+
return res.status(401).json({ error: '无效的 API 密钥' });
|
192 |
+
}
|
193 |
+
|
194 |
+
const hubApi = new HubApi({ accessToken: token });
|
195 |
+
const spaceInfo = await hubApi.getSpaceInfo({ repoId: spaceId });
|
196 |
+
const spaceRuntime = spaceInfo.runtime || {};
|
197 |
+
|
198 |
+
res.json({
|
199 |
+
id: spaceInfo.id,
|
200 |
+
status: spaceRuntime.stage || 'unknown',
|
201 |
+
last_modified: spaceInfo.lastModified || null,
|
202 |
+
created_at: spaceInfo.createdAt || null,
|
203 |
+
sdk: spaceInfo.sdk || 'unknown',
|
204 |
+
tags: spaceInfo.tags || [],
|
205 |
+
private: spaceInfo.private || false
|
206 |
+
});
|
207 |
+
} catch (error) {
|
208 |
+
console.error(`获取 space 信息失败 (外部 API):`, error.message);
|
209 |
+
res.status(error.response?.status || 404).json({ error: error.message });
|
210 |
+
}
|
211 |
+
});
|
212 |
+
|
213 |
+
app.post('/api/v1/action/:token/:spaceId(*)/restart', async (req, res) => {
|
214 |
+
try {
|
215 |
+
const { token, spaceId } = req.params;
|
216 |
+
const authHeader = req.headers.authorization;
|
217 |
+
if (!authHeader || !authHeader.startsWith('Bearer ') || authHeader.split(' ')[1] !== process.env.API_KEY) {
|
218 |
+
return res.status(401).json({ error: '无效的 API 密钥' });
|
219 |
+
}
|
220 |
+
|
221 |
+
const hubApi = new HubApi({ accessToken: token });
|
222 |
+
await hubApi.restartSpace({ repoId: spaceId });
|
223 |
+
res.json({ success: true, message: `Space ${spaceId} 重启成功` });
|
224 |
+
} catch (error) {
|
225 |
+
console.error(`重启 space 失败 (外部 API):`, error.message);
|
226 |
+
res.status(error.response?.status || 500).json({ success: false, error: error.message });
|
227 |
+
}
|
228 |
+
});
|
229 |
+
|
230 |
+
app.post('/api/v1/action/:token/:spaceId(*)/rebuild', async (req, res) => {
|
231 |
+
try {
|
232 |
+
const { token, spaceId } = req.params;
|
233 |
+
const authHeader = req.headers.authorization;
|
234 |
+
if (!authHeader || !authHeader.startsWith('Bearer ') || authHeader.split(' ')[1] !== process.env.API_KEY) {
|
235 |
+
return res.status(401).json({ error: '无效的 API 密钥' });
|
236 |
+
}
|
237 |
+
|
238 |
+
const hubApi = new HubApi({ accessToken: token });
|
239 |
+
await hubApi.restartSpace({ repoId: spaceId, factoryReboot: true });
|
240 |
+
res.json({ success: true, message: `Space ${spaceId} 重建成功` });
|
241 |
} catch (error) {
|
242 |
+
console.error(`重建 space 失败 (外部 API):`, error.message);
|
243 |
+
res.status(error.response?.status || 500).json({ success: false, error: error.message });
|
244 |
}
|
245 |
});
|
246 |
|
247 |
+
// 代理 HuggingFace API:获取实时监控数据(SSE)
|
248 |
app.get('/api/proxy/live-metrics/:username/:instanceId', async (req, res) => {
|
249 |
try {
|
250 |
const { username, instanceId } = req.params;
|
|
|
254 |
let headers = {};
|
255 |
if (token) {
|
256 |
headers = { 'Authorization': `Bearer ${token}` };
|
|
|
257 |
} else {
|
258 |
console.warn(`用户 ${username} 没有配置 API Token,将尝试无认证访问公开数据`);
|
259 |
}
|
260 |
|
|
|
261 |
const response = await axios({
|
262 |
method: 'get',
|
263 |
url,
|
|
|
265 |
responseType: 'stream'
|
266 |
});
|
267 |
|
|
|
268 |
res.set({
|
269 |
'Content-Type': 'text/event-stream',
|
270 |
'Cache-Control': 'no-cache',
|
|
|
272 |
});
|
273 |
response.data.pipe(res);
|
274 |
|
|
|
275 |
response.data.on('error', (err) => {
|
276 |
console.error(`直播监控数据流错误 (${username}/${instanceId}):`, err);
|
277 |
res.status(500).end();
|
|
|
281 |
response.data.destroy();
|
282 |
});
|
283 |
} catch (error) {
|
284 |
+
console.error(`代理获取直播监控数据失败 (${req.params.username}/${req.params.instanceId}):`, error.message);
|
285 |
res.status(error.response?.status || 500).json({ error: '获取监控数据失败', details: error.message });
|
286 |
}
|
287 |
});
|