/** * 插件测试模块 - 支持日志回调 * 每个步骤都会通过 logger 回调实时输出 */ const http = require('http'); const https = require('https'); const { URL } = require('url'); const { detectPlatform, PLATFORMS } = require('./detector'); function fetchPluginCode(url, timeout = 15000) { return new Promise((resolve, reject) => { const parsedUrl = new URL(url); const client = parsedUrl.protocol === 'https:' ? https : http; const options = { hostname: parsedUrl.hostname, port: parsedUrl.port || (parsedUrl.protocol === 'https:' ? 443 : 80), path: parsedUrl.pathname + parsedUrl.search, method: 'GET', headers: { 'User-Agent': 'Mozilla/5.0 Koneko-Music-Tester/1.1', 'Accept': '*/*' }, timeout }; const req = client.request(options, (res) => { if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { return fetchPluginCode(new URL(res.headers.location, url).href, timeout).then(resolve).catch(reject); } if (res.statusCode !== 200) return reject(new Error(`HTTP ${res.statusCode}`)); let data = ''; let dataBytes = Buffer.alloc(0); res.on('data', (chunk) => { typeof chunk === 'string' ? data += chunk : dataBytes = Buffer.concat([dataBytes, chunk]); }); res.on('end', () => { const content = data || dataBytes.toString('utf-8'); resolve({ code: content, statusCode: res.statusCode, headers: res.headers, size: Buffer.byteLength(content) }); }); }); req.on('timeout', () => { req.destroy(); reject(new Error('下载超时')); }); req.on('error', reject); req.end(); }); } function testUrlLatency(url, timeout = 10000) { return new Promise((resolve) => { const startTime = Date.now(); let resolved = false; const finish = (result) => { if (!resolved) { resolved = true; resolve(result); } }; try { const parsedUrl = new URL(url); const client = parsedUrl.protocol === 'https:' ? https : http; const options = { hostname: parsedUrl.hostname, port: parsedUrl.port || (parsedUrl.protocol === 'https:' ? 443 : 80), path: parsedUrl.pathname + parsedUrl.search, method: 'GET', headers: { 'User-Agent': 'Mozilla/5.0 Koneko-Music-Tester/1.1', 'Accept': '*/*', 'Range': 'bytes=0-1023' }, timeout }; const req = client.request(options, (res) => { const latency = Date.now() - startTime; let bodySize = 0; res.on('data', (chunk) => { bodySize += chunk.length; if (bodySize > 4096) res.destroy(); }); res.on('end', () => finish({ url, status: res.statusCode, latency, success: res.statusCode >= 200 && res.statusCode < 400, contentType: res.headers['content-type'] || '', contentLength: parseInt(res.headers['content-length'] || '0', 10), error: null })); res.on('error', () => finish({ url, status: res.statusCode, latency, success: false, contentType: '', contentLength: 0, error: '响应读取错误' })); }); req.on('timeout', () => { req.destroy(); finish({ url, status: 0, latency: timeout, success: false, contentType: '', contentLength: 0, error: '请求超时' }); }); req.on('error', (err) => finish({ url, status: 0, latency: Date.now() - startTime, success: false, contentType: '', contentLength: 0, error: err.message })); req.end(); } catch (err) { finish({ url, status: 0, latency: 0, success: false, contentType: '', contentLength: 0, error: err.message }); } }); } /** * 测试插件主入口 - 带日志回调 * @param {string} url * @param {string} customCode * @param {string} filename * @param {function} logger - 日志回调 (level, message, data) */ async function testPlugin(url, customCode = null, filename = '', logger = null) { const log = (level, message, data) => { if (logger) logger(level, message, data); }; const result = { url: url || filename, filename, platform: 'unknown', platformLabel: '未知格式', metadata: {}, downloadInfo: null, latencyTests: [], qualityTests: [], overallStatus: 'pending', summary: {}, testedAt: new Date().toISOString() }; let code = customCode; // 步骤1: 下载/读取 log('info', `开始处理: ${filename || url || '未知'}`); if (!code && url) { log('info', `正在下载插件文件...`); try { const downloadStart = Date.now(); const response = await fetchPluginCode(url); const downloadLatency = Date.now() - downloadStart; code = response.code; result.downloadInfo = { success: true, latency: downloadLatency, size: response.size, statusCode: response.statusCode, contentType: response.headers['content-type'] || '' }; log('success', `下载完成 | 大小: ${formatSize(response.size)} | 耗时: ${downloadLatency}ms`); } catch (err) { result.downloadInfo = { success: false, latency: 0, error: err.message }; result.overallStatus = 'failed'; result.summary = { error: `插件下载失败: ${err.message}`, score: 0 }; log('error', `下载失败: ${err.message}`); return result; } } else if (code) { result.downloadInfo = { success: true, latency: 0, size: Buffer.byteLength(code), statusCode: 0, contentType: 'text/plain' }; log('success', `文件已读取 | 大小: ${formatSize(Buffer.byteLength(code))}`); } if (!code) { result.overallStatus = 'failed'; result.summary = { error: '无插件代码', score: 0 }; log('error', '无插件代码'); return result; } // 步骤2: 格式检测 log('info', `正在识别插件格式...`); const detection = detectPlatform(code, filename); result.platform = detection.platform; result.platformLabel = detection.platformLabel; result.metadata = detection.metadata; result.detectionScores = detection.scores; if (detection.platform !== 'unknown') { log('success', `格式识别: ${detection.platformLabel} (置信度: ${detection.confidence})`); if (detection.metadata.name) log('info', `插件名称: ${detection.metadata.name}`); if (detection.metadata.version) log('info', `版本: ${detection.metadata.version}`); if (detection.metadata.author) log('info', `作者: ${detection.metadata.author}`); } else { log('warn', `未能识别插件格式,得分: lx=${detection.scores.lx}, qzv2=${detection.scores.qzv2}, musicfree=${detection.scores.musicfree}`); } // 步骤3: 功能分析 if (detection.metadata.features && detection.metadata.features.length > 0) { log('info', `功能列表: ${detection.metadata.features.join(', ')}`); } if (detection.metadata.sources && detection.metadata.sources.length > 0) { log('info', `音源: ${detection.metadata.sources.join(', ').toUpperCase()}`); } // 步骤4: 延时测试 const apiUrls = detection.metadata.apiUrls || []; const testUrls = apiUrls .filter(u => !u.match(/\.(png|jpg|jpeg|gif|svg|ico|css|woff|ttf)$/i)) .filter(u => !u.includes('catcat.work')) .filter(u => !u.includes('w3.org')) .filter(u => !u.includes('schema.org')) .slice(0, 5); if (url && !customCode) { log('info', `测试插件源 URL 可达性...`); const pluginUrlTest = await testUrlLatency(url); result.latencyTests.push({ ...pluginUrlTest, isPluginUrl: true }); log(pluginUrlTest.success ? 'success' : 'warn', `插件源 URL | 状态: ${pluginUrlTest.status} | 延时: ${pluginUrlTest.latency}ms`); } if (testUrls.length > 0) { log('info', `开始延时测试,共 ${testUrls.length} 个 API 端点...`); for (let i = 0; i < testUrls.length; i++) { const testUrl = testUrls[i]; log('info', `[${i + 1}/${testUrls.length}] 测试: ${truncate(testUrl, 60)}`); } const latencyPromises = testUrls.map(u => testUrlLatency(u)); const latencyResults = await Promise.all(latencyPromises); result.latencyTests.push(...latencyResults); const successCount = latencyResults.filter(t => t.success).length; if (successCount > 0) { const avgLatency = Math.round(latencyResults.filter(t => t.success).reduce((sum, t) => sum + t.latency, 0) / successCount); log('success', `延时测试完成 | 成功: ${successCount}/${testUrls.length} | 平均延时: ${avgLatency}ms`); } else { log('warn', `延时测试完成 | 所有端点均不可达`); } } else if (!url || customCode) { log('info', `未发现可测试的 API 端点`); } // 步骤5: 音质分析 const declaredQualities = detection.metadata.quality || []; const qualityMap = getQualityMap(detection.platform); result.qualityTests = declaredQualities.map(q => ({ quality: q, label: qualityMap[q] || q, declared: true, testable: testUrls.length > 0 })); if (result.qualityTests.length === 0 && detection.platform !== 'unknown') { const defaults = getDefaultQualities(detection.platform); result.qualityTests = defaults.map(q => ({ quality: q, label: qualityMap[q] || q, declared: false, testable: testUrls.length > 0 })); log('info', `未声明音质,使用默认音质列表: ${defaults.join(', ')}`); } else if (declaredQualities.length > 0) { log('success', `支持音质: ${declaredQualities.map(q => `${q}(${qualityMap[q] || q})`).join(', ')}`); } // 步骤6: 评分 log('info', `正在计算综合评分...`); result.summary = calculateSummary(result); result.overallStatus = result.summary.score >= 60 ? 'passed' : (result.summary.score > 0 ? 'warning' : 'failed'); log('success', `测试完成 | 评分: ${result.summary.score}/100 | 等级: ${result.summary.details?.grade || 'D'} | 状态: ${result.overallStatus === 'passed' ? '通过' : (result.overallStatus === 'warning' ? '警告' : '失败')}`); return result; } function getQualityMap(platform) { const maps = { lx: { '128k': '标准 128kbps', '192k': '标准 192kbps', '320k': '高品质 320kbps', 'flac': '无损 FLAC', 'flac24bit': 'Hi-Res FLAC 24bit', 'hires': 'Hi-Res', 'master': '母带 Master' }, qzv2: { 'low': '低音质', 'standard': '标准音质', 'exhigh': '极高音质', 'high': '高音质', 'lossless': '无损音质', 'hires': 'Hi-Res', 'master': '母带 Master' }, musicfree: { 'low': '低音质', 'standard': '标准音质', 'high': '高音质', 'super': '超高音质', 'hires': 'Hi-Res', 'master': '母带 Master' } }; return maps[platform] || {}; } function getDefaultQualities(platform) { const defaults = { lx: ['128k', '192k', '320k', 'flac', 'flac24bit', 'master'], qzv2: ['standard', 'exhigh', 'high', 'lossless', 'hires', 'master'], musicfree: ['low', 'standard', 'high', 'super', 'hires', 'master'] }; return defaults[platform] || []; } function calculateSummary(result) { let score = 0; const details = {}; if (result.downloadInfo && result.downloadInfo.success) { score += 20; details.download = '成功'; } else if (result.downloadInfo && !result.downloadInfo.success) { details.download = '失败'; return { score: 0, details, error: result.downloadInfo.error }; } else { score += 20; details.download = '文件上传'; } if (result.platform !== 'unknown') { score += 20; details.format = result.platformLabel; } else { details.format = '未识别'; } const successLatency = result.latencyTests.filter(t => t.success); if (result.latencyTests.length > 0) { if (successLatency.length > 0) { const avgLatency = successLatency.reduce((sum, t) => sum + t.latency, 0) / successLatency.length; details.avgLatency = Math.round(avgLatency); details.successRate = `${successLatency.length}/${result.latencyTests.length}`; if (avgLatency < 300) score += 30; else if (avgLatency < 800) score += 25; else if (avgLatency < 1500) score += 18; else if (avgLatency < 3000) score += 10; else score += 5; } else { details.avgLatency = -1; details.successRate = `0/${result.latencyTests.length}`; } } else { score += 15; details.avgLatency = -1; details.successRate = 'N/A'; } if (result.qualityTests.length > 0) { const declaredCount = result.qualityTests.filter(q => q.declared).length; score += Math.min(15, declaredCount * 5); details.qualityCount = result.qualityTests.length; details.qualities = result.qualityTests.map(q => q.quality).join(', '); } else { details.qualityCount = 0; } if (result.metadata.features && result.metadata.features.length > 0) { score += Math.min(15, result.metadata.features.length * 3); details.features = result.metadata.features.join(', '); } details.score = Math.min(100, score); if (details.score >= 85) details.grade = 'S'; else if (details.score >= 70) details.grade = 'A'; else if (details.score >= 55) details.grade = 'B'; else if (details.score >= 40) details.grade = 'C'; else details.grade = 'D'; return { score: Math.min(100, score), details }; } function formatSize(bytes) { if (bytes < 1024) return bytes + 'B'; if (bytes < 1048576) return (bytes / 1024).toFixed(1) + 'KB'; return (bytes / 1048576).toFixed(1) + 'MB'; } function truncate(str, max) { if (!str) return ''; if (str.length <= max) return str; return str.substring(0, max - 3) + '...'; } module.exports = { testPlugin, fetchPluginCode, testUrlLatency };