| const express = require('express'); |
| const path = require('path'); |
| const axios = require('axios'); |
| const crypto = require('crypto'); |
| const app = express(); |
| const port = process.env.PORT || 8080; |
|
|
| |
| app.use(express.json()); |
| app.use(express.urlencoded({ extended: true })); |
|
|
| |
| const userTokenMapping = {}; |
| const usernames = []; |
| const hfUserConfig = process.env.HF_USER || ''; |
| if (hfUserConfig) { |
| hfUserConfig.split(',').forEach(pair => { |
| const parts = pair.split(':').map(part => part.trim()); |
| const username = parts[0]; |
| const token = parts[1] || ''; |
| if (username) { |
| usernames.push(username); |
| if (token) { |
| userTokenMapping[username] = token; |
| } |
| } |
| }); |
| } |
|
|
| |
| const ADMIN_USERNAME = process.env.USER_NAME || 'admin'; |
| const ADMIN_PASSWORD = process.env.USER_PASSWORD || 'password'; |
|
|
| |
| const SHOW_PRIVATE = process.env.SHOW_PRIVATE === 'true'; |
| console.log(`SHOW_PRIVATE 配置: ${SHOW_PRIVATE ? '未登录时展示 private 实例' : '未登录时隐藏 private 实例'}`); |
|
|
| |
| const sessions = new Map(); |
| const SESSION_TIMEOUT = 24 * 60 * 60 * 1000; |
|
|
| |
| class SpaceCache { |
| constructor() { |
| this.spaces = {}; |
| this.lastUpdate = null; |
| } |
|
|
| updateAll(spacesData) { |
| this.spaces = spacesData.reduce((acc, space) => ({ ...acc, [space.repo_id]: space }), {}); |
| this.lastUpdate = Date.now(); |
| } |
|
|
| getAll() { |
| return Object.values(this.spaces); |
| } |
|
|
| isExpired(expireMinutes = 5) { |
| if (!this.lastUpdate) return true; |
| return (Date.now() - this.lastUpdate) > (expireMinutes * 60 * 1000); |
| } |
|
|
| invalidate() { |
| this.lastUpdate = null; |
| } |
| } |
|
|
| const spaceCache = new SpaceCache(); |
|
|
| |
| async function fetchSpacesWithRetry(username, token, maxRetries = 3, retryDelay = 2000) { |
| let retries = 0; |
| while (retries < maxRetries) { |
| try { |
| |
| const headers = token ? { 'Authorization': `Bearer ${token}` } : {}; |
| const response = await axios.get(`https://huggingface.co/api/spaces?author=${username}`, { |
| headers, |
| timeout: 10000 |
| }); |
| const spaces = response.data; |
| console.log(`获取到 ${spaces.length} 个 Spaces for ${username} (尝试 ${retries + 1}/${maxRetries}),使用 ${token ? 'Token 认证' : '无认证'}`); |
| return spaces; |
| } catch (error) { |
| retries++; |
| let errorDetail = error.message; |
| if (error.response) { |
| errorDetail += `, HTTP Status: ${error.response.status}`; |
| } else if (error.request) { |
| errorDetail += ', No response received (possible network issue)'; |
| } |
| console.error(`获取 Spaces 列表失败 for ${username} (尝试 ${retries}/${maxRetries}): ${errorDetail},使用 ${token ? 'Token 认证' : '无认证'}`); |
| if (retries < maxRetries) { |
| console.log(`等待 ${retryDelay/1000} 秒后重试...`); |
| await new Promise(resolve => setTimeout(resolve, retryDelay)); |
| } else { |
| console.error(`达到最大重试次数 (${maxRetries}),放弃重试 for ${username}`); |
| return []; |
| } |
| } |
| } |
| return []; |
| } |
|
|
| |
| app.use(express.static(path.join(__dirname, 'public'))); |
|
|
| |
| app.get('/api/config', (req, res) => { |
| res.json({ usernames: usernames.join(',') }); |
| }); |
|
|
| |
| app.post('/api/login', (req, res) => { |
| const { username, password } = req.body; |
| if (username === ADMIN_USERNAME && password === ADMIN_PASSWORD) { |
| |
| const token = crypto.randomBytes(16).toString('hex'); |
| const expiresAt = Date.now() + SESSION_TIMEOUT; |
| sessions.set(token, { username, expiresAt }); |
| console.log(`用户 ${username} 登录成功,生成 token: ${token.slice(0, 8)}...`); |
| res.json({ success: true, token }); |
| } else { |
| console.log(`用户 ${username} 登录失败,凭据无效`); |
| res.status(401).json({ success: false, message: '用户名或密码错误' }); |
| } |
| }); |
|
|
| |
| app.post('/api/verify-token', (req, res) => { |
| const { token } = req.body; |
| const session = sessions.get(token); |
| if (session && session.expiresAt > Date.now()) { |
| res.json({ success: true, message: 'Token 有效' }); |
| } else { |
| if (session) { |
| sessions.delete(token); |
| console.log(`Token ${token.slice(0, 8)}... 已过期,已删除`); |
| } |
| res.status(401).json({ success: false, message: 'Token 无效或已过期' }); |
| } |
| }); |
|
|
| |
| app.post('/api/logout', (req, res) => { |
| const { token } = req.body; |
| sessions.delete(token); |
| console.log(`Token ${token.slice(0, 8)}... 已手动登出`); |
| res.json({ success: true, message: '登出成功' }); |
| }); |
|
|
| |
| const authenticateToken = (req, res, next) => { |
| const authHeader = req.headers['authorization']; |
| if (!authHeader || !authHeader.startsWith('Bearer ')) { |
| return res.status(401).json({ error: '未提供有效的认证令牌' }); |
| } |
| const token = authHeader.split(' ')[1]; |
| const session = sessions.get(token); |
| if (session && session.expiresAt > Date.now()) { |
| req.session = session; |
| next(); |
| } else { |
| if (session) { |
| sessions.delete(token); |
| console.log(`Token ${token.slice(0, 8)}... 已过期,拒绝访问`); |
| } |
| return res.status(401).json({ error: '认证令牌无效或已过期' }); |
| } |
| }; |
|
|
| |
| app.get('/api/proxy/spaces', async (req, res) => { |
| try { |
| |
| let isAuthenticated = false; |
| const authHeader = req.headers['authorization']; |
| if (authHeader && authHeader.startsWith('Bearer ')) { |
| const token = authHeader.split(' ')[1]; |
| const session = sessions.get(token); |
| if (session && session.expiresAt > Date.now()) { |
| isAuthenticated = true; |
| console.log(`用户已登录,Token: ${token.slice(0, 8)}...`); |
| } else { |
| if (session) { |
| sessions.delete(token); |
| console.log(`Token ${token.slice(0, 8)}... 已过期,拒绝访问`); |
| } |
| console.log('用户认证失败,无有效 Token'); |
| } |
| } else { |
| console.log('用户未提供认证令牌'); |
| } |
|
|
| |
| const cachedSpaces = spaceCache.getAll(); |
| if (cachedSpaces.length === 0 || spaceCache.isExpired()) { |
| console.log(cachedSpaces.length === 0 ? '缓存为空,强制重新获取数据' : '缓存已过期,重新获取数据'); |
| const allSpaces = []; |
| for (const username of usernames) { |
| const token = userTokenMapping[username]; |
| if (!token) { |
| console.warn(`用户 ${username} 没有配置 API Token,将尝试无认证访问公开数据`); |
| } |
|
|
| try { |
| const spaces = await fetchSpacesWithRetry(username, token); |
| for (const space of spaces) { |
| try { |
| |
| const headers = token ? { 'Authorization': `Bearer ${token}` } : {}; |
| const spaceInfoResponse = await axios.get(`https://huggingface.co/api/spaces/${space.id}`, { headers }); |
| const spaceInfo = spaceInfoResponse.data; |
| const spaceRuntime = spaceInfo.runtime || {}; |
|
|
| allSpaces.push({ |
| repo_id: spaceInfo.id, |
| name: spaceInfo.cardData?.title || spaceInfo.id.split('/')[1], |
| owner: spaceInfo.author, |
| username: username, |
| url: `https://${spaceInfo.author}-${spaceInfo.id.split('/')[1]}.hf.space`, |
| status: spaceRuntime.stage || 'unknown', |
| last_modified: spaceInfo.lastModified || 'unknown', |
| created_at: spaceInfo.createdAt || 'unknown', |
| sdk: spaceInfo.sdk || 'unknown', |
| tags: spaceInfo.tags || [], |
| private: spaceInfo.private || false, |
| app_port: spaceInfo.cardData?.app_port || 'unknown' |
| }); |
| } catch (error) { |
| console.error(`处理 Space ${space.id} 失败:`, error.message, `使用 ${token ? 'Token 认证' : '无认证'}`); |
| } |
| } |
| } catch (error) { |
| console.error(`获取 Spaces 列表失败 for ${username}:`, error.message, `使用 ${token ? 'Token 认证' : '无认证'}`); |
| } |
| } |
|
|
| allSpaces.sort((a, b) => a.name.localeCompare(b.name)); |
| spaceCache.updateAll(allSpaces); |
| console.log(`总共获取到 ${allSpaces.length} 个 Spaces`); |
|
|
| const safeSpaces = allSpaces.map(space => { |
| const { token, ...safeSpace } = space; |
| return safeSpace; |
| }); |
|
|
| if (isAuthenticated) { |
| console.log('用户已登录,返回所有实例(包括 private)'); |
| res.json(safeSpaces); |
| } else if (SHOW_PRIVATE) { |
| console.log('用户未登录,但 SHOW_PRIVATE 为 true,返回所有实例'); |
| res.json(safeSpaces); |
| } else { |
| console.log('用户未登录,SHOW_PRIVATE 为 false,过滤 private 实例'); |
| res.json(safeSpaces.filter(space => !space.private)); |
| } |
| } else { |
| console.log('从缓存获取 Spaces 数据'); |
| const safeSpaces = cachedSpaces.map(space => { |
| const { token, ...safeSpace } = space; |
| return safeSpace; |
| }); |
|
|
| if (isAuthenticated) { |
| console.log('用户已登录,返回所有缓存实例(包括 private)'); |
| return res.json(safeSpaces); |
| } else if (SHOW_PRIVATE) { |
| console.log('用户未登录,但 SHOW_PRIVATE 为 true,返回所有缓存实例'); |
| return res.json(safeSpaces); |
| } else { |
| console.log('用户未登录,SHOW_PRIVATE 为 false,过滤 private 实例'); |
| return res.json(safeSpaces.filter(space => !space.private)); |
| } |
| } |
| } catch (error) { |
| console.error(`代理获取 spaces 列表失败:`, error.message); |
| res.status(500).json({ error: '获取 spaces 列表失败', details: error.message }); |
| } |
| }); |
|
|
| |
| app.post('/api/proxy/restart/:repoId(*)', authenticateToken, async (req, res) => { |
| try { |
| const { repoId } = req.params; |
| console.log(`尝试重启 Space: ${repoId}`); |
| const spaces = spaceCache.getAll(); |
| const space = spaces.find(s => s.repo_id === repoId); |
| if (!space || !userTokenMapping[space.username]) { |
| console.error(`Space ${repoId} 未找到或无 Token 配置`); |
| return res.status(404).json({ error: 'Space 未找到或无 Token 配置' }); |
| } |
|
|
| const headers = { 'Authorization': `Bearer ${userTokenMapping[space.username]}`, 'Content-Type': 'application/json' }; |
| const response = await axios.post(`https://huggingface.co/api/spaces/${repoId}/restart`, {}, { headers }); |
| console.log(`重启 Space ${repoId} 成功,状态码: ${response.status}`); |
| res.json({ success: true, message: `Space ${repoId} 重启成功` }); |
| } catch (error) { |
| console.error(`重启 space 失败 (${req.params.repoId}):`, error.message); |
| if (error.response) { |
| console.error(`状态码: ${error.response.status}, 响应数据:`, error.response.data); |
| res.status(error.response.status || 500).json({ error: '重启 space 失败', details: error.response.data?.message || error.message }); |
| } else { |
| res.status(500).json({ error: '重启 space 失败', details: error.message }); |
| } |
| } |
| }); |
|
|
| |
| app.post('/api/proxy/rebuild/:repoId(*)', authenticateToken, async (req, res) => { |
| try { |
| const { repoId } = req.params; |
| console.log(`尝试重建 Space: ${repoId}`); |
| const spaces = spaceCache.getAll(); |
| const space = spaces.find(s => s.repo_id === repoId); |
| if (!space || !userTokenMapping[space.username]) { |
| console.error(`Space ${repoId} 未找到或无 Token 配置`); |
| return res.status(404).json({ error: 'Space 未找到或无 Token 配置' }); |
| } |
|
|
| const headers = { 'Authorization': `Bearer ${userTokenMapping[space.username]}`, 'Content-Type': 'application/json' }; |
| |
| const response = await axios.post( |
| `https://huggingface.co/api/spaces/${repoId}/restart?factory=true`, |
| {}, |
| { headers } |
| ); |
| console.log(`重建 Space ${repoId} 成功,状态码: ${response.status}`); |
| res.json({ success: true, message: `Space ${repoId} 重建成功` }); |
| } catch (error) { |
| console.error(`重建 space 失败 (${req.params.repoId}):`, error.message); |
| if (error.response) { |
| console.error(`状态码: ${error.response.status}, 响应数据:`, error.response.data); |
| res.status(error.response.status || 500).json({ error: '重建 space 失败', details: error.response.data?.message || error.message }); |
| } else { |
| res.status(500).json({ error: '重建 space 失败', details: error.message }); |
| } |
| } |
| }); |
|
|
| |
| app.get('/api/v1/info/:token', async (req, res) => { |
| try { |
| const { token } = req.params; |
| const authHeader = req.headers.authorization; |
| if (!authHeader || !authHeader.startsWith('Bearer ') || authHeader.split(' ')[1] !== process.env.API_KEY) { |
| return res.status(401).json({ error: '无效的 API 密钥' }); |
| } |
|
|
| const headers = { 'Authorization': `Bearer ${token}` }; |
| const userInfoResponse = await axios.get('https://huggingface.co/api/whoami-v2', { headers }); |
| const username = userInfoResponse.data.name; |
| const spacesResponse = await axios.get(`https://huggingface.co/api/spaces?author=${username}`, { headers }); |
| const spaces = spacesResponse.data; |
| const spaceList = []; |
|
|
| for (const space of spaces) { |
| try { |
| const spaceInfoResponse = await axios.get(`https://huggingface.co/api/spaces/${space.id}`, { headers }); |
| spaceList.push(spaceInfoResponse.data.id); |
| } catch (error) { |
| console.error(`获取 Space 信息失败 (${space.id}):`, error.message); |
| } |
| } |
|
|
| res.json({ spaces: spaceList, total: spaceList.length }); |
| } catch (error) { |
| console.error(`获取 spaces 列表失败 (外部 API):`, error.message); |
| res.status(500).json({ error: error.message }); |
| } |
| }); |
|
|
| app.get('/api/v1/info/:token/:spaceId(*)', async (req, res) => { |
| try { |
| const { token, spaceId } = req.params; |
| const authHeader = req.headers.authorization; |
| if (!authHeader || !authHeader.startsWith('Bearer ') || authHeader.split(' ')[1] !== process.env.API_KEY) { |
| return res.status(401).json({ error: '无效的 API 密钥' }); |
| } |
|
|
| const headers = { 'Authorization': `Bearer ${token}` }; |
| const spaceInfoResponse = await axios.get(`https://huggingface.co/api/spaces/${spaceId}`, { headers }); |
| const spaceInfo = spaceInfoResponse.data; |
| const spaceRuntime = spaceInfo.runtime || {}; |
|
|
| res.json({ |
| id: spaceInfo.id, |
| status: spaceRuntime.stage || 'unknown', |
| last_modified: spaceInfo.lastModified || null, |
| created_at: spaceInfo.createdAt || null, |
| sdk: spaceInfo.sdk || 'unknown', |
| tags: spaceInfo.tags || [], |
| private: spaceInfo.private || false |
| }); |
| } catch (error) { |
| console.error(`获取 space 信息失败 (外部 API):`, error.message); |
| res.status(error.response?.status || 404).json({ error: error.message }); |
| } |
| }); |
|
|
| app.post('/api/v1/action/:token/:spaceId(*)/restart', async (req, res) => { |
| try { |
| const { token, spaceId } = req.params; |
| const authHeader = req.headers.authorization; |
| if (!authHeader || !authHeader.startsWith('Bearer ') || authHeader.split(' ')[1] !== process.env.API_KEY) { |
| return res.status(401).json({ error: '无效的 API 密钥' }); |
| } |
|
|
| const headers = { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }; |
| await axios.post(`https://huggingface.co/api/spaces/${spaceId}/restart`, {}, { headers }); |
| res.json({ success: true, message: `Space ${spaceId} 重启成功` }); |
| } catch (error) { |
| console.error(`重启 space 失败 (外部 API):`, error.message); |
| res.status(error.response?.status || 500).json({ success: false, error: error.message }); |
| } |
| }); |
|
|
| app.post('/api/v1/action/:token/:spaceId(*)/rebuild', async (req, res) => { |
| try { |
| const { token, spaceId } = req.params; |
| const authHeader = req.headers.authorization; |
| if (!authHeader || !authHeader.startsWith('Bearer ') || authHeader.split(' ')[1] !== process.env.API_KEY) { |
| return res.status(401).json({ error: '无效的 API 密钥' }); |
| } |
|
|
| const headers = { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }; |
| console.log(`外部 API 发送重建请求,spaceId: ${spaceId}`); |
| |
| const response = await axios.post( |
| `https://huggingface.co/api/spaces/${spaceId}/restart?factory=true`, |
| {}, |
| { headers } |
| ); |
| console.log(`外部 API 重建 Space ${spaceId} 成功,状态码: ${response.status}`); |
| res.json({ success: true, message: `Space ${spaceId} 重建成功` }); |
| } catch (error) { |
| console.error(`重建 space 失败 (外部 API):`, error.message); |
| if (error.response) { |
| console.error(`状态码: ${error.response.status}, 响应数据:`, error.response.data); |
| res.status(error.response.status || 500).json({ success: false, error: error.response.data?.message || error.message }); |
| } else { |
| res.status(500).json({ success: false, error: error.message }); |
| } |
| } |
| }); |
|
|
| |
| class MetricsConnectionManager { |
| constructor() { |
| this.connections = new Map(); |
| this.clients = new Map(); |
| this.instanceData = new Map(); |
| } |
|
|
| |
| async connectToInstance(repoId, username, token) { |
| if (this.connections.has(repoId)) { |
| return this.connections.get(repoId); |
| } |
|
|
| const instanceId = repoId.split('/')[1]; |
| const url = `https://api.hf.space/v1/${username}/${instanceId}/live-metrics/sse`; |
| |
| const headers = token ? { |
| 'Authorization': `Bearer ${token}`, |
| 'Accept': 'text/event-stream', |
| 'Cache-Control': 'no-cache', |
| 'Connection': 'keep-alive' |
| } : { |
| 'Accept': 'text/event-stream', |
| 'Cache-Control': 'no-cache', |
| 'Connection': 'keep-alive' |
| }; |
|
|
| try { |
| const response = await axios({ |
| method: 'get', |
| url, |
| headers, |
| responseType: 'stream', |
| timeout: 10000 |
| }); |
|
|
| const stream = response.data; |
| stream.on('data', (chunk) => { |
| const chunkStr = chunk.toString(); |
| if (chunkStr.includes('event: metric')) { |
| const dataMatch = chunkStr.match(/data: (.*)/); |
| if (dataMatch && dataMatch[1]) { |
| try { |
| const metrics = JSON.parse(dataMatch[1]); |
| this.instanceData.set(repoId, metrics); |
| |
| this.clients.forEach((clientRes, clientId) => { |
| if (clientRes.subscribedInstances && clientRes.subscribedInstances.includes(repoId)) { |
| clientRes.write(`event: metric\n`); |
| clientRes.write(`data: ${JSON.stringify({ repoId, metrics })}\n\n`); |
| } |
| }); |
| } catch (error) { |
| console.error(`解析监控数据失败 (${repoId}):`, error.message); |
| } |
| } |
| } |
| }); |
|
|
| stream.on('error', (error) => { |
| console.error(`监控连接错误 (${repoId}):`, error.message); |
| this.connections.delete(repoId); |
| this.instanceData.delete(repoId); |
| }); |
|
|
| stream.on('end', () => { |
| console.log(`监控连接结束 (${repoId})`); |
| this.connections.delete(repoId); |
| this.instanceData.delete(repoId); |
| }); |
|
|
| this.connections.set(repoId, stream); |
| console.log(`已建立监控连接 (${repoId}),使用 ${token ? 'Token 认证' : '无认证'}`); |
| return stream; |
| } catch (error) { |
| console.error(`无法连接到监控端点 (${repoId}):`, error.message); |
| this.connections.delete(repoId); |
| return null; |
| } |
| } |
|
|
| |
| registerClient(clientId, res, subscribedInstances) { |
| res.subscribedInstances = subscribedInstances || []; |
| this.clients.set(clientId, res); |
| console.log(`客户端 ${clientId} 注册,订阅实例: ${res.subscribedInstances.join(', ') || '无'}`); |
| |
| |
| res.subscribedInstances.forEach(repoId => { |
| if (this.instanceData.has(repoId)) { |
| const metrics = this.instanceData.get(repoId); |
| res.write(`event: metric\n`); |
| res.write(`data: ${JSON.stringify({ repoId, metrics })}\n\n`); |
| } |
| }); |
| } |
|
|
| |
| unregisterClient(clientId) { |
| this.clients.delete(clientId); |
| console.log(`客户端 ${clientId} 断开连接`); |
| this.cleanupConnections(); |
| } |
|
|
| |
| updateClientSubscriptions(clientId, subscribedInstances) { |
| const clientRes = this.clients.get(clientId); |
| if (clientRes) { |
| clientRes.subscribedInstances = subscribedInstances || []; |
| console.log(`客户端 ${clientId} 更新订阅: ${clientRes.subscribedInstances.join(', ') || '无'}`); |
| |
| subscribedInstances.forEach(repoId => { |
| if (this.instanceData.has(repoId)) { |
| const metrics = this.instanceData.get(repoId); |
| clientRes.write(`event: metric\n`); |
| clientRes.write(`data: ${JSON.stringify({ repoId, metrics })}\n\n`); |
| } |
| }); |
| } |
| this.cleanupConnections(); |
| } |
|
|
| |
| cleanupConnections() { |
| const subscribedRepoIds = new Set(); |
| this.clients.forEach(clientRes => { |
| clientRes.subscribedInstances.forEach(repoId => subscribedRepoIds.add(repoId)); |
| }); |
|
|
| const toRemove = []; |
| this.connections.forEach((stream, repoId) => { |
| if (!subscribedRepoIds.has(repoId)) { |
| toRemove.push(repoId); |
| stream.destroy(); |
| console.log(`清理未订阅的监控连接 (${repoId})`); |
| } |
| }); |
|
|
| toRemove.forEach(repoId => { |
| this.connections.delete(repoId); |
| this.instanceData.delete(repoId); |
| }); |
| } |
| } |
|
|
| const metricsManager = new MetricsConnectionManager(); |
|
|
| |
| app.get('/api/proxy/live-metrics-stream', (req, res) => { |
| |
| res.set({ |
| 'Content-Type': 'text/event-stream', |
| 'Cache-Control': 'no-cache', |
| 'Connection': 'keep-alive' |
| }); |
|
|
| |
| const clientId = crypto.randomBytes(8).toString('hex'); |
| |
| |
| const instancesParam = req.query.instances || ''; |
| const token = req.query.token || ''; |
| const subscribedInstances = instancesParam.split(',').filter(id => id.trim() !== ''); |
|
|
| |
| let isAuthenticated = false; |
| if (token) { |
| const session = sessions.get(token); |
| if (session && session.expiresAt > Date.now()) { |
| isAuthenticated = true; |
| console.log(`SSE 用户已登录,Token: ${token.slice(0, 8)}...`); |
| } else { |
| if (session) { |
| sessions.delete(token); |
| console.log(`SSE Token ${token.slice(0, 8)}... 已过期,拒绝访问`); |
| } |
| console.log('SSE 用户认证失败,无有效 Token'); |
| } |
| } else { |
| console.log('SSE 用户未提供认证令牌'); |
| } |
|
|
| |
| metricsManager.registerClient(clientId, res, subscribedInstances); |
|
|
| |
| const spaces = spaceCache.getAll(); |
| subscribedInstances.forEach(repoId => { |
| const space = spaces.find(s => s.repo_id === repoId); |
| if (space) { |
| const username = space.username; |
| const token = userTokenMapping[username] || ''; |
| metricsManager.connectToInstance(repoId, username, token); |
| } |
| }); |
|
|
| |
| req.on('close', () => { |
| metricsManager.unregisterClient(clientId); |
| console.log(`客户端 ${clientId} 断开 SSE 连接`); |
| }); |
| }); |
|
|
| |
| app.post('/api/proxy/update-subscriptions', (req, res) => { |
| const { clientId, instances } = req.body; |
| if (!clientId || !instances || !Array.isArray(instances)) { |
| return res.status(400).json({ error: '缺少 clientId 或 instances 参数' }); |
| } |
|
|
| metricsManager.updateClientSubscriptions(clientId, instances); |
| |
| const spaces = spaceCache.getAll(); |
| instances.forEach(repoId => { |
| const space = spaces.find(s => s.repo_id === repoId); |
| if (space) { |
| const username = space.username; |
| const token = userTokenMapping[username] || ''; |
| metricsManager.connectToInstance(repoId, username, token); |
| } |
| }); |
|
|
| res.json({ success: true, message: '订阅列表已更新' }); |
| }); |
|
|
| |
| app.get('*', (req, res) => { |
| res.sendFile(path.join(__dirname, 'public', 'index.html')); |
| }); |
|
|
| |
| setInterval(() => { |
| const now = Date.now(); |
| for (const [token, session] of sessions.entries()) { |
| if (session.expiresAt < now) { |
| sessions.delete(token); |
| console.log(`Token ${token.slice(0, 8)}... 已过期,自动清理`); |
| } |
| } |
| }, 60 * 60 * 1000); |
|
|
| |
| const REFRESH_INTERVAL = 5 * 60 * 1000; |
| async function refreshSpacesCachePeriodically() { |
| console.log('启动定时刷新缓存任务...'); |
| setInterval(async () => { |
| try { |
| const cachedSpaces = spaceCache.getAll(); |
| if (spaceCache.isExpired() || cachedSpaces.length === 0) { |
| console.log('定时任务:缓存已过期或为空,重新获取 Spaces 数据'); |
| const allSpaces = []; |
| for (const username of usernames) { |
| const token = userTokenMapping[username]; |
| if (!token) { |
| console.warn(`用户 ${username} 没有配置 API Token,将尝试无认证访问公开数据`); |
| } |
| try { |
| const spaces = await fetchSpacesWithRetry(username, token); |
| for (const space of spaces) { |
| try { |
| const headers = token ? { 'Authorization': `Bearer ${token}` } : {}; |
| const spaceInfoResponse = await axios.get(`https://huggingface.co/api/spaces/${space.id}`, { headers }); |
| const spaceInfo = spaceInfoResponse.data; |
| const spaceRuntime = spaceInfo.runtime || {}; |
|
|
| allSpaces.push({ |
| repo_id: spaceInfo.id, |
| name: spaceInfo.cardData?.title || spaceInfo.id.split('/')[1], |
| owner: spaceInfo.author, |
| username: username, |
| url: `https://${spaceInfo.author}-${spaceInfo.id.split('/')[1]}.hf.space`, |
| status: spaceRuntime.stage || 'unknown', |
| last_modified: spaceInfo.lastModified || 'unknown', |
| created_at: spaceInfo.createdAt || 'unknown', |
| sdk: spaceInfo.sdk || 'unknown', |
| tags: spaceInfo.tags || [], |
| private: spaceInfo.private || false, |
| app_port: spaceInfo.cardData?.app_port || 'unknown' |
| }); |
| } catch (error) { |
| console.error(`处理 Space ${space.id} 失败:`, error.message); |
| } |
| } |
| } catch (error) { |
| console.error(`获取 Spaces 列表失败 for ${username}:`, error.message); |
| } |
| } |
| allSpaces.sort((a, b) => a.name.localeCompare(b.name)); |
| spaceCache.updateAll(allSpaces); |
| console.log(`定时任务:总共获取到 ${allSpaces.length} 个 Spaces,缓存已更新`); |
| } else { |
| console.log('定时任务:缓存有效且不为空,无需更新'); |
| } |
| } catch (error) { |
| console.error('定时任务:刷新缓存失败:', error.message); |
| } |
| }, REFRESH_INTERVAL); |
| } |
|
|
| app.listen(port, () => { |
| console.log(`Server running on port ${port}`); |
| console.log(`User configurations:`, usernames.map(user => `${user}: ${userTokenMapping[user] ? 'Token Configured' : 'No Token'}`).join(', ') || 'None'); |
| console.log(`Admin login enabled: Username=${ADMIN_USERNAME}, Password=${ADMIN_PASSWORD ? 'Configured' : 'Not Configured'}`); |
| refreshSpacesCachePeriodically(); |
| }); |