- 逆向分析 QZMusic_PC pluginSystem.ts 和 CeruMusic 插件文档 - 重写 detector.js: 支持 webpack 打包和混淆代码解析 - 正确提取 pluginInfo.info.id 识别音乐平台(mg/wy/tx/kw/kg) - 提取 QZ API 端点(api.qz.shiqianjiang.cn)和 X-API-KEY - 重写 tester.js: 自动测试 QZ API 端点+逐音质级别测试 - 前端更新: 音质测试状态显示(PASS/FAIL)、QZ API 信息展示 - MD报告增加 QZ API 状态列和音质通过率 - 完整音质等级体系: 128k~master 共14级
518 lines
20 KiB
JavaScript
518 lines
20 KiB
JavaScript
/**
|
|
* 插件测试模块 v3.0 - 逆向分析增强版
|
|
* 支持日志回调、QZ API 端点测试、音质级别测试
|
|
*
|
|
* 逆向来源:
|
|
* - QZ getUrl(songId, quality) 调用 api.qz.shiqianjiang.cn/music/url
|
|
* - 代理服务器 localhost:5266 测试模式
|
|
* - 各音乐平台直接 API 端点测试
|
|
*/
|
|
|
|
const http = require('http');
|
|
const https = require('https');
|
|
const { URL } = require('url');
|
|
const { detectPlatform, PLATFORMS, QUALITY_MAP } = require('./detector');
|
|
|
|
// ============================================================
|
|
// HTTP 工具函数
|
|
// ============================================================
|
|
|
|
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/3.0', '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();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 测试 URL 延时 - 支持自定义 headers
|
|
*/
|
|
function testUrlLatency(url, options = {}) {
|
|
const timeout = options.timeout || 10000;
|
|
const headers = options.headers || { 'User-Agent': 'Mozilla/5.0 Koneko-Music-Tester/3.0', 'Accept': '*/*' };
|
|
if (options.range) headers['Range'] = options.range;
|
|
|
|
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 reqOptions = {
|
|
hostname: parsedUrl.hostname,
|
|
port: parsedUrl.port || (parsedUrl.protocol === 'https:' ? 443 : 80),
|
|
path: parsedUrl.pathname + parsedUrl.search,
|
|
method: options.method || 'GET',
|
|
headers: headers,
|
|
timeout
|
|
};
|
|
const req = client.request(reqOptions, (res) => {
|
|
const latency = Date.now() - startTime;
|
|
let bodySize = 0;
|
|
let bodyChunks = [];
|
|
res.on('data', (chunk) => {
|
|
bodySize += chunk.length;
|
|
if (bodyChunks.length < 5) bodyChunks.push(chunk);
|
|
if (bodySize > 8192) res.destroy();
|
|
});
|
|
res.on('end', () => {
|
|
const body = Buffer.concat(bodyChunks).toString('utf-8').substring(0, 500);
|
|
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),
|
|
bodyPreview: body,
|
|
error: null
|
|
});
|
|
});
|
|
res.on('error', () => finish({
|
|
url, status: res.statusCode, latency, success: false,
|
|
contentType: '', contentLength: 0, bodyPreview: '', error: '响应读取错误'
|
|
}));
|
|
});
|
|
req.on('timeout', () => {
|
|
req.destroy();
|
|
finish({ url, status: 0, latency: timeout, success: false, contentType: '', contentLength: 0, bodyPreview: '', error: '请求超时' });
|
|
});
|
|
req.on('error', (err) => finish({
|
|
url, status: 0, latency: Date.now() - startTime, success: false,
|
|
contentType: '', contentLength: 0, bodyPreview: '', error: err.message
|
|
}));
|
|
req.end();
|
|
} catch (err) {
|
|
finish({ url, status: 0, latency: 0, success: false, contentType: '', contentLength: 0, bodyPreview: '', error: err.message });
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 构建并测试 QZ API 端点
|
|
* 逆向来源: getUrl(songId, quality) -> api.qz.shiqianjiang.cn/music/url?songId=X&quality=Y&source=Z
|
|
*/
|
|
async function testQzApiEndpoint(qzApiEndpoint, musicPlatform, qualities, log) {
|
|
const results = [];
|
|
|
|
if (!qzApiEndpoint || !qzApiEndpoint.apiUrl) {
|
|
return results;
|
|
}
|
|
|
|
// 构建基础 API URL
|
|
let baseUrl = qzApiEndpoint.apiUrl;
|
|
// 替换模板变量为测试值
|
|
baseUrl = baseUrl.replace(/\$\{songId\}|\$\{id\}|\$\{musicId\}/g, '1');
|
|
baseUrl = baseUrl.replace(/\$\{quality\}/g, '128k');
|
|
baseUrl = baseUrl.replace(/\$\{[^}]+\}/g, '1');
|
|
|
|
// 如果 URL 中没有 source 参数,添加它
|
|
if (musicPlatform && !/source=/.test(baseUrl)) {
|
|
baseUrl += (baseUrl.includes('?') ? '&' : '?') + `source=${musicPlatform}`;
|
|
}
|
|
|
|
// 构建请求 headers
|
|
const headers = { 'User-Agent': 'Mozilla/5.0 Koneko-Music-Tester/3.0', 'Accept': 'application/json' };
|
|
if (qzApiEndpoint.apiKey) {
|
|
headers['X-API-KEY'] = qzApiEndpoint.apiKey;
|
|
log('info', `使用 API Key: ${qzApiEndpoint.apiKey.substring(0, 8)}...`);
|
|
}
|
|
|
|
log('info', `测试 QZ API 端点: ${truncate(baseUrl, 100)}`);
|
|
|
|
// 测试基础 API 连通性
|
|
const baseTest = await testUrlLatency(baseUrl, { headers, timeout: 12000 });
|
|
results.push({
|
|
...baseTest,
|
|
isQzApi: true,
|
|
quality: 'basic',
|
|
label: 'API 连通性测试'
|
|
});
|
|
|
|
if (baseTest.success) {
|
|
log('success', `QZ API 连通 | 状态: ${baseTest.status} | 延时: ${baseTest.latency}ms`);
|
|
if (baseTest.bodyPreview) {
|
|
// 检查响应体是否包含 url 字段
|
|
if (baseTest.bodyPreview.includes('"url"') || baseTest.bodyPreview.includes("'url'")) {
|
|
log('success', `API 返回包含 url 字段`);
|
|
} else if (baseTest.bodyPreview.includes('error') || baseTest.bodyPreview.includes('Error')) {
|
|
log('warn', `API 返回错误信息: ${truncate(baseTest.bodyPreview, 100)}`);
|
|
}
|
|
}
|
|
} else {
|
|
log('warn', `QZ API 不通 | 状态: ${baseTest.status} | 错误: ${baseTest.error || '未知'}`);
|
|
}
|
|
|
|
// 逐个测试音质级别
|
|
const testQualities = qualities.length > 0 ? qualities : ['128k', '320k', 'flac'];
|
|
log('info', `开始音质级别测试 (${testQualities.length} 个)...`);
|
|
|
|
for (const quality of testQualities) {
|
|
// 构建音质测试 URL
|
|
let qualityUrl = qzApiEndpoint.apiUrl
|
|
.replace(/\$\{songId\}|\$\{id\}|\$\{musicId\}/g, '1')
|
|
.replace(/\$\{quality\}/g, quality)
|
|
.replace(/\$\{[^}]+\}/g, '1');
|
|
|
|
if (musicPlatform && !/source=/.test(qualityUrl)) {
|
|
qualityUrl += (qualityUrl.includes('?') ? '&' : '?') + `source=${musicPlatform}`;
|
|
}
|
|
|
|
// 替换已有的 quality 参数值
|
|
qualityUrl = qualityUrl.replace(/(quality=)[^&]*/, `$1${quality}`);
|
|
|
|
const qualityTest = await testUrlLatency(qualityUrl, { headers, timeout: 10000 });
|
|
const qualityLabel = QUALITY_MAP[quality] ? QUALITY_MAP[quality].name : quality;
|
|
|
|
results.push({
|
|
...qualityTest,
|
|
isQzApi: true,
|
|
quality: quality,
|
|
label: qualityLabel
|
|
});
|
|
|
|
const statusIcon = qualityTest.success ? 'OK' : 'FAIL';
|
|
log(qualityTest.success ? 'success' : 'warn',
|
|
`音质 ${quality} (${qualityLabel}) | ${statusIcon} | 状态: ${qualityTest.status} | 延时: ${qualityTest.latency}ms`);
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
// ============================================================
|
|
// 测试主函数
|
|
// ============================================================
|
|
|
|
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.isObfuscated) {
|
|
log('info', `检测到混淆代码,使用部分提取模式`);
|
|
}
|
|
if (detection.metadata.name) log('info', `插件名称: ${detection.metadata.name}`);
|
|
if (detection.metadata.version) log('info', `版本: ${detection.metadata.version}`);
|
|
if (detection.metadata.description) log('info', `描述: ${detection.metadata.description}`);
|
|
if (detection.metadata.musicPlatformName) {
|
|
log('success', `音乐平台: ${detection.metadata.musicPlatformName} (${detection.metadata.musicPlatform || 'N/A'})`);
|
|
}
|
|
} 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()}`);
|
|
}
|
|
if (detection.metadata.quality && detection.metadata.quality.length > 0) {
|
|
const qualityStr = detection.metadata.quality.map((q, i) => {
|
|
const detail = detection.metadata.qualityDetails[i] || {};
|
|
return `${q}(${detail.name || QUALITY_MAP[q]?.name || q})`;
|
|
}).join(', ');
|
|
log('success', `支持音质 (${detection.metadata.quality.length} 级): ${qualityStr}`);
|
|
}
|
|
if (detection.metadata.envConfig && detection.metadata.envConfig.length > 0) {
|
|
log('info', `环境配置: ${detection.metadata.envConfig.map(e => `${e.key}(${e.name})`).join(', ')}`);
|
|
}
|
|
if (detection.metadata.extConfig && detection.metadata.extConfig.length > 0) {
|
|
log('info', `扩展功能: ${detection.metadata.extConfig.map(e => e.name).join(', ')}`);
|
|
}
|
|
|
|
// 步骤4: 延时测试
|
|
// 4a: 测试插件源 URL 可达性
|
|
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`);
|
|
}
|
|
|
|
// 4b: 测试 QZ API 端点 (核心!)
|
|
const qzApi = detection.metadata.qzApiEndpoint;
|
|
if (qzApi && qzApi.apiUrl) {
|
|
log('info', `发现 QZ API 端点: ${truncate(qzApi.apiUrl, 80)}`);
|
|
if (qzApi.sourceParam) log('info', `API source 参数: ${qzApi.sourceParam}`);
|
|
if (qzApi.apiKey) log('info', `API Key: ${qzApi.apiKey.substring(0, 8)}...`);
|
|
|
|
const qzApiResults = await testQzApiEndpoint(
|
|
qzApi,
|
|
detection.metadata.musicPlatform,
|
|
detection.metadata.quality,
|
|
log
|
|
);
|
|
result.latencyTests.push(...qzApiResults);
|
|
} else {
|
|
log('info', `未发现 QZ 统一 API 端点,测试直连 API...`);
|
|
}
|
|
|
|
// 4c: 测试音乐平台直连 API 端点
|
|
const directApiUrls = (detection.metadata.filteredApiUrls || []).slice(0, 5);
|
|
const qzApiUrls = directApiUrls.filter(u => /api\.qz\.shiqianjiang/i.test(u));
|
|
const platformUrls = directApiUrls.filter(u => !/api\.qz\.shiqianjiang/i.test(u));
|
|
|
|
if (platformUrls.length > 0) {
|
|
log('info', `测试音乐平台直连 API (${platformUrls.length} 个)...`);
|
|
for (let i = 0; i < platformUrls.length; i++) {
|
|
log('info', `[${i + 1}/${platformUrls.length}] ${truncate(platformUrls[i], 80)}`);
|
|
}
|
|
const latencyPromises = platformUrls.map(u => testUrlLatency(u, { range: 'bytes=0-1023' }));
|
|
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', `直连 API 测试完成 | 成功: ${successCount}/${platformUrls.length} | 平均延时: ${avgLatency}ms`);
|
|
} else {
|
|
log('warn', `直连 API 测试完成 | 所有端点均不可达`);
|
|
}
|
|
}
|
|
|
|
// 步骤5: 音质测试结果汇总
|
|
const declaredQualities = detection.metadata.quality || [];
|
|
const qualityDetails = detection.metadata.qualityDetails || [];
|
|
const qualityMap = QUALITY_MAP;
|
|
|
|
// 从 QZ API 测试结果中提取音质测试状态
|
|
const qzQualityResults = result.latencyTests.filter(t => t.isQzApi && t.quality && t.quality !== 'basic');
|
|
|
|
result.qualityTests = declaredQualities.map((q, idx) => {
|
|
const detail = qualityDetails[idx] || {};
|
|
const qzResult = qzQualityResults.find(t => t.quality === q);
|
|
return {
|
|
quality: q,
|
|
label: detail.name || qualityMap[q]?.name || q,
|
|
ui: detail.ui || '',
|
|
declared: true,
|
|
tested: !!qzResult,
|
|
testStatus: qzResult ? (qzResult.success ? 'pass' : 'fail') : 'untested',
|
|
testLatency: qzResult?.latency || 0,
|
|
testStatusCode: qzResult?.status || 0,
|
|
};
|
|
});
|
|
|
|
// 如果有 QZ API 测试但没有声明音质,从测试结果生成
|
|
if (result.qualityTests.length === 0 && qzQualityResults.length > 0) {
|
|
result.qualityTests = qzQualityResults.map(t => ({
|
|
quality: t.quality,
|
|
label: t.label || qualityMap[t.quality]?.name || t.quality,
|
|
ui: '',
|
|
declared: false,
|
|
tested: true,
|
|
testStatus: t.success ? 'pass' : 'fail',
|
|
testLatency: t.latency,
|
|
testStatusCode: t.status,
|
|
}));
|
|
}
|
|
|
|
// 如果没有任何音质信息,使用默认值
|
|
if (result.qualityTests.length === 0 && detection.platform !== 'unknown') {
|
|
const defaults = getDefaultQualities(detection.platform);
|
|
result.qualityTests = defaults.map(q => ({
|
|
quality: q, label: qualityMap[q]?.name || q, ui: '',
|
|
declared: false, tested: false, testStatus: 'untested', testLatency: 0, testStatusCode: 0
|
|
}));
|
|
log('info', `未声明音质,使用默认音质列表: ${defaults.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 getDefaultQualities(platform) {
|
|
const defaults = {
|
|
lx: ['128k', '320k', 'flac', 'flac24bit', 'hires', 'master'],
|
|
qzv2: ['128k', '320k', 'flac', 'hires', 'master'],
|
|
musicfree: ['low', 'standard', 'high', 'super', 'hires', 'master']
|
|
};
|
|
return defaults[platform] || [];
|
|
}
|
|
|
|
function calculateSummary(result) {
|
|
let score = 0;
|
|
const details = {};
|
|
|
|
// 下载/读取 (20分)
|
|
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 = '文件上传';
|
|
}
|
|
|
|
// 格式识别 (20分)
|
|
if (result.platform !== 'unknown') {
|
|
score += 20; details.format = result.platformLabel;
|
|
} else {
|
|
details.format = '未识别';
|
|
}
|
|
|
|
// 音乐平台识别 (5分)
|
|
if (result.metadata?.musicPlatform) {
|
|
score += 5;
|
|
details.musicPlatform = result.metadata.musicPlatformName;
|
|
}
|
|
|
|
// 延时测试 (30分)
|
|
const allLatencyTests = result.latencyTests || [];
|
|
const successLatency = allLatencyTests.filter(t => t.success);
|
|
|
|
if (allLatencyTests.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}/${allLatencyTests.length}`;
|
|
|
|
// QZ API 测试单独加分
|
|
const qzApiSuccess = successLatency.filter(t => t.isQzApi);
|
|
if (qzApiSuccess.length > 0) {
|
|
score += 25; // QZ API 通则高分
|
|
details.qzApiStatus = 'OK';
|
|
} else {
|
|
if (avgLatency < 300) score += 25;
|
|
else if (avgLatency < 800) score += 20;
|
|
else if (avgLatency < 1500) score += 15;
|
|
else if (avgLatency < 3000) score += 8;
|
|
else score += 4;
|
|
}
|
|
} else {
|
|
details.avgLatency = -1;
|
|
details.successRate = `0/${allLatencyTests.length}`;
|
|
score += 3;
|
|
}
|
|
} else {
|
|
score += 15;
|
|
details.avgLatency = -1;
|
|
details.successRate = 'N/A';
|
|
}
|
|
|
|
// 音质 (15分)
|
|
if (result.qualityTests.length > 0) {
|
|
const declaredCount = result.qualityTests.filter(q => q.declared).length;
|
|
const testedCount = result.qualityTests.filter(q => q.tested).length;
|
|
const passedCount = result.qualityTests.filter(q => q.testStatus === 'pass').length;
|
|
score += Math.min(15, declaredCount * 3 + passedCount * 2);
|
|
details.qualityCount = result.qualityTests.length;
|
|
details.qualities = result.qualityTests.map(q => q.quality).join(', ');
|
|
details.qualityPassRate = `${passedCount}/${testedCount}`;
|
|
} else {
|
|
details.qualityCount = 0;
|
|
}
|
|
|
|
// 功能 (10分)
|
|
if (result.metadata.features && result.metadata.features.length > 0) {
|
|
score += Math.min(10, result.metadata.features.length * 2);
|
|
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, testQzApiEndpoint };
|