/** * Koneko 音源测试平台 - 服务器 v3.0 * 端口: 1255 * 支持 SSE 实时日志推送 * 支持 .qz/.zip/.js 文件上传和自动解压 */ const express = require('express'); const path = require('path'); const fs = require('fs'); const multer = require('multer'); const { testPlugin } = require('./lib/tester'); const { ALL_PLATFORMS, MUSIC_SOURCES } = require('./lib/detector'); const app = express(); const PORT = 1255; // 文件上传配置 const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 50 * 1024 * 1024 } // 50MB }); app.use(express.json({ limit: '50mb' })); app.use(express.urlencoded({ extended: true, limit: '50mb' })); app.use(express.static(path.join(__dirname, 'public'))); /** * 获取所有支持的平台信息 */ app.get('/api/platforms', (req, res) => { res.json({ success: true, platforms: ALL_PLATFORMS, musicSources: MUSIC_SOURCES }); }); /** * 文件上传接口 * 支持 .js / .qz / .zip 文件 */ app.post('/api/upload', upload.array('files', 20), async (req, res) => { try { if (!req.files || req.files.length === 0) { return res.status(400).json({ error: '未收到文件' }); } const plugins = []; for (const file of req.files) { const filename = file.originalname; const ext = path.extname(filename).toLowerCase(); const buffer = file.buffer; if (ext === '.qz' || ext === '.zip') { // 尝试解压 ZIP 文件 const extracted = extractZipFile(buffer, filename); if (extracted.length > 0) { for (const item of extracted) { plugins.push({ code: item.code, filename: item.filename, url: '' }); } } else { // 解压失败,直接用原始内容 plugins.push({ code: buffer.toString('utf-8'), filename: filename, url: '' }); } } else if (ext === '.js') { plugins.push({ code: buffer.toString('utf-8'), filename: filename, url: '' }); } else { // 尝试当作 JS 文件处理 plugins.push({ code: buffer.toString('utf-8'), filename: filename, url: '' }); } } res.json({ success: true, plugins, count: plugins.length }); } catch (err) { console.error('上传处理出错:', err); res.status(500).json({ error: '文件处理失败: ' + err.message }); } }); /** * 从 ZIP/QZ 文件中提取 JS 文件 */ function extractZipFile(buffer, originalName) { const results = []; try { const AdmZip = require('adm-zip'); const zip = new AdmZip(buffer); const entries = zip.getEntries(); for (const entry of entries) { if (entry.isDirectory) continue; const entryName = entry.entryName; if (entryName.endsWith('.js')) { const code = entry.getData().toString('utf-8'); if (code && code.length > 100) { // 过滤太小的文件 const baseName = path.basename(originalName, path.extname(originalName)); const jsName = path.basename(entryName); results.push({ code: code, filename: `${baseName}/${jsName}` }); } } } } catch (err) { console.error('解压失败:', err.message); // 尝试用 unzip 命令行 try { const tmpDir = `/tmp/koneko_${Date.now()}`; const tmpFile = `${tmpDir}.zip`; fs.writeFileSync(tmpFile, buffer); const { execSync } = require('child_process'); fs.mkdirSync(tmpDir, { recursive: true }); execSync(`unzip -o -q "${tmpFile}" -d "${tmpDir}"`, { timeout: 10000 }); const files = findJsFiles(tmpDir); for (const f of files) { const code = fs.readFileSync(f, 'utf-8'); if (code && code.length > 100) { const baseName = path.basename(originalName, path.extname(originalName)); const jsName = path.basename(f); results.push({ code, filename: `${baseName}/${jsName}` }); } } // 清理临时文件 fs.unlinkSync(tmpFile); fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (err2) { console.error('命令行解压也失败:', err2.message); } } return results; } function findJsFiles(dir) { let results = []; const items = fs.readdirSync(dir); for (const item of items) { const fullPath = path.join(dir, item); const stat = fs.statSync(fullPath); if (stat.isDirectory()) { results = results.concat(findJsFiles(fullPath)); } else if (item.endsWith('.js')) { results.push(fullPath); } } return results; } /** * SSE 流式测试接口 * POST /api/test/stream * body: { plugins: [{ url, code, filename }] } * 返回: text/event-stream */ app.post('/api/test/stream', async (req, res) => { try { const { plugins } = req.body; if (!plugins || !Array.isArray(plugins) || plugins.length === 0) { return res.status(400).json({ error: '请提供至少一个插件链接或文件' }); } if (plugins.length > 50) { return res.status(400).json({ error: '单次最多测试 50 个插件' }); } // SSE headers res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', 'X-Accel-Buffering': 'no', 'Access-Control-Allow-Origin': '*' }); const send = (event, data) => { res.write(`event: ${event}\n`); res.write(`data: ${JSON.stringify(data)}\n\n`); }; send('start', { total: plugins.length, time: new Date().toISOString() }); const concurrencyLimit = 3; let completed = 0; for (let i = 0; i < plugins.length; i += concurrencyLimit) { const batch = plugins.slice(i, i + concurrencyLimit); const batchResults = await Promise.all( batch.map(async (p, batchIdx) => { const globalIdx = i + batchIdx; send('plugin_start', { index: globalIdx, url: p.url || '', filename: p.filename || '' }); const logger = (level, message, data) => { send('log', { index: globalIdx, level, message, data: data || null, time: new Date().toISOString() }); }; try { const result = await testPlugin(p.url || '', p.code || null, p.filename || '', logger); send('plugin_done', { index: globalIdx, result }); return result; } catch (err) { const errorResult = { url: p.url || p.filename, filename: p.filename || '', platform: 'unknown', platformLabel: '未知格式', metadata: {}, downloadInfo: { success: false, error: err.message }, latencyTests: [], qualityTests: [], overallStatus: 'failed', summary: { score: 0, details: { error: err.message } }, testedAt: new Date().toISOString() }; send('log', { index: globalIdx, level: 'error', message: `测试异常: ${err.message}`, time: new Date().toISOString() }); send('plugin_done', { index: globalIdx, result: errorResult }); return errorResult; } }) ); completed += batchResults.length; send('progress', { completed, total: plugins.length, percent: Math.round((completed / plugins.length) * 100) }); } send('done', { total: plugins.length, time: new Date().toISOString() }); res.end(); } catch (err) { console.error('测试出错:', err); if (!res.headersSent) { res.status(500).json({ error: '服务器内部错误: ' + err.message }); } else { res.end(); } } }); /** * 兼容旧接口 - 批量测试(非流式) */ app.post('/api/test', async (req, res) => { try { const { plugins } = req.body; if (!plugins || !Array.isArray(plugins) || plugins.length === 0) { return res.status(400).json({ error: '请提供至少一个插件链接或文件' }); } const results = []; const concurrencyLimit = 5; for (let i = 0; i < plugins.length; i += concurrencyLimit) { const batch = plugins.slice(i, i + concurrencyLimit); const batchResults = await Promise.all( batch.map(p => testPlugin(p.url || '', p.code || null, p.filename || '').catch(err => ({ url: p.url || p.filename, filename: p.filename || '', platform: 'unknown', platformLabel: '未知格式', metadata: {}, downloadInfo: { success: false, error: err.message }, latencyTests: [], qualityTests: [], overallStatus: 'failed', summary: { score: 0, details: { error: err.message } }, testedAt: new Date().toISOString() }))) ); results.push(...batchResults); } res.json({ success: true, count: results.length, results, testedAt: new Date().toISOString() }); } catch (err) { res.status(500).json({ error: '服务器内部错误: ' + err.message }); } }); /** * 导出 Markdown 表格 */ app.post('/api/export/md', (req, res) => { try { const { results } = req.body; if (!results || !Array.isArray(results)) return res.status(400).json({ error: '无效的测试结果' }); res.json({ success: true, markdown: generateMarkdownTable(results) }); } catch (err) { res.status(500).json({ error: err.message }); } }); app.get('/api/health', (req, res) => { res.json({ status: 'ok', service: 'Koneko 音源测试平台', version: '3.0.0', time: new Date().toISOString() }); }); function generateMarkdownTable(results) { const lines = []; lines.push('# Koneko 音源测试平台 - 测试报告'); lines.push(''); lines.push(`> 测试时间: ${new Date().toLocaleString('zh-CN')}`); lines.push(`> 测试数量: ${results.length}`); lines.push(''); lines.push('## 测试概览'); lines.push(''); lines.push('| # | 插件名称 | 音乐平台 | 插件格式 | 版本 | 音质 | QZ API | 平均延时(ms) | 成功率 | 评分 | 等级 | 状态 |'); lines.push('|---|---------|---------|---------|------|------|--------|------------|--------|------|------|------|'); results.forEach((r, i) => { const name = r.metadata?.name || r.filename || r.url || '未知'; const musicPlatform = r.metadata?.musicPlatformName || '未知'; const platform = r.platform === 'unknown' ? '未知' : r.platform.toUpperCase(); const version = r.metadata?.version || '-'; const quality = (r.qualityTests?.map(q => q.quality).join(', ')) || '-'; const qzApi = r.summary?.details?.qzApiStatus ? 'OK' : (r.metadata?.qzApiEndpoint?.apiUrl ? '不通' : 'N/A'); const avgLatency = r.summary?.details?.avgLatency ?? '-'; const successRate = r.summary?.details?.successRate ?? '-'; const score = r.summary?.score ?? 0; const grade = r.summary?.details?.grade ?? '-'; const status = r.overallStatus === 'passed' ? '通过' : (r.overallStatus === 'warning' ? '警告' : '失败'); lines.push(`| ${i + 1} | ${name} | ${musicPlatform} | ${platform} | ${version} | ${quality} | ${qzApi} | ${avgLatency} | ${successRate} | ${score} | ${grade} | ${status} |`); }); lines.push(''); lines.push('## 详细信息'); lines.push(''); results.forEach((r, i) => { const name = r.metadata?.name || r.filename || r.url || '未知'; lines.push(`### ${i + 1}. ${name}`); lines.push(''); lines.push(`- **插件链接**: ${r.url || '文件上传'}`); lines.push(`- **音乐平台**: ${r.metadata?.musicPlatformName || '未知'} (${r.metadata?.musicPlatform || 'N/A'})`); lines.push(`- **插件格式**: ${r.platformLabel}`); if (r.metadata?.isObfuscated) lines.push(`- **混淆代码**: 是`); lines.push(`- **版本**: ${r.metadata?.version || '未指定'}`); lines.push(`- **作者**: ${r.metadata?.author || '未知'}`); lines.push(`- **描述**: ${r.metadata?.description || '无'}`); if (r.metadata?.qzApiEndpoint?.apiUrl) { lines.push(`- **QZ API 端点**: ${r.metadata.qzApiEndpoint.apiUrl}`); if (r.metadata.qzApiEndpoint.apiKey) lines.push(`- **API Key**: ${r.metadata.qzApiEndpoint.apiKey.substring(0, 8)}...`); } lines.push(`- **音质支持**: ${r.qualityTests?.map(q => `${q.quality}(${q.label || ''})${q.tested ? `[${q.testStatus === 'pass' ? 'PASS' : 'FAIL'}]` : ''}`).join(', ') || '无'}`); lines.push(`- **音质通过率**: ${r.summary?.details?.qualityPassRate || '0/0'}`); lines.push(`- **功能列表**: ${r.metadata?.features?.join(', ') || '无'}`); lines.push(`- **评分**: ${r.summary?.score || 0}/100 (${r.summary?.details?.grade || 'D'})`); lines.push(''); if (r.latencyTests?.length > 0) { lines.push('#### 延时测试'); lines.push(''); lines.push('| 端点 | 类型 | 状态码 | 延时(ms) | 状态 |'); lines.push('|------|------|--------|---------|------|'); for (const t of r.latencyTests) { const status = t.success ? '成功' : `失败(${t.error || ''})`; const type = t.isQzApi ? (t.quality === 'basic' ? 'QZ API' : `QZ-${t.quality}`) : (t.isPluginUrl ? '插件源' : '直连'); const displayUrl = t.url.length > 60 ? t.url.substring(0, 57) + '...' : t.url; lines.push(`| ${displayUrl} | ${type} | ${t.status} | ${t.latency} | ${status} |`); } lines.push(''); } lines.push('---'); lines.push(''); }); return lines.join('\n'); } app.listen(PORT, '0.0.0.0', () => { console.log(`Koneko 音源测试平台 v3.0 已启动`); console.log(`服务地址: http://0.0.0.0:${PORT}`); console.log(`测试时间: ${new Date().toLocaleString('zh-CN')}`); });