v3.0: 逆向分析增强 - QZ插件深度解析器

- 逆向分析 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级
This commit is contained in:
Koneko Tester
2026-08-04 16:19:35 +08:00
parent 99d813cedf
commit 2c6ea2450f
7 changed files with 1465 additions and 223 deletions

View File

@@ -329,8 +329,12 @@ body::before{content:'';position:fixed;inset:0;background:var(--bg-primary);z-in
.latency-bar-value.bad{color:var(--danger)}
.quality-tags{display:flex;flex-wrap:wrap;gap:4px;margin-top:8px}
.quality-tag{padding:3px 8px;border-radius:4px;font-size:11px;font-weight:500;background:var(--bg-tertiary);color:var(--text-secondary);border:1px solid var(--border-color)}
.quality-tag{padding:3px 8px;border-radius:4px;font-size:11px;font-weight:500;background:var(--bg-tertiary);color:var(--text-secondary);border:1px solid var(--border-color);display:inline-flex;align-items:center;gap:2px}
.quality-tag.declared{background:var(--success-bg);color:var(--success);border-color:transparent}
.quality-tag.qpass{background:rgba(0,184,148,0.15);color:var(--success);border-color:rgba(0,184,148,0.3)}
.quality-tag.qfail{background:rgba(225,112,85,0.15);color:var(--danger);border-color:rgba(225,112,85,0.3)}
.quality-tag.untested{opacity:0.6}
.quality-tag svg{width:10px;height:10px}
.modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,0.5);backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);z-index:1000;display:none;align-items:center;justify-content:center;padding:20px}
.modal-overlay.show{display:flex}

View File

