CoolFace
Apppublic

CaramelAI/dashboard

sourceHugging Faceapache-2.0updated 1y agoView on Hugging Face
0likes
server.js754 linesDownload Raw Back to root
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});