CaramelAI/dashboard
0
1const express = require('express');2const path = require('path');3const axios = require('axios');4const crypto = require('crypto');5const app = express();6const port = process.env.PORT || 8080;7 8// 启用 JSON 和 URL-encoded 请求解析9app.use(express.json());10app.use(express.urlencoded({ extended: true }));11 12// 从环境变量获取 HuggingFace 用户名和对应的 API Token 映射13const userTokenMapping = {};14const usernames = [];15const hfUserConfig = process.env.HF_USER || '';16if (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// 从环境变量获取登录凭据31const ADMIN_USERNAME = process.env.USER_NAME || 'admin';32const ADMIN_PASSWORD = process.env.USER_PASSWORD || 'password';33 34// 从环境变量获取是否在未登录时展示 private 实例的配置,默认值为 false35const SHOW_PRIVATE = process.env.SHOW_PRIVATE === 'true';36console.log(`SHOW_PRIVATE 配置: ${SHOW_PRIVATE ? '未登录时展示 private 实例' : '未登录时隐藏 private 实例'}`);37 38// 存储会话 token 的简单内存数据库(生产环境中应使用数据库或 Redis)39const sessions = new Map();40const SESSION_TIMEOUT = 24 * 60 * 60 * 1000; // 24小时超时41 42// 缓存管理43class SpaceCache {44 constructor() {45 this.spaces = {};46 this.lastUpdate = null;47 }48 49 updateAll(spacesData) {50 this.spaces = spacesData.reduce((acc, space) => ({ ...acc, [space.repo_id]: space }), {});51 this.lastUpdate = Date.now();52 }53 54 getAll() {55 return Object.values(this.spaces);56 }57 58 isExpired(expireMinutes = 5) {59 if (!this.lastUpdate) return true;60 return (Date.now() - this.lastUpdate) > (expireMinutes * 60 * 1000);61 }62 63 invalidate() {64 this.lastUpdate = null;65 }66}67 68const spaceCache = new SpaceCache();69 70// 用于获取 Spaces 数据的函数,带有重试机制71async function fetchSpacesWithRetry(username, token, maxRetries = 3, retryDelay = 2000) {72 let retries = 0;73 while (retries < maxRetries) {74 try {75 // 仅在 token 存在时添加 Authorization 头76 const headers = token ? { 'Authorization': `Bearer ${token}` } : {};77 const response = await axios.get(`https://huggingface.co/api/spaces?author=${username}`, {78 headers,79 timeout: 10000 // 设置 10 秒超时80 });81 const spaces = response.data;82 console.log(`获取到 ${spaces.length} 个 Spaces for ${username} (尝试 ${retries + 1}/${maxRetries}),使用 ${token ? 'Token 认证' : '无认证'}`);83 return spaces;84 } catch (error) {85 retries++;86 let errorDetail = error.message;87 if (error.response) {88 errorDetail += `, HTTP Status: ${error.response.status}`;89 } else if (error.request) {90 errorDetail += ', No response received (possible network issue)';91 }92 console.error(`获取 Spaces 列表失败 for ${username} (尝试 ${retries}/${maxRetries}): ${errorDetail},使用 ${token ? 'Token 认证' : '无认证'}`);93 if (retries < maxRetries) {94 console.log(`等待 ${retryDelay/1000} 秒后重试...`);95 await new Promise(resolve => setTimeout(resolve, retryDelay));96 } else {97 console.error(`达到最大重试次数 (${maxRetries}),放弃重试 for ${username}`);98 return [];99 }100 }101 }102 return [];103}104 105// 提供静态文件(前端文件)106app.use(express.static(path.join(__dirname, 'public')));107 108// 提供配置信息的 API 接口109app.get('/api/config', (req, res) => {110 res.json({ usernames: usernames.join(',') });111});112 113// 登录 API 接口114app.post('/api/login', (req, res) => {115 const { username, password } = req.body;116 if (username === ADMIN_USERNAME && password === ADMIN_PASSWORD) {117 // 生成一个随机 token 作为会话标识118 const token = crypto.randomBytes(16).toString('hex');119 const expiresAt = Date.now() + SESSION_TIMEOUT;120 sessions.set(token, { username, expiresAt });121 console.log(`用户 ${username} 登录成功,生成 token: ${token.slice(0, 8)}...`);122 res.json({ success: true, token });123 } else {124 console.log(`用户 ${username} 登录失败,凭据无效`);125 res.status(401).json({ success: false, message: '用户名或密码错误' });126 }127});128 129// 验证登录状态 API 接口130app.post('/api/verify-token', (req, res) => {131 const { token } = req.body;132 const session = sessions.get(token);133 if (session && session.expiresAt > Date.now()) {134 res.json({ success: true, message: 'Token 有效' });135 } else {136 if (session) {137 sessions.delete(token); // 删除过期的 token138 console.log(`Token ${token.slice(0, 8)}... 已过期,已删除`);139 }140 res.status(401).json({ success: false, message: 'Token 无效或已过期' });141 }142});143 144// 登出 API 接口145app.post('/api/logout', (req, res) => {146 const { token } = req.body;147 sessions.delete(token);148 console.log(`Token ${token.slice(0, 8)}... 已手动登出`);149 res.json({ success: true, message: '登出成功' });150});151 152// 中间件:验证请求中的 token153const authenticateToken = (req, res, next) => {154 const authHeader = req.headers['authorization'];155 if (!authHeader || !authHeader.startsWith('Bearer ')) {156 return res.status(401).json({ error: '未提供有效的认证令牌' });157 }158 const token = authHeader.split(' ')[1];159 const session = sessions.get(token);160 if (session && session.expiresAt > Date.now()) {161 req.session = session;162 next();163 } else {164 if (session) {165 sessions.delete(token); // 删除过期的 token166 console.log(`Token ${token.slice(0, 8)}... 已过期,拒绝访问`);167 }168 return res.status(401).json({ error: '认证令牌无效或已过期' });169 }170};171 172// 获取所有 spaces 列表(包括私有)173app.get('/api/proxy/spaces', async (req, res) => {174 try {175 // 检查是否登录176 let isAuthenticated = false;177 const authHeader = req.headers['authorization'];178 if (authHeader && authHeader.startsWith('Bearer ')) {179 const token = authHeader.split(' ')[1];180 const session = sessions.get(token);181 if (session && session.expiresAt > Date.now()) {182 isAuthenticated = true;183 console.log(`用户已登录,Token: ${token.slice(0, 8)}...`);184 } else {185 if (session) {186 sessions.delete(token); // 删除过期的 token187 console.log(`Token ${token.slice(0, 8)}... 已过期,拒绝访问`);188 }189 console.log('用户认证失败,无有效 Token');190 }191 } else {192 console.log('用户未提供认证令牌');193 }194 195 // 如果缓存为空或已过期,强制重新获取数据196 const cachedSpaces = spaceCache.getAll();197 if (cachedSpaces.length === 0 || spaceCache.isExpired()) {198 console.log(cachedSpaces.length === 0 ? '缓存为空,强制重新获取数据' : '缓存已过期,重新获取数据');199 const allSpaces = [];200 for (const username of usernames) {201 const token = userTokenMapping[username];202 if (!token) {203 console.warn(`用户 ${username} 没有配置 API Token,将尝试无认证访问公开数据`);204 }205 206 try {207 const spaces = await fetchSpacesWithRetry(username, token);208 for (const space of spaces) {209 try {210 // 仅在 token 存在时添加 Authorization 头211 const headers = token ? { 'Authorization': `Bearer ${token}` } : {};212 const spaceInfoResponse = await axios.get(`https://huggingface.co/api/spaces/${space.id}`, { headers });213 const spaceInfo = spaceInfoResponse.data;214 const spaceRuntime = spaceInfo.runtime || {};215 216 allSpaces.push({217 repo_id: spaceInfo.id,218 name: spaceInfo.cardData?.title || spaceInfo.id.split('/')[1],219 owner: spaceInfo.author,220 username: username,221 url: `https://${spaceInfo.author}-${spaceInfo.id.split('/')[1]}.hf.space`,222 status: spaceRuntime.stage || 'unknown',223 last_modified: spaceInfo.lastModified || 'unknown',224 created_at: spaceInfo.createdAt || 'unknown',225 sdk: spaceInfo.sdk || 'unknown',226 tags: spaceInfo.tags || [],227 private: spaceInfo.private || false,228 app_port: spaceInfo.cardData?.app_port || 'unknown',229 short_description: spaceInfo.cardData?.short_description || '' // 新增字段,确保为空时返回空字符串230 });231 } catch (error) {232 console.error(`处理 Space ${space.id} 失败:`, error.message, `使用 ${token ? 'Token 认证' : '无认证'}`);233 }234 }235 } catch (error) {236 console.error(`获取 Spaces 列表失败 for ${username}:`, error.message, `使用 ${token ? 'Token 认证' : '无认证'}`);237 }238 }239 240 allSpaces.sort((a, b) => a.name.localeCompare(b.name));241 spaceCache.updateAll(allSpaces);242 console.log(`总共获取到 ${allSpaces.length} 个 Spaces`);243 244 const safeSpaces = allSpaces.map(space => {245 const { token, ...safeSpace } = space;246 return safeSpace;247 });248 249 if (isAuthenticated) {250 console.log('用户已登录,返回所有实例(包括 private)');251 res.json(safeSpaces);252 } else if (SHOW_PRIVATE) {253 console.log('用户未登录,但 SHOW_PRIVATE 为 true,返回所有实例');254 res.json(safeSpaces);255 } else {256 console.log('用户未登录,SHOW_PRIVATE 为 false,过滤 private 实例');257 res.json(safeSpaces.filter(space => !space.private));258 }259 } else {260 console.log('从缓存获取 Spaces 数据');261 const safeSpaces = cachedSpaces.map(space => {262 const { token, ...safeSpace } = space;263 return safeSpace;264 });265 266 if (isAuthenticated) {267 console.log('用户已登录,返回所有缓存实例(包括 private)');268 return res.json(safeSpaces);269 } else if (SHOW_PRIVATE) {270 console.log('用户未登录,但 SHOW_PRIVATE 为 true,返回所有缓存实例');271 return res.json(safeSpaces);272 } else {273 console.log('用户未登录,SHOW_PRIVATE 为 false,过滤 private 实例');274 return res.json(safeSpaces.filter(space => !space.private));275 }276 }277 } catch (error) {278 console.error(`代理获取 spaces 列表失败:`, error.message);279 res.status(500).json({ error: '获取 spaces 列表失败', details: error.message });280 }281});282 283// 代理重启 Space(需要认证)284app.post('/api/proxy/restart/:repoId(*)', authenticateToken, async (req, res) => {285 try {286 const { repoId } = req.params;287 console.log(`尝试重启 Space: ${repoId}`);288 const spaces = spaceCache.getAll();289 const space = spaces.find(s => s.repo_id === repoId);290 if (!space || !userTokenMapping[space.username]) {291 console.error(`Space ${repoId} 未找到或无 Token 配置`);292 return res.status(404).json({ error: 'Space 未找到或无 Token 配置' });293 }294 295 const headers = { 'Authorization': `Bearer ${userTokenMapping[space.username]}`, 'Content-Type': 'application/json' };296 const response = await axios.post(`https://huggingface.co/api/spaces/${repoId}/restart`, {}, { headers });297 console.log(`重启 Space ${repoId} 成功,状态码: ${response.status}`);298 res.json({ success: true, message: `Space ${repoId} 重启成功` });299 } catch (error) {300 console.error(`重启 space 失败 (${req.params.repoId}):`, error.message);301 if (error.response) {302 console.error(`状态码: ${error.response.status}, 响应数据:`, error.response.data);303 res.status(error.response.status || 500).json({ error: '重启 space 失败', details: error.response.data?.message || error.message });304 } else {305 res.status(500).json({ error: '重启 space 失败', details: error.message });306 }307 }308});309 310// 代理重建 Space(需要认证)311app.post('/api/proxy/rebuild/:repoId(*)', authenticateToken, async (req, res) => {312 try {313 const { repoId } = req.params;314 console.log(`尝试重建 Space: ${repoId}`);315 const spaces = spaceCache.getAll();316 const space = spaces.find(s => s.repo_id === repoId);317 if (!space || !userTokenMapping[space.username]) {318 console.error(`Space ${repoId} 未找到或无 Token 配置`);319 return res.status(404).json({ error: 'Space 未找到或无 Token 配置' });320 }321 322 const headers = { 'Authorization': `Bearer ${userTokenMapping[space.username]}`, 'Content-Type': 'application/json' };323 // 将 factory_reboot 参数作为查询参数传递,而非请求体324 const response = await axios.post(325 `https://huggingface.co/api/spaces/${repoId}/restart?factory=true`,326 {},327 { headers }328 );329 console.log(`重建 Space ${repoId} 成功,状态码: ${response.status}`);330 res.json({ success: true, message: `Space ${repoId} 重建成功` });331 } catch (error) {332 console.error(`重建 space 失败 (${req.params.repoId}):`, error.message);333 if (error.response) {334 console.error(`状态码: ${error.response.status}, 响应数据:`, error.response.data);335 res.status(error.response.status || 500).json({ error: '重建 space 失败', details: error.response.data?.message || error.message });336 } else {337 res.status(500).json({ error: '重建 space 失败', details: error.message });338 }339 }340});341 342// 外部 API 服务(类似于 Flask 的 /api/v1)343app.get('/api/v1/info/:token', async (req, res) => {344 try {345 const { token } = req.params;346 const authHeader = req.headers.authorization;347 if (!authHeader || !authHeader.startsWith('Bearer ') || authHeader.split(' ')[1] !== process.env.API_KEY) {348 return res.status(401).json({ error: '无效的 API 密钥' });349 }350 351 const headers = { 'Authorization': `Bearer ${token}` };352 const userInfoResponse = await axios.get('https://huggingface.co/api/whoami-v2', { headers });353 const username = userInfoResponse.data.name;354 const spacesResponse = await axios.get(`https://huggingface.co/api/spaces?author=${username}`, { headers });355 const spaces = spacesResponse.data;356 const spaceList = [];357 358 for (const space of spaces) {359 try {360 const spaceInfoResponse = await axios.get(`https://huggingface.co/api/spaces/${space.id}`, { headers });361 spaceList.push(spaceInfoResponse.data.id);362 } catch (error) {363 console.error(`获取 Space 信息失败 (${space.id}):`, error.message);364 }365 }366 367 res.json({ spaces: spaceList, total: spaceList.length });368 } catch (error) {369 console.error(`获取 spaces 列表失败 (外部 API):`, error.message);370 res.status(500).json({ error: error.message });371 }372});373 374app.get('/api/v1/info/:token/:spaceId(*)', async (req, res) => {375 try {376 const { token, spaceId } = req.params;377 const authHeader = req.headers.authorization;378 if (!authHeader || !authHeader.startsWith('Bearer ') || authHeader.split(' ')[1] !== process.env.API_KEY) {379 return res.status(401).json({ error: '无效的 API 密钥' });380 }381 382 const headers = { 'Authorization': `Bearer ${token}` };383 const spaceInfoResponse = await axios.get(`https://huggingface.co/api/spaces/${spaceId}`, { headers });384 const spaceInfo = spaceInfoResponse.data;385 const spaceRuntime = spaceInfo.runtime || {};386 387 res.json({388 id: spaceInfo.id,389 status: spaceRuntime.stage || 'unknown',390 last_modified: spaceInfo.lastModified || null,391 created_at: spaceInfo.createdAt || null,392 sdk: spaceInfo.sdk || 'unknown',393 tags: spaceInfo.tags || [],394 private: spaceInfo.private || false395 });396 } catch (error) {397 console.error(`获取 space 信息失败 (外部 API):`, error.message);398 res.status(error.response?.status || 404).json({ error: error.message });399 }400});401 402app.post('/api/v1/action/:token/:spaceId(*)/restart', async (req, res) => {403 try {404 const { token, spaceId } = req.params;405 const authHeader = req.headers.authorization;406 if (!authHeader || !authHeader.startsWith('Bearer ') || authHeader.split(' ')[1] !== process.env.API_KEY) {407 return res.status(401).json({ error: '无效的 API 密钥' });408 }409 410 const headers = { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' };411 await axios.post(`https://huggingface.co/api/spaces/${spaceId}/restart`, {}, { headers });412 res.json({ success: true, message: `Space ${spaceId} 重启成功` });413 } catch (error) {414 console.error(`重启 space 失败 (外部 API):`, error.message);415 res.status(error.response?.status || 500).json({ success: false, error: error.message });416 }417});418 419app.post('/api/v1/action/:token/:spaceId(*)/rebuild', async (req, res) => {420 try {421 const { token, spaceId } = req.params;422 const authHeader = req.headers.authorization;423 if (!authHeader || !authHeader.startsWith('Bearer ') || authHeader.split(' ')[1] !== process.env.API_KEY) {424 return res.status(401).json({ error: '无效的 API 密钥' });425 }426 427 const headers = { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' };428 console.log(`外部 API 发送重建请求,spaceId: ${spaceId}`);429 // 将 factory_reboot 参数作为查询参数传递,而非请求体430 const response = await axios.post(431 `https://huggingface.co/api/spaces/${spaceId}/restart?factory=true`,432 {},433 { headers }434 );435 console.log(`外部 API 重建 Space ${spaceId} 成功,状态码: ${response.status}`);436 res.json({ success: true, message: `Space ${spaceId} 重建成功` });437 } catch (error) {438 console.error(`重建 space 失败 (外部 API):`, error.message);439 if (error.response) {440 console.error(`状态码: ${error.response.status}, 响应数据:`, error.response.data);441 res.status(error.response.status || 500).json({ success: false, error: error.response.data?.message || error.message });442 } else {443 res.status(500).json({ success: false, error: error.message });444 }445 }446});447 448// 监控数据管理类449class MetricsConnectionManager {450 constructor() {451 this.connections = new Map(); // 存储 HuggingFace API 的监控连接452 this.clients = new Map(); // 存储前端客户端的 SSE 连接453 this.instanceData = new Map(); // 存储每个实例的最新监控数据454 }455 456 // 建立到 HuggingFace API 的监控连接457 async connectToInstance(repoId, username, token) {458 if (this.connections.has(repoId)) {459 return this.connections.get(repoId);460 }461 462 const instanceId = repoId.split('/')[1];463 const url = `https://api.hf.space/v1/${username}/${instanceId}/live-metrics/sse`;464 // 仅在 token 存在且非空时添加 Authorization 头465 const headers = token ? {466 'Authorization': `Bearer ${token}`,467 'Accept': 'text/event-stream',468 'Cache-Control': 'no-cache',469 'Connection': 'keep-alive'470 } : {471 'Accept': 'text/event-stream',472 'Cache-Control': 'no-cache',473 'Connection': 'keep-alive'474 };475 476 try {477 const response = await axios({478 method: 'get',479 url,480 headers,481 responseType: 'stream',482 timeout: 10000483 });484 485 const stream = response.data;486 stream.on('data', (chunk) => {487 const chunkStr = chunk.toString();488 if (chunkStr.includes('event: metric')) {489 const dataMatch = chunkStr.match(/data: (.*)/);490 if (dataMatch && dataMatch[1]) {491 try {492 const metrics = JSON.parse(dataMatch[1]);493 this.instanceData.set(repoId, metrics);494 // 推送给所有订阅了该实例的客户端495 this.clients.forEach((clientRes, clientId) => {496 if (clientRes.subscribedInstances && clientRes.subscribedInstances.includes(repoId)) {497 clientRes.write(`event: metric\n`);498 clientRes.write(`data: ${JSON.stringify({ repoId, metrics })}\n\n`);499 }500 });501 } catch (error) {502 console.error(`解析监控数据失败 (${repoId}):`, error.message);503 }504 }505 }506 });507 508 stream.on('error', (error) => {509 console.error(`监控连接错误 (${repoId}):`, error.message);510 this.connections.delete(repoId);511 this.instanceData.delete(repoId);512 });513 514 stream.on('end', () => {515 console.log(`监控连接结束 (${repoId})`);516 this.connections.delete(repoId);517 this.instanceData.delete(repoId);518 });519 520 this.connections.set(repoId, stream);521 console.log(`已建立监控连接 (${repoId}),使用 ${token ? 'Token 认证' : '无认证'}`);522 return stream;523 } catch (error) {524 console.error(`无法连接到监控端点 (${repoId}):`, error.message);525 this.connections.delete(repoId);526 return null;527 }528 }529 530 // 注册前端客户端的 SSE 连接531 registerClient(clientId, res, subscribedInstances) {532 res.subscribedInstances = subscribedInstances || [];533 this.clients.set(clientId, res);534 console.log(`客户端 ${clientId} 注册,订阅实例: ${res.subscribedInstances.join(', ') || '无'}`);535 536 // 首次连接时,推送已缓存的最新数据537 res.subscribedInstances.forEach(repoId => {538 if (this.instanceData.has(repoId)) {539 const metrics = this.instanceData.get(repoId);540 res.write(`event: metric\n`);541 res.write(`data: ${JSON.stringify({ repoId, metrics })}\n\n`);542 }543 });544 }545 546 // 客户端断开连接547 unregisterClient(clientId) {548 this.clients.delete(clientId);549 console.log(`客户端 ${clientId} 断开连接`);550 this.cleanupConnections();551 }552 553 // 更新客户端订阅的实例列表554 updateClientSubscriptions(clientId, subscribedInstances) {555 const clientRes = this.clients.get(clientId);556 if (clientRes) {557 clientRes.subscribedInstances = subscribedInstances || [];558 console.log(`客户端 ${clientId} 更新订阅: ${clientRes.subscribedInstances.join(', ') || '无'}`);559 // 更新后推送最新的缓存数据560 subscribedInstances.forEach(repoId => {561 if (this.instanceData.has(repoId)) {562 const metrics = this.instanceData.get(repoId);563 clientRes.write(`event: metric\n`);564 clientRes.write(`data: ${JSON.stringify({ repoId, metrics })}\n\n`);565 }566 });567 }568 this.cleanupConnections();569 }570 571 // 清理未被任何客户端订阅的连接572 cleanupConnections() {573 const subscribedRepoIds = new Set();574 this.clients.forEach(clientRes => {575 clientRes.subscribedInstances.forEach(repoId => subscribedRepoIds.add(repoId));576 });577 578 const toRemove = [];579 this.connections.forEach((stream, repoId) => {580 if (!subscribedRepoIds.has(repoId)) {581 toRemove.push(repoId);582 stream.destroy();583 console.log(`清理未订阅的监控连接 (${repoId})`);584 }585 });586 587 toRemove.forEach(repoId => {588 this.connections.delete(repoId);589 this.instanceData.delete(repoId);590 });591 }592}593 594const metricsManager = new MetricsConnectionManager();595 596// 新增统一监控数据的SSE端点597app.get('/api/proxy/live-metrics-stream', (req, res) => {598 // 设置 SSE 所需的响应头599 res.set({600 'Content-Type': 'text/event-stream',601 'Cache-Control': 'no-cache',602 'Connection': 'keep-alive'603 });604 605 // 生成唯一的客户端ID606 const clientId = crypto.randomBytes(8).toString('hex');607 608 // 获取查询参数中的实例列表和 token609 const instancesParam = req.query.instances || '';610 const token = req.query.token || '';611 const subscribedInstances = instancesParam.split(',').filter(id => id.trim() !== '');612 613 // 检查登录状态614 let isAuthenticated = false;615 if (token) {616 const session = sessions.get(token);617 if (session && session.expiresAt > Date.now()) {618 isAuthenticated = true;619 console.log(`SSE 用户已登录,Token: ${token.slice(0, 8)}...`);620 } else {621 if (session) {622 sessions.delete(token);623 console.log(`SSE Token ${token.slice(0, 8)}... 已过期,拒绝访问`);624 }625 console.log('SSE 用户认证失败,无有效 Token');626 }627 } else {628 console.log('SSE 用户未提供认证令牌');629 }630 631 // 注册客户端632 metricsManager.registerClient(clientId, res, subscribedInstances);633 634 // 根据订阅列表建立监控连接635 const spaces = spaceCache.getAll();636 subscribedInstances.forEach(repoId => {637 const space = spaces.find(s => s.repo_id === repoId);638 if (space) {639 const username = space.username;640 const token = userTokenMapping[username] || '';641 metricsManager.connectToInstance(repoId, username, token);642 }643 });644 645 // 监听客户端断开连接646 req.on('close', () => {647 metricsManager.unregisterClient(clientId);648 console.log(`客户端 ${clientId} 断开 SSE 连接`);649 });650});651 652// 新增接口:更新客户端订阅的实例列表653app.post('/api/proxy/update-subscriptions', (req, res) => {654 const { clientId, instances } = req.body;655 if (!clientId || !instances || !Array.isArray(instances)) {656 return res.status(400).json({ error: '缺少 clientId 或 instances 参数' });657 }658 659 metricsManager.updateClientSubscriptions(clientId, instances);660 // 根据新订阅列表建立监控连接661 const spaces = spaceCache.getAll();662 instances.forEach(repoId => {663 const space = spaces.find(s => s.repo_id === repoId);664 if (space) {665 const username = space.username;666 const token = userTokenMapping[username] || '';667 metricsManager.connectToInstance(repoId, username, token);668 }669 });670 671 res.json({ success: true, message: '订阅列表已更新' });672});673 674// 处理其他请求,重定向到 index.html675app.get('*', (req, res) => {676 res.sendFile(path.join(__dirname, 'public', 'index.html'));677});678 679// 定期清理过期的会话680setInterval(() => {681 const now = Date.now();682 for (const [token, session] of sessions.entries()) {683 if (session.expiresAt < now) {684 sessions.delete(token);685 console.log(`Token ${token.slice(0, 8)}... 已过期,自动清理`);686 }687 }688}, 60 * 60 * 1000); // 每小时清理一次689 690// 定时刷新缓存任务691const REFRESH_INTERVAL = 5 * 60 * 1000; // 每 5 分钟检查一次692async function refreshSpacesCachePeriodically() {693 console.log('启动定时刷新缓存任务...');694 setInterval(async () => {695 try {696 const cachedSpaces = spaceCache.getAll();697 if (spaceCache.isExpired() || cachedSpaces.length === 0) {698 console.log('定时任务:缓存已过期或为空,重新获取 Spaces 数据');699 const allSpaces = [];700 for (const username of usernames) {701 const token = userTokenMapping[username];702 if (!token) {703 console.warn(`用户 ${username} 没有配置 API Token,将尝试无认证访问公开数据`);704 }705 try {706 const spaces = await fetchSpacesWithRetry(username, token);707 for (const space of spaces) {708 try {709 const headers = token ? { 'Authorization': `Bearer ${token}` } : {};710 const spaceInfoResponse = await axios.get(`https://huggingface.co/api/spaces/${space.id}`, { headers });711 const spaceInfo = spaceInfoResponse.data;712 const spaceRuntime = spaceInfo.runtime || {};713 714 allSpaces.push({715 repo_id: spaceInfo.id,716 name: spaceInfo.cardData?.title || spaceInfo.id.split('/')[1],717 owner: spaceInfo.author,718 username: username,719 url: `https://${spaceInfo.author}-${spaceInfo.id.split('/')[1]}.hf.space`,720 status: spaceRuntime.stage || 'unknown',721 last_modified: spaceInfo.lastModified || 'unknown',722 created_at: spaceInfo.createdAt || 'unknown',723 sdk: spaceInfo.sdk || 'unknown',724 tags: spaceInfo.tags || [],725 private: spaceInfo.private || false,726 app_port: spaceInfo.cardData?.app_port || 'unknown',727 short_description: spaceInfo.cardData?.short_description || '' // 新增字段,确保为空时返回空字符串728 });729 } catch (error) {730 console.error(`处理 Space ${space.id} 失败:`, error.message);731 }732 }733 } catch (error) {734 console.error(`获取 Spaces 列表失败 for ${username}:`, error.message);735 }736 }737 allSpaces.sort((a, b) => a.name.localeCompare(b.name));738 spaceCache.updateAll(allSpaces);739 console.log(`定时任务:总共获取到 ${allSpaces.length} 个 Spaces,缓存已更新`);740 } else {741 console.log('定时任务:缓存有效且不为空,无需更新');742 }743 } catch (error) {744 console.error('定时任务:刷新缓存失败:', error.message);745 }746 }, REFRESH_INTERVAL);747}748 749app.listen(port, () => {750 console.log(`Server running on port ${port}`);751 console.log(`User configurations:`, usernames.map(user => `${user}: ${userTokenMapping[user] ? 'Token Configured' : 'No Token'}`).join(', ') || 'None');752 console.log(`Admin login enabled: Username=${ADMIN_USERNAME}, Password=${ADMIN_PASSWORD ? 'Configured' : 'Not Configured'}`);753 refreshSpacesCachePeriodically(); // 启动定时任务754});