2026-08-04 14:41:21 +08:00
|
|
|
|
/**
|
2026-08-04 16:19:35 +08:00
|
|
|
|
* Koneko 音源测试平台 - 服务器 v3.0
|
2026-08-04 14:41:21 +08:00
|
|
|
|
* 端口: 1255
|
|
|
|
|
|
* 支持 SSE 实时日志推送
|
2026-08-04 16:19:35 +08:00
|
|
|
|
* 支持 .qz/.zip/.js 文件上传和自动解压
|
2026-08-04 14:41:21 +08:00
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
const express = require('express');
|
|
|
|
|
|
const path = require('path');
|
2026-08-04 16:19:35 +08:00
|
|
|
|
const fs = require('fs');
|
|
|
|
|
|
const multer = require('multer');
|
2026-08-04 14:41:21 +08:00
|
|
|
|
const { testPlugin } = require('./lib/tester');
|
2026-08-04 16:19:35 +08:00
|
|
|
|
const { ALL_PLATFORMS, MUSIC_SOURCES } = require('./lib/detector');
|
2026-08-04 14:41:21 +08:00
|
|
|
|
|
|
|
|
|
|
const app = express();
|
|
|
|
|
|
const PORT = 1255;
|
|
|
|
|
|
|
2026-08-04 16:19:35 +08:00
|
|
|
|
// 文件上传配置
|
|
|
|
|
|
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' }));
|
2026-08-04 14:41:21 +08:00
|
|
|
|
app.use(express.static(path.join(__dirname, 'public')));
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取所有支持的平台信息
|
|
|
|
|
|
*/
|
|
|
|
|
|
app.get('/api/platforms', (req, res) => {
|
2026-08-04 16:19:35 +08:00
|
|
|
|
res.json({ success: true, platforms: ALL_PLATFORMS, musicSources: MUSIC_SOURCES });
|
2026-08-04 14:41:21 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-08-04 16:19:35 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 文件上传接口
|
|
|
|
|
|
* 支持 .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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-04 14:41:21 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 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: '请提供至少一个插件链接或文件' });
|
|
|
|
|
|
}
|
2026-08-04 16:19:35 +08:00
|
|
|
|
if (plugins.length > 50) {
|
|
|
|
|
|
return res.status(400).json({ error: '单次最多测试 50 个插件' });
|
2026-08-04 14:41:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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,
|
2026-08-04 16:19:35 +08:00
|
|
|
|
level,
|
2026-08-04 14:41:21 +08:00
|
|
|
|
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) => {
|
2026-08-04 16:19:35 +08:00
|
|
|
|
res.json({ status: 'ok', service: 'Koneko 音源测试平台', version: '3.0.0', time: new Date().toISOString() });
|
2026-08-04 14:41:21 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
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('');
|
2026-08-04 16:19:35 +08:00
|
|
|
|
lines.push('| # | 插件名称 | 音乐平台 | 插件格式 | 版本 | 音质 | QZ API | 平均延时(ms) | 成功率 | 评分 | 等级 | 状态 |');
|
|
|
|
|
|
lines.push('|---|---------|---------|---------|------|------|--------|------------|--------|------|------|------|');
|
2026-08-04 14:41:21 +08:00
|
|
|
|
results.forEach((r, i) => {
|
|
|
|
|
|
const name = r.metadata?.name || r.filename || r.url || '未知';
|
2026-08-04 16:19:35 +08:00
|
|
|
|
const musicPlatform = r.metadata?.musicPlatformName || '未知';
|
2026-08-04 14:41:21 +08:00
|
|
|
|
const platform = r.platform === 'unknown' ? '未知' : r.platform.toUpperCase();
|
|
|
|
|
|
const version = r.metadata?.version || '-';
|
|
|
|
|
|
const quality = (r.qualityTests?.map(q => q.quality).join(', ')) || '-';
|
2026-08-04 16:19:35 +08:00
|
|
|
|
const qzApi = r.summary?.details?.qzApiStatus ? 'OK' : (r.metadata?.qzApiEndpoint?.apiUrl ? '不通' : 'N/A');
|
2026-08-04 14:41:21 +08:00
|
|
|
|
const avgLatency = r.summary?.details?.avgLatency ?? '-';
|
2026-08-04 16:19:35 +08:00
|
|
|
|
const successRate = r.summary?.details?.successRate ?? '-';
|
2026-08-04 14:41:21 +08:00
|
|
|
|
const score = r.summary?.score ?? 0;
|
|
|
|
|
|
const grade = r.summary?.details?.grade ?? '-';
|
|
|
|
|
|
const status = r.overallStatus === 'passed' ? '通过' : (r.overallStatus === 'warning' ? '警告' : '失败');
|
2026-08-04 16:19:35 +08:00
|
|
|
|
lines.push(`| ${i + 1} | ${name} | ${musicPlatform} | ${platform} | ${version} | ${quality} | ${qzApi} | ${avgLatency} | ${successRate} | ${score} | ${grade} | ${status} |`);
|
2026-08-04 14:41:21 +08:00
|
|
|
|
});
|
|
|
|
|
|
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 || '文件上传'}`);
|
2026-08-04 16:19:35 +08:00
|
|
|
|
lines.push(`- **音乐平台**: ${r.metadata?.musicPlatformName || '未知'} (${r.metadata?.musicPlatform || 'N/A'})`);
|
|
|
|
|
|
lines.push(`- **插件格式**: ${r.platformLabel}`);
|
|
|
|
|
|
if (r.metadata?.isObfuscated) lines.push(`- **混淆代码**: 是`);
|
2026-08-04 14:41:21 +08:00
|
|
|
|
lines.push(`- **版本**: ${r.metadata?.version || '未指定'}`);
|
|
|
|
|
|
lines.push(`- **作者**: ${r.metadata?.author || '未知'}`);
|
2026-08-04 16:19:35 +08:00
|
|
|
|
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'}`);
|
2026-08-04 14:41:21 +08:00
|
|
|
|
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('');
|
2026-08-04 16:19:35 +08:00
|
|
|
|
lines.push('| 端点 | 类型 | 状态码 | 延时(ms) | 状态 |');
|
|
|
|
|
|
lines.push('|------|------|--------|---------|------|');
|
2026-08-04 14:41:21 +08:00
|
|
|
|
for (const t of r.latencyTests) {
|
|
|
|
|
|
const status = t.success ? '成功' : `失败(${t.error || ''})`;
|
2026-08-04 16:19:35 +08:00
|
|
|
|
const type = t.isQzApi ? (t.quality === 'basic' ? 'QZ API' : `QZ-${t.quality}`) : (t.isPluginUrl ? '插件源' : '直连');
|
2026-08-04 14:41:21 +08:00
|
|
|
|
const displayUrl = t.url.length > 60 ? t.url.substring(0, 57) + '...' : t.url;
|
2026-08-04 16:19:35 +08:00
|
|
|
|
lines.push(`| ${displayUrl} | ${type} | ${t.status} | ${t.latency} | ${status} |`);
|
2026-08-04 14:41:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
lines.push('');
|
|
|
|
|
|
}
|
|
|
|
|
|
lines.push('---');
|
|
|
|
|
|
lines.push('');
|
|
|
|
|
|
});
|
|
|
|
|
|
return lines.join('\n');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
app.listen(PORT, '0.0.0.0', () => {
|
2026-08-04 16:19:35 +08:00
|
|
|
|
console.log(`Koneko 音源测试平台 v3.0 已启动`);
|
2026-08-04 14:41:21 +08:00
|
|
|
|
console.log(`服务地址: http://0.0.0.0:${PORT}`);
|
|
|
|
|
|
console.log(`测试时间: ${new Date().toLocaleString('zh-CN')}`);
|
|
|
|
|
|
});
|