@@ -1,6 +1,7 @@
/**
* Koneko 音源测试平台 - 前端逻辑 v1.1
* Koneko 音源测试平台 - 前端逻辑 v2.0
* SSE 实时日志 + 上传进度 + 平台展示
* 支持 .qz/.zip/.js 文件上传和服务端解压
*/
const state = {
@@ -67,7 +68,7 @@ function initFileUpload() {
function handleFiles(fileList) {
for (const file of fileList) {
if (file.name.match(/\.(js|zip)$/i)) state.files.push(file);
if (file.name.match(/\.(js|zip|qz)$/i)) state.files.push(file);
}
renderFileList();
}
@@ -78,7 +79,7 @@ function renderFileList() {
state.files.forEach((file, idx) => {
const chip = document.createElement('div');
chip.className = 'file-chip';
chip.innerHTML = `<i data-lucide="${file.name.endsWith('.zip') ? 'file-archive' : 'file-code'}"></i><span>${file.name}</span><span class="remove-file" data-idx="${idx}"><i data-lucide="x"></i></span>`;
chip.innerHTML = `<i data-lucide="${file.name.match(/\.(zip|qz)$/i) ? 'file-archive' : 'file-code'}"></i><span>${file.name}</span><span class="remove-file" data-idx="${idx}"><i data-lucide="x"></i></span>`;
chip.querySelector('.remove-file').addEventListener('click', () => { state.files.splice(idx, 1); renderFileList(); });
container.appendChild(chip);
});
@@ -86,20 +87,56 @@ function renderFileList() {
}
// 逐个读取文件,显示进度
// .qz/.zip 文件上传到服务端解压,.js 文件直接读取
async function readFilesWithProgress(files) {
const plugins = [];
const total = files.length;
$('uploadProgress').style.display = 'block';
for (let i = 0; i < total; i++) {
const file = files[i];
const pct = Math.round(((i) / total) * 100);
$('uploadProgressText').textContent = `读取文件 ${i + 1}/${total}: ${file.name}`;
$('uploadProgressPercent').textContent = `${pct}%`;
$('uploadProgressFill').style.width = `${pct}%`;
// 先将所有 .qz/.zip 文件批量上传到服务端解压
const archiveFiles = files.filter(f => f.name.match(/\.(qz|zip)$/i));
const jsFiles = files.filter(f => !f.name.match(/\.(qz|zip)$/i));
if (archiveFiles.length > 0) {
$('uploadProgressText').textContent = `正在上传 ${archiveFiles.length} 个压缩包到服务端解压...`;
$('uploadProgressPercent').textContent = '0%';
$('uploadProgressFill').style.width = '0%';
const formData = new FormData();
for (const f of archiveFiles) formData.append('files', f);
try {
const res = await fetch('/api/upload', { method: 'POST', body: formData });
const data = await res.json();
if (data.success && data.plugins) {
for (const p of data.plugins) {
plugins.push({ code: p.code, filename: p.filename });
}
$('uploadProgressText').textContent = `解压完成: ${archiveFiles.length} 个文件 -> ${data.plugins.length} 个插件`;
$('uploadProgressPercent').textContent = '100%';
$('uploadProgressFill').style.width = '100%';
addLog('success', `压缩包解压完成: ${archiveFiles.length} 个文件 -> ${data.plugins.length} 个插件`);
} else {
addLog('error', `压缩包解压失败: ${data.error || '未知错误'}`);
}
} catch (err) {
addLog('error', `上传失败: ${err.message}`);
}
await sleep(300);
}
// 读取 JS 文件
for (let i = 0; i < jsFiles.length; i++) {
const file = jsFiles[i];
const pct = Math.round(((archiveFiles.length > 0 ? 1 : 0 + i) / total) * 100);
$('uploadProgressText').textContent = `读取文件 ${i + 1}/${jsFiles.length}: ${file.name}`;
$('uploadProgressPercent').textContent = `${Math.min(100, pct)}%`;
$('uploadProgressFill').style.width = `${Math.min(100, pct)}%`;
const code = await readFile(file);
plugins.push({ code, filename: file.name });
await sleep(50);
}
$('uploadProgressText').textContent = `全部文件读取完成`;
$('uploadProgressPercent').textContent = `100%`;
$('uploadProgressFill').style.width = `100%`;
@@ -330,7 +367,7 @@ function renderIncrementalCard(index, result) {
card.id = `result-card-${index}`;
grid.appendChild(card);
}
card.outerHTML = renderResultCard(result, index).replace('<div class="result-card', `<div id="result-card-${index}" class="result-card`);
card.outerHTML = renderResultCard(result, index);
}
// === 渲染结果 ===
@@ -350,13 +387,31 @@ function renderResultCard(r, index) {
const score = r.summary?.score ?? 0;
const grade = r.summary?.details?.grade ?? 'D';
const status = r.overallStatus || 'pending';
const musicPlatform = r.metadata?.musicPlatformName || '';
const musicPlatformId = r.metadata?.musicPlatform || '';
const isObf = r.metadata?.isObfuscated;
const platformIcons = { lx:'music', qzv2:'disc-3', musicfree:'radio', unknown:'help-circle' };
const platformTag = `<span class="platform-tag tag-${platform}"><i data-lucide="${platformIcons[platform] || 'help-circle'}"></i>${r.platformLabel || '未知'}</span>`;
const musicTag = musicPlatform ? `<span class="platform-tag tag-qzv2" style="font-size:11px;"><i data-lucide="radio"></i>${musicPlatform}</span>` : '';
const obfTag = isObf ? `<span class="platform-tag" style="font-size:11px;background:var(--warning);color:#fff;"><i data-lucide="eye-off"></i>混淆</span>` : '';
const scoreBadge = `<div class="score-badge grade-${grade}"><div class="score-num">${score}</div><div class="score-grade">${grade}</div></div>`;
// 延时测试 - 区分 QZ API 和直连 API
let latencyHtml = '';
if (r.latencyTests?.length > 0) {
latencyHtml = r.latencyTests.slice(0, 4).map(t => {
const qzApiTests = r.latencyTests.filter(t => t.isQzApi);
const otherTests = r.latencyTests.filter(t => !t.isQzApi);
if (qzApiTests.length > 0) {
// QZ API 连通性测试
const basicTest = qzApiTests.find(t => t.quality === 'basic') || qzApiTests[0];
const qzStatus = basicTest.success ? 'success' : 'danger';
const qzIcon = basicTest.success ? 'check-circle' : 'x-circle';
latencyHtml += `<div style="margin-bottom:6px;"><span style="font-size:11px;color:var(--text-muted);"><i data-lucide="zap" style="width:12px;height:12px;display:inline;"></i> QZ API</span> <span style="color:var(--${qzStatus});font-size:11px;font-weight:600;">${basicTest.success ? basicTest.latency + 'ms' : '不通'}</span></div>`;
}
const displayTests = otherTests.slice(0, 3);
latencyHtml += displayTests.map(t => {
let cls = 'good'; if (t.latency > 1500) cls = 'bad'; else if (t.latency > 800) cls = 'ok';
const pct = Math.min(100, (t.latency / 3000) * 100);
const label = t.isPluginUrl ? '插件源' : truncUrl(t.url, 25);
@@ -364,18 +419,46 @@ function renderResultCard(r, index) {
}).join('');
}
// 音质测试 - 带状态指示
let qualityHtml = '';
if (r.qualityTests?.length > 0) {
qualityHtml = r.qualityTests.map(q => `<span class="quality-tag ${q.declared ? 'declared' : ''}">${q.quality}</span>`).join('');
qualityHtml = r.qualityTests.map(q => {
const label = q.label ? ` title="${q.label}"` : '';
let statusCls = 'untested';
let statusIcon = '';
if (q.tested) {
if (q.testStatus === 'pass') {
statusCls = 'qpass';
statusIcon = '<i data-lucide="check" style="width:10px;height:10px;display:inline;"></i>';
} else if (q.testStatus === 'fail') {
statusCls = 'qfail';
statusIcon = '<i data-lucide="x" style="width:10px;height:10px;display:inline;"></i>';
}
}
const latencyInfo = q.tested && q.testLatency ? ` (${q.testLatency}ms)` : '';
return `<span class="quality-tag ${q.declared ? 'declared' : ''} ${statusCls}"${label}>${statusIcon}${q.quality}${latencyInfo}</span>`;
}).join('');
}
// QZ API 端点信息
let qzApiHtml = '';
if (r.metadata?.qzApiEndpoint?.apiUrl) {
const qzApi = r.metadata.qzApiEndpoint;
qzApiHtml = `<div style="margin-bottom:8px;padding:6px 8px;background:var(--bg-secondary);border-radius:6px;font-size:11px;"><i data-lucide="link" style="width:12px;height:12px;display:inline;"></i> <span style="color:var(--text-muted);">QZ API:</span> <span style="color:var(--accent);font-family:monospace;">${truncUrl(qzApi.apiUrl, 60)}</span>${qzApi.apiKey ? ` <span style="color:var(--success);">Key: ${qzApi.apiKey.substring(0,6)}...</span>` : ''}</div>`;
}
const features = r.metadata?.features || [];
const featuresText = features.length > 0 ? features.slice(0, 5).join(', ') + (features.length > 5 ? ` +${features.length - 5}` : '') : '无';
const version = r.metadata?.version || '-';
const author = r.metadata?.author || '-';
const description = r.metadata?.description || '';
const dlInfo = r.downloadInfo || {};
const dlStatus = dlInfo.success ? `<span style="color:var(--success)">${formatSize(dlInfo.size||0)} · ${dlInfo.latency||0}ms</span>` : `<span style="color:var(--danger)">${dlInfo.error || '失败'}</span>`;
// 音质通过率
const qPassRate = r.summary?.details?.qualityPassRate || '0/0';
const qzApiStatus = r.summary?.details?.qzApiStatus ? `<span style="color:var(--success);font-size:11px;"><i data-lucide="check-circle" style="width:11px;height:11px;display:inline;"></i> QZ API OK</span>` : '';
return `<div class="result-card status-${status}" id="result-card-${index}">
<div class="result-card-header">
<div class="result-card-title">
@@ -383,15 +466,17 @@ function renderResultCard(r, index) {
<div class="result-url" title="${r.url || r.filename}">${r.url || r.filename || ''}</div>
</div>${scoreBadge}
</div>
<div class="platform-tags">${platformTag}${r.metadata?.sources?.length ? r.metadata.sources.map(s => `<span class="platform-tag tag-lx" style="font-size:11px;">${s.toUpperCase()}</span>`).join('') : ''}</div>
<div class="platform-tags">${platformTag}${musicTag}${obfTag}</div>
${description ? `<div style="font-size:12px;color:var(--text-muted);margin-bottom:8px;">${escapeHtml(description)}</div>` : ''}
${qzApiHtml}
<div class="info-grid">
<div class="info-item"><span class="info-label">版本</span><span class="info-value">${version}</span></div>
<div class="info-item"><span class="info-label">作者</span><span class="info-value">${author}</span></div>
<div class="info-item"><span class="info-label">下载</span><span class="info-value">${dlStatus}</span></div>
<div class="info-item"><span class="info-label">成功率</span><span class="info-value">${r.summary?.details?.successRate || 'N/A'}</span></div>
</div>
${latencyHtml ? `<div style="margin-bottom:12px;"><div class="info-label" style="margin-bottom:6px;">延时测试</div>${latencyHtml}</div>` : ''}
${qualityHtml ? `<div><div class="info-label" style="margin-bottom:4px;">音质支持</div><div class="quality-tags">${qualityHtml}</div></div>` : ''}
${latencyHtml ? `<div style="margin-bottom:12px;"><div class="info-label" style="margin-bottom:6px;">延时测试 ${qzApiStatus}</div>${latencyHtml}</div>` : ''}
${qualityHtml ? `<div><div class="info-label" style="margin-bottom:4px;">音质支持 <span style="font-size:10px;color:var(--text-muted);">(通过率: ${qPassRate})</span></div><div class="quality-tags">${qualityHtml}</div></div>` : ''}
</div>`;
}
@@ -502,14 +587,26 @@ function generatePoster(results) {
const platformLabels = { lx:'Lx Music', qzv2:'QZV2', musicfree:'MusicFree', unknown:'未知' };
const platText = platformLabels[platform] || '未知';
const musicPlat = r.metadata?.musicPlatformName || '';
const platColor = platform==='lx'?accent:(platform==='qzv2'?success:(platform==='musicfree'?'#e84393':textMuted));
const platTextW = ctx.measureText(platText).width;
ctx.fillStyle = platColor+'20'; roundRect(ctx,padding+42,y+34,platTextW+16,20,10); ctx.fill();
ctx.fillStyle = platColor; ctx.font = '11px Inter, sans-serif';
ctx.fillText(platText, padding+50, y+38);
ctx.fillStyle = textMuted; ctx.font = '12px Inter, sans-serif';
ctx.fillText(r.metadata?.version?`v${r.metadata.version}`:'v-', padding+50+platTextW+16, y+38);
// 音乐平台标签
if (musicPlat) {
const mpTextW = ctx.measureText(musicPlat).width;
const mpX = padding + 50 + platTextW + 16;
ctx.fillStyle = '#6c5ce720'; roundRect(ctx, mpX, y+34, mpTextW+16, 20, 10); ctx.fill();
ctx.fillStyle = '#6c5ce7'; ctx.font = '11px Inter, "Noto Sans SC", sans-serif';
ctx.fillText(musicPlat, mpX+8, y+38);
ctx.fillStyle = textMuted; ctx.font = '12px Inter, sans-serif';
ctx.fillText(r.metadata?.version?`v${r.metadata.version}`:'v-', mpX+mpTextW+24, y+38);
} else {
ctx.fillStyle = textMuted; ctx.font = '12px Inter, sans-serif';
ctx.fillText(r.metadata?.version?`v${r.metadata.version}`:'v-', padding+50+platTextW+16, y+38);
}
ctx.fillStyle = textMuted; ctx.font = '11px Inter, "Noto Sans SC", sans-serif';
ctx.fillText('音质:', padding+42, y+64);