Files
koneko-music-tester/server.js
Koneko Tester 99d813cedf v1.1: 实时日志系统 + SSE流式输出 + 上传进度 + 平台展示
- 新增 SSE 流式接口 /api/test/stream,测试过程实时推送日志
- 前端日志面板:终端风格,每步骤实时输出(info/success/warn/error)
- 文件上传进度条:逐文件读取显示进度百分比
- 平台展示卡片:点击测试时显示所有支持的平台信息
- 新增 /api/platforms 接口返回三大平台完整信息
- 增量渲染:每个插件测试完成后立即显示结果卡片
- 日志支持清空操作
- 六级音质:128k/192k/320k/FLAC/Hi-Res/Master
2026-08-04 14:41:21 +08:00

220 lines
8.3 KiB
JavaScript

/**
* Koneko 音源测试平台 - 服务器 v1.1
* 端口: 1255
* 支持 SSE 实时日志推送
*/
const express = require('express');
const path = require('path');
const { testPlugin } = require('./lib/tester');
const { ALL_PLATFORMS } = require('./lib/detector');
const app = express();
const PORT = 1255;
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true }));
app.use(express.static(path.join(__dirname, 'public')));
/**
* 获取所有支持的平台信息
*/
app.get('/api/platforms', (req, res) => {
res.json({ success: true, platforms: ALL_PLATFORMS });
});
/**
* 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 > 20) {
return res.status(400).json({ error: '单次最多测试 20 个插件' });
}
// 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, // info, success, warn, error
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: '1.1.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('| # | 插件名称 | 平台 | 版本 | 音质 | 延时(ms) | 评分 | 等级 | 状态 |');
lines.push('|---|---------|------|------|------|---------|------|------|------|');
results.forEach((r, i) => {
const name = r.metadata?.name || r.filename || r.url || '未知';
const platform = r.platform === 'unknown' ? '未知' : r.platform.toUpperCase();
const version = r.metadata?.version || '-';
const quality = (r.qualityTests?.map(q => q.quality).join(', ')) || '-';
const avgLatency = r.summary?.details?.avgLatency ?? '-';
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} | ${platform} | ${version} | ${quality} | ${avgLatency} | ${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.platformLabel}`);
lines.push(`- **版本**: ${r.metadata?.version || '未指定'}`);
lines.push(`- **作者**: ${r.metadata?.author || '未知'}`);
lines.push(`- **音质支持**: ${r.qualityTests?.map(q => `${q.quality}${q.declared ? '' : '(默认)'}`).join(', ') || '无'}`);
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 displayUrl = t.url.length > 60 ? t.url.substring(0, 57) + '...' : t.url;
lines.push(`| ${displayUrl} | ${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 音源测试平台 v1.1 已启动`);
console.log(`服务地址: http://0.0.0.0:${PORT}`);
console.log(`测试时间: ${new Date().toLocaleString('zh-CN')}`);
});