require('dotenv').config(); const express = require('express'); const cors = require('cors'); const https = require('https'); const fs = require('fs'); const path = require('path'); const app = express(); const PORT = process.env.PORT || 3000; app.use(cors()); app.use(express.json()); // 密码验证中间件 function requireAuth(req, res, next) { const password = req.headers['x-admin-password']; const savedPassword = loadAdminPassword(); if (!savedPassword) { // 如果没有设置密码,允许访问(首次设置) next(); } else if (password === savedPassword) { next(); } else { res.status(401).json({ error: '密码错误' }); } } app.use(express.static('public')); // 数据文件路径 const ACCOUNTS_FILE = path.join(__dirname, 'accounts.json'); const PASSWORD_FILE = path.join(__dirname, 'password.json'); // 读取服务器存储的账号 function loadServerAccounts() { try { if (fs.existsSync(ACCOUNTS_FILE)) { const data = fs.readFileSync(ACCOUNTS_FILE, 'utf8'); return JSON.parse(data); } } catch (e) { console.error('❌ 读取账号文件失败:', e.message); } return []; } // 保存账号到服务器 function saveServerAccounts(accounts) { try { fs.writeFileSync(ACCOUNTS_FILE, JSON.stringify(accounts, null, 2), 'utf8'); return true; } catch (e) { console.error('❌ 保存账号文件失败:', e.message); return false; } } // 读取管理员密码 function loadAdminPassword() { try { if (fs.existsSync(PASSWORD_FILE)) { const data = fs.readFileSync(PASSWORD_FILE, 'utf8'); return JSON.parse(data).password; } } catch (e) { console.error('❌ 读取密码文件失败:', e.message); } return null; } // 保存管理员密码 function saveAdminPassword(password) { try { fs.writeFileSync(PASSWORD_FILE, JSON.stringify({ password }, null, 2), 'utf8'); return true; } catch (e) { console.error('❌ 保存密码文件失败:', e.message); return false; } } // Zeabur GraphQL 查询 async function queryZeabur(token, query) { return new Promise((resolve, reject) => { const data = JSON.stringify({ query }); const options = { hostname: 'api.zeabur.com', path: '/graphql', method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', 'Content-Length': data.length }, timeout: 10000 }; const req = https.request(options, (res) => { let body = ''; res.on('data', (chunk) => body += chunk); res.on('end', () => { try { resolve(JSON.parse(body)); } catch (e) { reject(new Error('Invalid JSON response')); } }); }); req.on('error', reject); req.on('timeout', () => { req.destroy(); reject(new Error('Request timeout')); }); req.write(data); req.end(); }); } // 获取用户信息和项目 async function fetchAccountData(token) { // 查询用户信息 const userQuery = ` query { me { _id username email credit } } `; // 查询项目信息 const projectsQuery = ` query { projects { edges { node { _id name region { name } environments { _id } services { _id name status template resourceLimit { cpu memory } domains { domain isGenerated } } } } } } `; // 查询 AI Hub 余额 const aihubQuery = ` query GetAIHubTenant { aihubTenant { balance keys { keyID alias cost } } } `; const [userData, projectsData, aihubData] = await Promise.all([ queryZeabur(token, userQuery), queryZeabur(token, projectsQuery), queryZeabur(token, aihubQuery).catch(() => ({ data: { aihubTenant: null } })) ]); return { user: userData.data?.me || {}, projects: (projectsData.data?.projects?.edges || []).map(edge => edge.node), aihub: aihubData.data?.aihubTenant || null }; } // 获取项目用量数据 async function fetchUsageData(token, userID, projects = []) { const now = new Date(); const year = now.getFullYear(); const month = now.getMonth() + 1; const fromDate = `${year}-${String(month).padStart(2, '0')}-01`; // 使用明天的日期确保包含今天的所有数据 const tomorrow = new Date(now); tomorrow.setDate(tomorrow.getDate() + 1); const toDate = `${tomorrow.getFullYear()}-${String(tomorrow.getMonth() + 1).padStart(2, '0')}-${String(tomorrow.getDate()).padStart(2, '0')}`; const usageQuery = { operationName: 'GetHeaderMonthlyUsage', variables: { from: fromDate, to: toDate, groupByEntity: 'PROJECT', groupByTime: 'DAY', groupByType: 'ALL', userID: userID }, query: `query GetHeaderMonthlyUsage($from: String!, $to: String!, $groupByEntity: GroupByEntity, $groupByTime: GroupByTime, $groupByType: GroupByType, $userID: ObjectID!) { usages( from: $from to: $to groupByEntity: $groupByEntity groupByTime: $groupByTime groupByType: $groupByType userID: $userID ) { categories data { id name groupByEntity usageOfEntity __typename } __typename } }` }; return new Promise((resolve, reject) => { const data = JSON.stringify(usageQuery); const options = { hostname: 'api.zeabur.com', path: '/graphql', method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) }, timeout: 10000 }; const req = https.request(options, (res) => { let body = ''; res.on('data', (chunk) => body += chunk); res.on('end', () => { try { const result = JSON.parse(body); const usages = result.data?.usages?.data || []; // 计算每个项目的总费用 const projectCosts = {}; let totalUsage = 0; usages.forEach(project => { const projectTotal = project.usageOfEntity.reduce((a, b) => a + b, 0); // 单个项目显示:向上取整到 $0.01(与 Zeabur 官方一致) const displayCost = projectTotal > 0 ? Math.ceil(projectTotal * 100) / 100 : 0; projectCosts[project.id] = displayCost; // 总用量计算:使用原始费用(不取整,保证总余额准确) totalUsage += projectTotal; }); resolve({ projectCosts, totalUsage, freeQuotaRemaining: 5 - totalUsage, // 免费额度 $5 freeQuotaLimit: 5 }); } catch (e) { reject(new Error('Invalid JSON response')); } }); }); req.on('error', reject); req.on('timeout', () => { req.destroy(); reject(new Error('Request timeout')); }); req.write(data); req.end(); }); } // 临时账号API - 获取账号信息 app.post('/api/temp-accounts', requireAuth, express.json(), async (req, res) => { const { accounts } = req.body; console.log('📥 收到账号请求:', accounts?.length, '个账号'); if (!accounts || !Array.isArray(accounts)) { return res.status(400).json({ error: '无效的账号列表' }); } const results = await Promise.all(accounts.map(async (account) => { try { console.log(`🔍 正在获取账号 [${account.name}] 的数据...`); const { user, projects, aihub } = await fetchAccountData(account.token); console.log(` API 返回的 credit: ${user.credit}`); // 获取用量数据 let usageData = { totalUsage: 0, freeQuotaRemaining: 5, freeQuotaLimit: 5 }; if (user._id) { try { usageData = await fetchUsageData(account.token, user._id, projects); console.log(`💰 [${account.name}] 用量: $${usageData.totalUsage.toFixed(2)}, 剩余: $${usageData.freeQuotaRemaining.toFixed(2)}`); } catch (e) { console.log(`⚠️ [${account.name}] 获取用量失败:`, e.message); } } // 计算剩余额度并转换为 credit(以分为单位) const creditInCents = Math.round(usageData.freeQuotaRemaining * 100); return { name: account.name, success: true, data: { ...user, credit: creditInCents, // 使用计算的剩余额度 totalUsage: usageData.totalUsage, freeQuotaLimit: usageData.freeQuotaLimit }, aihub: aihub }; } catch (error) { console.error(`❌ [${account.name}] 错误:`, error.message); return { name: account.name, success: false, error: error.message }; } })); console.log('📤 返回结果:', results.length, '个账号'); res.json(results); }); // 临时账号API - 获取项目信息 app.post('/api/temp-projects', requireAuth, express.json(), async (req, res) => { const { accounts } = req.body; console.log('📥 收到项目请求:', accounts?.length, '个账号'); if (!accounts || !Array.isArray(accounts)) { return res.status(400).json({ error: '无效的账号列表' }); } const results = await Promise.all(accounts.map(async (account) => { try { console.log(`🔍 正在获取账号 [${account.name}] 的项目...`); const { user, projects } = await fetchAccountData(account.token); // 获取用量数据 let projectCosts = {}; if (user._id) { try { const usageData = await fetchUsageData(account.token, user._id, projects); projectCosts = usageData.projectCosts; } catch (e) { console.log(`⚠️ [${account.name}] 获取用量失败:`, e.message); } } console.log(`📦 [${account.name}] 找到 ${projects.length} 个项目`); const projectsWithCost = projects.map(project => { const cost = projectCosts[project._id] || 0; console.log(` - ${project.name}: $${cost.toFixed(2)}`); return { _id: project._id, name: project.name, region: project.region?.name || 'Unknown', environments: project.environments || [], services: project.services || [], cost: cost, hasCostData: cost > 0 }; }); return { name: account.name, success: true, projects: projectsWithCost }; } catch (error) { console.error(`❌ [${account.name}] 错误:`, error.message); return { name: account.name, success: false, error: error.message }; } })); console.log('📤 返回项目结果'); res.json(results); }); // 验证账号 app.post('/api/validate-account', requireAuth, express.json(), async (req, res) => { const { accountName, apiToken } = req.body; if (!accountName || !apiToken) { return res.status(400).json({ error: '账号名称和 API Token 不能为空' }); } try { const { user } = await fetchAccountData(apiToken); if (user._id) { res.json({ success: true, message: '账号验证成功!', userData: user, accountName, apiToken }); } else { res.status(400).json({ error: 'API Token 无效或没有权限' }); } } catch (error) { res.status(400).json({ error: 'API Token 验证失败: ' + error.message }); } }); // 从环境变量读取预配置的账号 function getEnvAccounts() { const accountsEnv = process.env.ACCOUNTS; if (!accountsEnv) return []; try { // 格式: "账号1名称:token1,账号2名称:token2" return accountsEnv.split(',').map(item => { const [name, token] = item.split(':'); return { name: name.trim(), token: token.trim() }; }).filter(acc => acc.name && acc.token); } catch (e) { console.error('❌ 解析环境变量 ACCOUNTS 失败:', e.message); return []; } } // 检查是否已设置密码 app.get('/api/check-password', (req, res) => { const savedPassword = loadAdminPassword(); res.json({ hasPassword: !!savedPassword }); }); // 设置管理员密码(首次) app.post('/api/set-password', (req, res) => { const { password } = req.body; const savedPassword = loadAdminPassword(); if (savedPassword) { return res.status(400).json({ error: '密码已设置,无法重复设置' }); } if (!password || password.length < 6) { return res.status(400).json({ error: '密码长度至少6位' }); } if (saveAdminPassword(password)) { console.log('✅ 管理员密码已设置'); res.json({ success: true }); } else { res.status(500).json({ error: '保存密码失败' }); } }); // 验证密码 app.post('/api/verify-password', (req, res) => { const { password } = req.body; const savedPassword = loadAdminPassword(); if (!savedPassword) { return res.status(400).json({ success: false, error: '请先设置密码' }); } if (password === savedPassword) { res.json({ success: true }); } else { res.status(401).json({ success: false, error: '密码错误' }); } }); // 获取所有账号(服务器存储 + 环境变量) app.get('/api/server-accounts', requireAuth, async (req, res) => { const serverAccounts = loadServerAccounts(); const envAccounts = getEnvAccounts(); // 合并账号,环境变量账号优先 const allAccounts = [...envAccounts, ...serverAccounts]; console.log(`📋 返回 ${allAccounts.length} 个账号 (环境变量: ${envAccounts.length}, 服务器: ${serverAccounts.length})`); res.json(allAccounts); }); // 保存账号到服务器 app.post('/api/server-accounts', requireAuth, async (req, res) => { const { accounts } = req.body; if (!accounts || !Array.isArray(accounts)) { return res.status(400).json({ error: '无效的账号列表' }); } if (saveServerAccounts(accounts)) { console.log(`✅ 保存 ${accounts.length} 个账号到服务器`); res.json({ success: true, message: '账号已保存到服务器' }); } else { res.status(500).json({ error: '保存失败' }); } }); // 删除服务器账号 app.delete('/api/server-accounts/:index', requireAuth, async (req, res) => { const index = parseInt(req.params.index); const accounts = loadServerAccounts(); if (index >= 0 && index < accounts.length) { const removed = accounts.splice(index, 1); if (saveServerAccounts(accounts)) { console.log(`🗑️ 删除账号: ${removed[0].name}`); res.json({ success: true, message: '账号已删除' }); } else { res.status(500).json({ error: '删除失败' }); } } else { res.status(404).json({ error: '账号不存在' }); } }); // 服务器配置的账号API(兼容旧版本) app.get('/api/accounts', async (req, res) => { res.json([]); }); app.get('/api/projects', async (req, res) => { res.json([]); }); // 暂停服务 app.post('/api/service/pause', requireAuth, async (req, res) => { const { token, serviceId, environmentId } = req.body; if (!token || !serviceId || !environmentId) { return res.status(400).json({ error: '缺少必要参数' }); } try { const mutation = `mutation { suspendService(serviceID: "${serviceId}", environmentID: "${environmentId}") }`; const result = await queryZeabur(token, mutation); if (result.data?.suspendService) { res.json({ success: true, message: '服务已暂停' }); } else { res.status(400).json({ error: '暂停失败', details: result }); } } catch (error) { res.status(500).json({ error: '暂停服务失败: ' + error.message }); } }); // 重启服务 app.post('/api/service/restart', requireAuth, async (req, res) => { const { token, serviceId, environmentId } = req.body; if (!token || !serviceId || !environmentId) { return res.status(400).json({ error: '缺少必要参数' }); } try { const mutation = `mutation { restartService(serviceID: "${serviceId}", environmentID: "${environmentId}") }`; const result = await queryZeabur(token, mutation); if (result.data?.restartService) { res.json({ success: true, message: '服务已重启' }); } else { res.status(400).json({ error: '重启失败', details: result }); } } catch (error) { res.status(500).json({ error: '重启服务失败: ' + error.message }); } }); // 获取服务日志 app.post('/api/service/logs', requireAuth, express.json(), async (req, res) => { const { token, serviceId, environmentId, projectId, limit = 200 } = req.body; if (!token || !serviceId || !environmentId || !projectId) { return res.status(400).json({ error: '缺少必要参数' }); } try { const query = ` query { runtimeLogs( projectID: "${projectId}" serviceID: "${serviceId}" environmentID: "${environmentId}" ) { message timestamp } } `; const result = await queryZeabur(token, query); if (result.data?.runtimeLogs) { // 按时间戳排序,最新的在最后 const sortedLogs = result.data.runtimeLogs.sort((a, b) => { return new Date(a.timestamp) - new Date(b.timestamp); }); // 获取最后 N 条日志 const logs = sortedLogs.slice(-limit); res.json({ success: true, logs, count: logs.length, totalCount: result.data.runtimeLogs.length }); } else { res.status(400).json({ error: '获取日志失败', details: result }); } } catch (error) { res.status(500).json({ error: '获取日志失败: ' + error.message }); } }); // 重命名项目 app.post('/api/project/rename', requireAuth, async (req, res) => { const { token, projectId, newName } = req.body; console.log(`📝 收到重命名请求: projectId=${projectId}, newName=${newName}`); if (!token || !projectId || !newName) { return res.status(400).json({ error: '缺少必要参数' }); } try { const mutation = `mutation { renameProject(_id: "${projectId}", name: "${newName}") }`; console.log(`🔍 发送 GraphQL mutation:`, mutation); const result = await queryZeabur(token, mutation); console.log(`📥 API 响应:`, JSON.stringify(result, null, 2)); if (result.data?.renameProject) { console.log(`✅ 项目已重命名: ${newName}`); res.json({ success: true, message: '项目已重命名' }); } else { console.log(`❌ 重命名失败:`, result); res.status(400).json({ error: '重命名失败', details: result }); } } catch (error) { console.log(`❌ 异常:`, error); res.status(500).json({ error: '重命名项目失败: ' + error.message }); } }); app.listen(PORT, () => { console.log(`✨ Zeabur Monitor 运行在 http://localhost:${PORT}`); const envAccounts = getEnvAccounts(); const serverAccounts = loadServerAccounts(); const totalAccounts = envAccounts.length + serverAccounts.length; if (totalAccounts > 0) { console.log(`📋 已加载 ${totalAccounts} 个账号`); if (envAccounts.length > 0) { console.log(` 环境变量: ${envAccounts.length} 个`); envAccounts.forEach(acc => console.log(` - ${acc.name}`)); } if (serverAccounts.length > 0) { console.log(` 服务器存储: ${serverAccounts.length} 个`); serverAccounts.forEach(acc => console.log(` - ${acc.name}`)); } } else { console.log(`📊 准备就绪,等待添加账号...`); } });