mirror of
https://github.com/NeteaseCloudMusicApiEnhanced/api-clawer.git
synced 2026-08-12 08:50:33 +00:00
feats
This commit is contained in:
parent
9015b5dadf
commit
817a5572af
5
.codegraph/.gitignore
vendored
Normal file
5
.codegraph/.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# CodeGraph data files — local to each machine, not for committing.
|
||||||
|
# Ignore everything in .codegraph/ except this file itself, so transient
|
||||||
|
# files (the database, daemon.pid, sockets, logs) never show up in git.
|
||||||
|
*
|
||||||
|
!.gitignore
|
||||||
@ -2,6 +2,7 @@ const express = require('express');
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const axios = require('axios');
|
const axios = require('axios');
|
||||||
|
const { randomUUID } = require('crypto');
|
||||||
require('dotenv').config();
|
require('dotenv').config();
|
||||||
|
|
||||||
const CAPTURE_FILE = path.join(__dirname, '..', '..', 'captures.jsonl');
|
const CAPTURE_FILE = path.join(__dirname, '..', '..', 'captures.jsonl');
|
||||||
@ -9,6 +10,22 @@ const CAPTURE_FILE = path.join(__dirname, '..', '..', 'captures.jsonl');
|
|||||||
let capturedData = [];
|
let capturedData = [];
|
||||||
let clients = [];
|
let clients = [];
|
||||||
|
|
||||||
|
function ensureCaptureId(data) {
|
||||||
|
if (!data || typeof data !== 'object') return data;
|
||||||
|
if (typeof data.id !== 'string' || !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(data.id)) {
|
||||||
|
data.id = randomUUID();
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
function rewriteCaptureFile() {
|
||||||
|
fs.writeFileSync(
|
||||||
|
CAPTURE_FILE,
|
||||||
|
capturedData.map(data => JSON.stringify(data)).join('\n') + (capturedData.length ? '\n' : ''),
|
||||||
|
'utf-8'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ======================== 数据持久化 ========================
|
// ======================== 数据持久化 ========================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -25,7 +42,10 @@ function loadFromFile() {
|
|||||||
try { return JSON.parse(line); }
|
try { return JSON.parse(line); }
|
||||||
catch { return null; }
|
catch { return null; }
|
||||||
})
|
})
|
||||||
.filter(Boolean);
|
.filter(Boolean)
|
||||||
|
.map(ensureCaptureId);
|
||||||
|
// 旧记录没有稳定 ID;补齐后写回,确保选择与删除跨刷新保持一致。
|
||||||
|
rewriteCaptureFile();
|
||||||
console.log(`Loaded ${capturedData.length} historical records from ${CAPTURE_FILE}`);
|
console.log(`Loaded ${capturedData.length} historical records from ${CAPTURE_FILE}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -177,7 +197,7 @@ function createApp() {
|
|||||||
app.use(express.static(path.join(__dirname, 'public')));
|
app.use(express.static(path.join(__dirname, 'public')));
|
||||||
|
|
||||||
app.post('/api/capture', (req, res) => {
|
app.post('/api/capture', (req, res) => {
|
||||||
const data = req.body;
|
const data = ensureCaptureId(req.body);
|
||||||
capturedData.push(data);
|
capturedData.push(data);
|
||||||
console.log('Captured data:', data.path);
|
console.log('Captured data:', data.path);
|
||||||
// 持久化
|
// 持久化
|
||||||
@ -207,6 +227,27 @@ function createApp() {
|
|||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.post('/api/delete', (req, res) => {
|
||||||
|
const ids = Array.isArray(req.body.ids) ? [...new Set(req.body.ids.filter(id => typeof id === 'string'))] : [];
|
||||||
|
if (ids.length === 0) return res.status(400).json({ error: 'ids must be a non-empty string array' });
|
||||||
|
|
||||||
|
const idSet = new Set(ids);
|
||||||
|
const previousData = capturedData;
|
||||||
|
const nextData = capturedData.filter(item => !idSet.has(item.id));
|
||||||
|
const deleted = previousData.length - nextData.length;
|
||||||
|
|
||||||
|
try {
|
||||||
|
capturedData = nextData;
|
||||||
|
rewriteCaptureFile();
|
||||||
|
broadcastData();
|
||||||
|
res.json({ success: true, deleted });
|
||||||
|
} catch (e) {
|
||||||
|
capturedData = previousData;
|
||||||
|
console.error('Failed to persist deleted captures:', e.message);
|
||||||
|
res.status(500).json({ error: 'Failed to persist deleted captures' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// 请求重放端点
|
// 请求重放端点
|
||||||
app.post('/api/replay', async (req, res) => {
|
app.post('/api/replay', async (req, res) => {
|
||||||
const { path: apiPath, method, params, crypto, rawPath, requestHeaders } = req.body;
|
const { path: apiPath, method, params, crypto, rawPath, requestHeaders } = req.body;
|
||||||
@ -216,7 +257,10 @@ function createApp() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await replayRequest({ path: apiPath, method, params, crypto, rawPath, requestHeaders });
|
if (!['eapi', 'linuxapi', 'api'].includes(crypto)) {
|
||||||
|
return res.status(400).json({ error: `Unsupported replay crypto: ${crypto || 'unknown'}` });
|
||||||
|
}
|
||||||
|
const result = ensureCaptureId(await replayRequest({ path: apiPath, method, params, crypto, rawPath, requestHeaders }));
|
||||||
// 将重放结果也加入抓包列表
|
// 将重放结果也加入抓包列表
|
||||||
capturedData.push(result);
|
capturedData.push(result);
|
||||||
appendToFile(result);
|
appendToFile(result);
|
||||||
|
|||||||
@ -6,7 +6,6 @@
|
|||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
<meta name="theme-color" content="#171717">
|
<meta name="theme-color" content="#171717">
|
||||||
<meta name="color-scheme" content="dark">
|
<meta name="color-scheme" content="dark">
|
||||||
<link rel="manifest" href="/manifest.json">
|
|
||||||
<title>NCM CLAWER · 网易云音乐抓包工具</title>
|
<title>NCM CLAWER · 网易云音乐抓包工具</title>
|
||||||
<style>
|
<style>
|
||||||
/* =====================================================================
|
/* =====================================================================
|
||||||
@ -871,6 +870,12 @@
|
|||||||
.toolbar .search-wrap { min-width: 100%; order: -1; }
|
.toolbar .search-wrap { min-width: 100%; order: -1; }
|
||||||
.topbar-actions { gap: 8px; }
|
.topbar-actions { gap: 8px; }
|
||||||
.sb-stats { display: none; }
|
.sb-stats { display: none; }
|
||||||
|
.list-head,
|
||||||
|
.list-card {
|
||||||
|
grid-template-columns: var(--col-check) var(--col-method) minmax(0, 1fr) var(--col-status) var(--col-del);
|
||||||
|
}
|
||||||
|
.list-head > :nth-child(5), .list-head > :nth-child(6), .list-head > :nth-child(7),
|
||||||
|
.list-card > :nth-child(5), .list-card > :nth-child(6), .list-card > :nth-child(7) { display: none; }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
@ -895,7 +900,7 @@
|
|||||||
<span class="badge badge-primary" id="count-badge">0 REC</span>
|
<span class="badge badge-primary" id="count-badge">0 REC</span>
|
||||||
<button class="btn btn-outline btn-sm" id="full-capture-btn" onclick="toggleFullCapture()" title="切换完整抓包模式(捕获全部域名流量)">完整抓包</button>
|
<button class="btn btn-outline btn-sm" id="full-capture-btn" onclick="toggleFullCapture()" title="切换完整抓包模式(捕获全部域名流量)">完整抓包</button>
|
||||||
<button class="btn btn-outline btn-sm" id="auto-scroll-btn" onclick="toggleAutoScroll()" title="自动滚动到新数据">自动滚动</button>
|
<button class="btn btn-outline btn-sm" id="auto-scroll-btn" onclick="toggleAutoScroll()" title="自动滚动到新数据">自动滚动</button>
|
||||||
<button class="btn btn-outline btn-icon-sm" id="drawer-open-btn" title="抓包列表" aria-label="打开抓包列表">☰</button>
|
<button class="btn btn-outline btn-icon-sm" id="drawer-open-btn" title="抓包列表" aria-label="打开抓包列表" aria-controls="list-panel" aria-expanded="false">☰</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@ -961,7 +966,7 @@
|
|||||||
|
|
||||||
<!-- 工作区:左表格列表 + 可拖拽分隔条 + 右详情 -->
|
<!-- 工作区:左表格列表 + 可拖拽分隔条 + 右详情 -->
|
||||||
<section class="workspace">
|
<section class="workspace">
|
||||||
<div class="list-panel" id="list-panel">
|
<div class="list-panel" id="list-panel" aria-label="抓包记录列表">
|
||||||
<div class="list-head">
|
<div class="list-head">
|
||||||
<span class="col" style="grid-column:1;"></span>
|
<span class="col" style="grid-column:1;"></span>
|
||||||
<span class="col" data-col="method">METHOD<span class="col-resizer" data-col="method" title="拖拽调整宽度,双击恢复默认"></span></span>
|
<span class="col" data-col="method">METHOD<span class="col-resizer" data-col="method" title="拖拽调整宽度,双击恢复默认"></span></span>
|
||||||
@ -998,8 +1003,8 @@
|
|||||||
<div class="toast-container" id="toast-container"></div>
|
<div class="toast-container" id="toast-container"></div>
|
||||||
|
|
||||||
<!-- ======================== 确认 Modal ======================== -->
|
<!-- ======================== 确认 Modal ======================== -->
|
||||||
<div class="modal-overlay" id="modal-overlay">
|
<div class="modal-overlay" id="modal-overlay" aria-hidden="true">
|
||||||
<div class="modal">
|
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="modal-title">
|
||||||
<h3 class="modal-title" id="modal-title">确认操作</h3>
|
<h3 class="modal-title" id="modal-title">确认操作</h3>
|
||||||
<p class="modal-message" id="modal-message">确定要执行此操作吗?</p>
|
<p class="modal-message" id="modal-message">确定要执行此操作吗?</p>
|
||||||
<div class="modal-actions">
|
<div class="modal-actions">
|
||||||
@ -1010,8 +1015,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ======================== 重放 Modal ======================== -->
|
<!-- ======================== 重放 Modal ======================== -->
|
||||||
<div class="modal-overlay" id="replay-modal">
|
<div class="modal-overlay" id="replay-modal" aria-hidden="true">
|
||||||
<div class="modal modal-wide">
|
<div class="modal modal-wide" role="dialog" aria-modal="true" aria-labelledby="replay-title">
|
||||||
<h3 class="modal-title" id="replay-title">重放请求</h3>
|
<h3 class="modal-title" id="replay-title">重放请求</h3>
|
||||||
<div class="dh-meta" style="margin-bottom:12px;">
|
<div class="dh-meta" style="margin-bottom:12px;">
|
||||||
<span class="badge badge-secondary" id="replay-method">POST</span>
|
<span class="badge badge-secondary" id="replay-method">POST</span>
|
||||||
@ -1032,7 +1037,7 @@
|
|||||||
<script>
|
<script>
|
||||||
// ======================== State ========================
|
// ======================== State ========================
|
||||||
let capturedData = [];
|
let capturedData = [];
|
||||||
let selectedIndex = -1;
|
let selectedId = null;
|
||||||
let searchQuery = '';
|
let searchQuery = '';
|
||||||
var filterMethod = '';
|
var filterMethod = '';
|
||||||
var filterStatus = '';
|
var filterStatus = '';
|
||||||
@ -1046,6 +1051,7 @@
|
|||||||
let exportFormat = 'json';
|
let exportFormat = 'json';
|
||||||
let activeTab = 'params';
|
let activeTab = 'params';
|
||||||
let showRawPath = false;
|
let showRawPath = false;
|
||||||
|
let lastFocusedElement = null;
|
||||||
|
|
||||||
// ======================== 工具函数 ========================
|
// ======================== 工具函数 ========================
|
||||||
function escapeHtml(text) {
|
function escapeHtml(text) {
|
||||||
@ -1057,6 +1063,15 @@
|
|||||||
try { return JSON.stringify(obj, null, 2); }
|
try { return JSON.stringify(obj, null, 2); }
|
||||||
catch (e) { return String(obj); }
|
catch (e) { return String(obj); }
|
||||||
}
|
}
|
||||||
|
function escapeJson(obj) { return escapeHtml(formatJson(obj)); }
|
||||||
|
function getItemById(id) { return capturedData.find(item => item.id === id); }
|
||||||
|
function getIndexById(id) { return capturedData.findIndex(item => item.id === id); }
|
||||||
|
function checkResponse(response) {
|
||||||
|
if (response.ok) return response;
|
||||||
|
return response.json().catch(() => ({})).then(data => {
|
||||||
|
throw new Error(data.error || ('HTTP ' + response.status));
|
||||||
|
});
|
||||||
|
}
|
||||||
function countKeys(obj) {
|
function countKeys(obj) {
|
||||||
if (!obj || typeof obj !== 'object') return 0;
|
if (!obj || typeof obj !== 'object') return 0;
|
||||||
return Object.keys(obj).length;
|
return Object.keys(obj).length;
|
||||||
@ -1074,6 +1089,9 @@
|
|||||||
try { return new Date(timestamp).toLocaleString('zh-CN', { hour12: false }); }
|
try { return new Date(timestamp).toLocaleString('zh-CN', { hour12: false }); }
|
||||||
catch (e) { return timestamp; }
|
catch (e) { return timestamp; }
|
||||||
}
|
}
|
||||||
|
function includesQuery(value, query) {
|
||||||
|
return value != null && String(value).toLowerCase().includes(query);
|
||||||
|
}
|
||||||
function filterParam(param) {
|
function filterParam(param) {
|
||||||
if (!param || typeof param !== 'object') return param;
|
if (!param || typeof param !== 'object') return param;
|
||||||
const filtered = {};
|
const filtered = {};
|
||||||
@ -1108,9 +1126,14 @@
|
|||||||
confirmBtn.parentNode.replaceChild(newBtn, confirmBtn);
|
confirmBtn.parentNode.replaceChild(newBtn, confirmBtn);
|
||||||
newBtn.addEventListener('click', () => { closeModal(); if (onConfirm) onConfirm(); });
|
newBtn.addEventListener('click', () => { closeModal(); if (onConfirm) onConfirm(); });
|
||||||
overlay.classList.add('show');
|
overlay.classList.add('show');
|
||||||
|
overlay.setAttribute('aria-hidden', 'false');
|
||||||
|
lastFocusedElement = document.activeElement;
|
||||||
|
newBtn.focus();
|
||||||
}
|
}
|
||||||
function closeModal() {
|
function closeModal() {
|
||||||
document.getElementById('modal-overlay').classList.remove('show');
|
document.getElementById('modal-overlay').classList.remove('show');
|
||||||
|
document.getElementById('modal-overlay').setAttribute('aria-hidden', 'true');
|
||||||
|
if (lastFocusedElement) lastFocusedElement.focus();
|
||||||
}
|
}
|
||||||
document.getElementById('modal-overlay').addEventListener('click', (e) => {
|
document.getElementById('modal-overlay').addEventListener('click', (e) => {
|
||||||
if (e.target.id === 'modal-overlay') closeModal();
|
if (e.target.id === 'modal-overlay') closeModal();
|
||||||
@ -1126,6 +1149,8 @@
|
|||||||
const isOpen = force !== undefined ? force : !panel.classList.contains('open');
|
const isOpen = force !== undefined ? force : !panel.classList.contains('open');
|
||||||
panel.classList.toggle('open', isOpen);
|
panel.classList.toggle('open', isOpen);
|
||||||
backdrop.classList.toggle('show', isOpen);
|
backdrop.classList.toggle('show', isOpen);
|
||||||
|
document.getElementById('drawer-open-btn').setAttribute('aria-expanded', String(isOpen));
|
||||||
|
if (isOpen) document.getElementById('drawer-close-btn').focus();
|
||||||
}
|
}
|
||||||
function isMobile() {
|
function isMobile() {
|
||||||
return window.matchMedia('(max-width: 767px)').matches;
|
return window.matchMedia('(max-width: 767px)').matches;
|
||||||
@ -1143,13 +1168,13 @@
|
|||||||
function matchesFilters(item) {
|
function matchesFilters(item) {
|
||||||
if (searchQuery) {
|
if (searchQuery) {
|
||||||
const q = searchQuery;
|
const q = searchQuery;
|
||||||
if ((item.path || '').toLowerCase().includes(q)) return true;
|
if (includesQuery(item.path, q)) return true;
|
||||||
if ((item.hostname || '').toLowerCase().includes(q)) return true;
|
if (includesQuery(item.hostname, q)) return true;
|
||||||
if ((item.rawPath || '').toLowerCase().includes(q)) return true;
|
if (includesQuery(item.rawPath, q)) return true;
|
||||||
if ((item.crypto || '').toLowerCase().includes(q)) return true;
|
if (includesQuery(item.crypto, q)) return true;
|
||||||
if ((item.method || '').toLowerCase().includes(q)) return true;
|
if (includesQuery(item.method, q)) return true;
|
||||||
if (String(item.statusCode || '').includes(q)) return true;
|
if (String(item.statusCode || '').includes(q)) return true;
|
||||||
if ((item.error || '').toLowerCase().includes(q)) return true;
|
if (includesQuery(item.error, q)) return true;
|
||||||
const objFields = ['param', 'response', 'requestHeaders', 'responseHeaders'];
|
const objFields = ['param', 'response', 'requestHeaders', 'responseHeaders'];
|
||||||
for (const key of objFields) {
|
for (const key of objFields) {
|
||||||
const val = item[key];
|
const val = item[key];
|
||||||
@ -1157,7 +1182,7 @@
|
|||||||
try { if (JSON.stringify(val).toLowerCase().includes(q)) return true; } catch (_) {}
|
try { if (JSON.stringify(val).toLowerCase().includes(q)) return true; } catch (_) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (item.responseBody && item.responseBody.toLowerCase().includes(q)) return true;
|
if (includesQuery(item.responseBody, q)) return true;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@ -1198,7 +1223,7 @@
|
|||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ fullCapture: newState }),
|
body: JSON.stringify({ fullCapture: newState }),
|
||||||
})
|
})
|
||||||
.then(r => r.json())
|
.then(checkResponse).then(r => r.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
btn.classList.toggle('active', data.fullCapture);
|
btn.classList.toggle('active', data.fullCapture);
|
||||||
@ -1219,14 +1244,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ======================== 排序 ========================
|
// ======================== 排序 ========================
|
||||||
function setSort(field) {
|
function setSort(field, initialize) {
|
||||||
if (sortBy === field) {
|
if (sortBy === field && !initialize) {
|
||||||
sortAsc = !sortAsc;
|
sortAsc = !sortAsc;
|
||||||
} else {
|
} else {
|
||||||
sortBy = field;
|
sortBy = field;
|
||||||
sortAsc = false;
|
sortAsc = false;
|
||||||
}
|
}
|
||||||
document.querySelectorAll('.filters .chip[data-sort]').forEach(el => {
|
document.querySelectorAll('.filter-group .chip[data-sort]').forEach(el => {
|
||||||
const isActive = el.dataset.sort === sortBy;
|
const isActive = el.dataset.sort === sortBy;
|
||||||
el.classList.toggle('active', isActive);
|
el.classList.toggle('active', isActive);
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
@ -1256,16 +1281,15 @@
|
|||||||
function toggleSelectAll(checked) {
|
function toggleSelectAll(checked) {
|
||||||
const filtered = capturedData.filter(item => matchesFilters(item) && matchesFiltersStrict(item));
|
const filtered = capturedData.filter(item => matchesFilters(item) && matchesFiltersStrict(item));
|
||||||
filtered.forEach(item => {
|
filtered.forEach(item => {
|
||||||
const idx = capturedData.indexOf(item);
|
if (checked) selectedItems.add(item.id);
|
||||||
if (checked) selectedItems.add(idx);
|
else selectedItems.delete(item.id);
|
||||||
else selectedItems.delete(idx);
|
|
||||||
});
|
});
|
||||||
updateBatchUI();
|
updateBatchUI();
|
||||||
renderCaptureList();
|
renderCaptureList();
|
||||||
}
|
}
|
||||||
function toggleItemSelection(index) {
|
function toggleItemSelection(id) {
|
||||||
if (selectedItems.has(index)) selectedItems.delete(index);
|
if (selectedItems.has(id)) selectedItems.delete(id);
|
||||||
else selectedItems.add(index);
|
else selectedItems.add(id);
|
||||||
updateBatchUI();
|
updateBatchUI();
|
||||||
renderCaptureList();
|
renderCaptureList();
|
||||||
}
|
}
|
||||||
@ -1275,26 +1299,19 @@
|
|||||||
const allCheck = document.getElementById('select-all-checkbox');
|
const allCheck = document.getElementById('select-all-checkbox');
|
||||||
if (allCheck) {
|
if (allCheck) {
|
||||||
const filtered = capturedData.filter(item => matchesFilters(item) && matchesFiltersStrict(item));
|
const filtered = capturedData.filter(item => matchesFilters(item) && matchesFiltersStrict(item));
|
||||||
allCheck.checked = filtered.length > 0 && filtered.every(item => selectedItems.has(capturedData.indexOf(item)));
|
allCheck.checked = filtered.length > 0 && filtered.every(item => selectedItems.has(item.id));
|
||||||
allCheck.indeterminate = count > 0 && !allCheck.checked;
|
allCheck.indeterminate = count > 0 && !allCheck.checked;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function batchDelete() {
|
function batchDelete() {
|
||||||
if (selectedItems.size === 0) return showToast('请先选择要删除的记录', 'info');
|
if (selectedItems.size === 0) return showToast('请先选择要删除的记录', 'info');
|
||||||
showConfirm('批量删除', '确定要删除选中的 ' + selectedItems.size + ' 条记录吗?', () => {
|
showConfirm('批量删除', '确定要删除选中的 ' + selectedItems.size + ' 条记录吗?', () => {
|
||||||
const sorted = [...selectedItems].sort((a, b) => b - a);
|
deleteCaptures([...selectedItems]);
|
||||||
sorted.forEach(idx => capturedData.splice(idx, 1));
|
|
||||||
selectedItems.clear();
|
|
||||||
if (selectedIndex >= capturedData.length) selectedIndex = -1;
|
|
||||||
renderCaptureList();
|
|
||||||
updateBatchUI();
|
|
||||||
if (selectedIndex === -1) renderEmptyDetail();
|
|
||||||
showToast('已删除 ' + sorted.length + ' 条记录', 'info');
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function batchExport() {
|
function batchExport() {
|
||||||
if (selectedItems.size === 0) return showToast('请先选择要导出的记录', 'info');
|
if (selectedItems.size === 0) return showToast('请先选择要导出的记录', 'info');
|
||||||
const selected = [...selectedItems].sort((a, b) => a - b).map(idx => capturedData[idx]).filter(Boolean);
|
const selected = capturedData.filter(item => selectedItems.has(item.id));
|
||||||
downloadExport(selected, 'netease-selected');
|
downloadExport(selected, 'netease-selected');
|
||||||
showToast('已导出 ' + selected.length + ' 条记录', 'success');
|
showToast('已导出 ' + selected.length + ' 条记录', 'success');
|
||||||
}
|
}
|
||||||
@ -1306,28 +1323,28 @@
|
|||||||
}
|
}
|
||||||
function getStatusBadge(code) {
|
function getStatusBadge(code) {
|
||||||
if (!code) return '<span class="badge badge-destructive">ERR</span>';
|
if (!code) return '<span class="badge badge-destructive">ERR</span>';
|
||||||
const group = Math.floor(code / 100);
|
const group = Math.floor(Number(code) / 100);
|
||||||
const cls = (group === 4 || group === 5) ? 'badge-destructive' : 'badge-outline';
|
const cls = (group === 4 || group === 5) ? 'badge-destructive' : 'badge-outline';
|
||||||
return '<span class="badge ' + cls + '">' + code + '</span>';
|
return '<span class="badge ' + cls + '">' + escapeHtml(String(code)) + '</span>';
|
||||||
}
|
}
|
||||||
function getCryptoLabel(crypto) {
|
function getCryptoLabel(crypto) {
|
||||||
const names = { eapi: 'EAPI', xeapi: 'XEAPI', linuxapi: 'LINUX', weapi: 'WEAPI', api: 'API' };
|
const names = { eapi: 'EAPI', xeapi: 'XEAPI', linuxapi: 'LINUX', weapi: 'WEAPI', api: 'API' };
|
||||||
const label = names[crypto] || (crypto ? crypto.toUpperCase() : '');
|
const label = names[crypto] || (crypto ? String(crypto).toUpperCase() : '');
|
||||||
return label ? '<span class="badge badge-outline">' + label + '</span>' : '';
|
return label ? '<span class="badge badge-outline">' + escapeHtml(label) + '</span>' : '';
|
||||||
}
|
}
|
||||||
function getCryptoName(crypto) {
|
function getCryptoName(crypto) {
|
||||||
const names = {
|
const names = {
|
||||||
eapi: 'EAPI (AES-128-ECB)', xeapi: 'XEAPI (AES-128-ECB)',
|
eapi: 'EAPI (AES-128-ECB)', xeapi: 'XEAPI (AES-128-ECB)',
|
||||||
linuxapi: 'LinuxAPI (AES-128-ECB)', weapi: 'WEAPI (RSA + AES)', api: 'API (未加密)',
|
linuxapi: 'LinuxAPI (AES-128-ECB)', weapi: 'WEAPI (RSA + AES)', api: 'API (未加密)',
|
||||||
};
|
};
|
||||||
return names[crypto] || crypto || '未知';
|
return names[crypto] || String(crypto || '未知');
|
||||||
}
|
}
|
||||||
function toggleCryptoDisplay(index, el) {
|
function toggleCryptoDisplay(index, el) {
|
||||||
const item = capturedData[index];
|
const item = capturedData[index];
|
||||||
if (!item || !el) return;
|
if (!item || !el) return;
|
||||||
el.dataset.expanded = el.dataset.expanded === '1' ? '0' : '1';
|
el.dataset.expanded = el.dataset.expanded === '1' ? '0' : '1';
|
||||||
const expanded = el.dataset.expanded === '1';
|
const expanded = el.dataset.expanded === '1';
|
||||||
el.innerHTML = expanded ? '<span class="badge badge-outline">' + getCryptoName(item.crypto) + '</span>'
|
el.innerHTML = expanded ? '<span class="badge badge-outline">' + escapeHtml(getCryptoName(item.crypto)) + '</span>'
|
||||||
: getCryptoLabel(item.crypto);
|
: getCryptoLabel(item.crypto);
|
||||||
el.title = expanded ? '点击切换简写' : '点击切换完整名称';
|
el.title = expanded ? '点击切换简写' : '点击切换完整名称';
|
||||||
}
|
}
|
||||||
@ -1359,8 +1376,8 @@
|
|||||||
|
|
||||||
container.innerHTML = sorted.map((item) => {
|
container.innerHTML = sorted.map((item) => {
|
||||||
const realIdx = capturedData.indexOf(item);
|
const realIdx = capturedData.indexOf(item);
|
||||||
const isActive = realIdx === selectedIndex;
|
const isActive = item.id === selectedId;
|
||||||
const isChecked = selectedItems.has(realIdx);
|
const isChecked = selectedItems.has(item.id);
|
||||||
const cryptoLabel = item.crypto
|
const cryptoLabel = item.crypto
|
||||||
? `<span class="crypto-toggle" onclick="event.stopPropagation();toggleCryptoDisplay(${realIdx}, this)" title="点击切换完整名称">${getCryptoLabel(item.crypto)}</span>`
|
? `<span class="crypto-toggle" onclick="event.stopPropagation();toggleCryptoDisplay(${realIdx}, this)" title="点击切换完整名称">${getCryptoLabel(item.crypto)}</span>`
|
||||||
: '';
|
: '';
|
||||||
@ -1376,18 +1393,18 @@
|
|||||||
: (item.hostname ? item.hostname + (item.path ? item.path.replace(/^https?:\/\/[^/]+/, '') : '') : (item.path || 'Unknown'));
|
: (item.hostname ? item.hostname + (item.path ? item.path.replace(/^https?:\/\/[^/]+/, '') : '') : (item.path || 'Unknown'));
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="list-card ${isActive ? 'active' : ''}"
|
<div class="list-card ${isActive ? 'active' : ''}" role="button" tabindex="0" aria-selected="${isActive}"
|
||||||
data-index="${realIdx}"
|
data-index="${realIdx}"
|
||||||
onclick="selectCapture(${realIdx})">
|
onclick="selectCapture(${realIdx})" onkeydown="if(event.target===event.currentTarget&&(event.key==='Enter'||event.key===' ')){event.preventDefault();selectCapture(${realIdx})}">
|
||||||
<input type="checkbox" class="checkbox" ${isChecked ? 'checked' : ''}
|
<input type="checkbox" class="checkbox" ${isChecked ? 'checked' : ''}
|
||||||
onclick="event.stopPropagation();toggleItemSelection(${realIdx})"
|
onclick="event.stopPropagation();toggleItemSelection('${item.id}')"
|
||||||
aria-label="选择记录">
|
aria-label="选择记录">
|
||||||
${methodBadge}
|
${methodBadge}
|
||||||
<span class="col col-path" title="${escapeHtml(displayPath)}">${replayMark}${escapeHtml(displayPath)}</span>
|
<span class="col col-path" title="${escapeHtml(displayPath)}">${replayMark}${escapeHtml(displayPath)}</span>
|
||||||
${statusBadge}
|
${statusBadge}
|
||||||
${cryptoLabel}
|
${cryptoLabel}
|
||||||
<span class="badge ${durCls}" title="耗时">${durText}</span>
|
<span class="badge ${durCls}" title="耗时">${escapeHtml(String(durText))}</span>
|
||||||
<span class="col col-time">${time}</span>
|
<span class="col col-time">${escapeHtml(String(time))}</span>
|
||||||
<button class="lc-del" onclick="event.stopPropagation();deleteItem(${realIdx})" title="删除">×</button>
|
<button class="lc-del" onclick="event.stopPropagation();deleteItem(${realIdx})" title="删除">×</button>
|
||||||
</div>`;
|
</div>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
@ -1415,8 +1432,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function selectCapture(index, instant) {
|
function selectCapture(index, instant) {
|
||||||
selectedIndex = index;
|
|
||||||
const item = capturedData[index];
|
const item = capturedData[index];
|
||||||
|
selectedId = item ? item.id : null;
|
||||||
activeTab = 'params';
|
activeTab = 'params';
|
||||||
showRawPath = false;
|
showRawPath = false;
|
||||||
|
|
||||||
@ -1439,7 +1456,7 @@
|
|||||||
function renderDetail(item, index) {
|
function renderDetail(item, index) {
|
||||||
const panel = document.getElementById('detail-panel');
|
const panel = document.getElementById('detail-panel');
|
||||||
const cryptoLabel = getCryptoLabel(item.crypto);
|
const cryptoLabel = getCryptoLabel(item.crypto);
|
||||||
const cryptoName = item.crypto ? getCryptoName(item.crypto) : '';
|
const cryptoName = item.crypto ? escapeHtml(getCryptoName(item.crypto)) : '';
|
||||||
const methodBadge = getMethodBadge(item.method);
|
const methodBadge = getMethodBadge(item.method);
|
||||||
const statusBadge = getStatusBadge(item.statusCode);
|
const statusBadge = getStatusBadge(item.statusCode);
|
||||||
const fullDate = formatFullDate(item.timestamp);
|
const fullDate = formatFullDate(item.timestamp);
|
||||||
@ -1473,9 +1490,9 @@
|
|||||||
${statusBadge}
|
${statusBadge}
|
||||||
${cryptoLabel}
|
${cryptoLabel}
|
||||||
${neteaseBadge}
|
${neteaseBadge}
|
||||||
<span class="badge ${durCls}">${durText}</span>
|
<span class="badge ${durCls}">${escapeHtml(String(durText))}</span>
|
||||||
<span class="dim">${cryptoName}</span>
|
<span class="dim">${cryptoName}</span>
|
||||||
<span class="dim">${fullDate}</span>
|
<span class="dim">${escapeHtml(String(fullDate))}</span>
|
||||||
${detailHost ? `<span class="dim">${detailHost}</span>` : ''}
|
${detailHost ? `<span class="dim">${detailHost}</span>` : ''}
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@ -1487,7 +1504,7 @@
|
|||||||
<button class="btn btn-outline btn-sm" onclick="copyResponse(${index})">复制响应</button>
|
<button class="btn btn-outline btn-sm" onclick="copyResponse(${index})">复制响应</button>
|
||||||
<button class="btn btn-outline btn-sm" onclick="copyHeaders(${index}, 'req')">请求头</button>
|
<button class="btn btn-outline btn-sm" onclick="copyHeaders(${index}, 'req')">请求头</button>
|
||||||
<button class="btn btn-outline btn-sm" onclick="copyHeaders(${index}, 'res')">响应头</button>
|
<button class="btn btn-outline btn-sm" onclick="copyHeaders(${index}, 'res')">响应头</button>
|
||||||
${item.isNetease ? `<button class="btn btn-default btn-sm" onclick="replayCapture(${index})">重放</button>` : ''}
|
${item.isNetease && ['eapi', 'linuxapi', 'api'].includes(item.crypto) ? `<button class="btn btn-default btn-sm" onclick="replayCapture(${index})">重放</button>` : ''}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav class="tabs" aria-label="详情标签">
|
<nav class="tabs" aria-label="详情标签">
|
||||||
@ -1530,7 +1547,7 @@
|
|||||||
case 'params':
|
case 'params':
|
||||||
return `
|
return `
|
||||||
<div class="block-label"><strong>请求参数</strong> ${countKeys(filteredParam)} 个字段</div>
|
<div class="block-label"><strong>请求参数</strong> ${countKeys(filteredParam)} 个字段</div>
|
||||||
<pre class="code-block">${formatJson(filteredParam)}</pre>`;
|
<pre class="code-block">${escapeJson(filteredParam)}</pre>`;
|
||||||
case 'reqHeaders':
|
case 'reqHeaders':
|
||||||
return `
|
return `
|
||||||
<div class="block-label"><strong>请求 Headers</strong> ${countKeys(item.requestHeaders)} 个</div>
|
<div class="block-label"><strong>请求 Headers</strong> ${countKeys(item.requestHeaders)} 个</div>
|
||||||
@ -1557,7 +1574,7 @@
|
|||||||
原始格式
|
原始格式
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<pre class="code-block" id="response-content">${formatJson(item.response)}</pre>`;
|
<pre class="code-block" id="response-content">${escapeJson(item.response)}</pre>`;
|
||||||
}
|
}
|
||||||
if (item.responseBody) {
|
if (item.responseBody) {
|
||||||
return `
|
return `
|
||||||
@ -1571,7 +1588,7 @@
|
|||||||
<strong>完整原始数据</strong>
|
<strong>完整原始数据</strong>
|
||||||
<button class="btn btn-outline btn-xs" onclick="copyRaw(${capturedData.indexOf(item)})">复制</button>
|
<button class="btn btn-outline btn-xs" onclick="copyRaw(${capturedData.indexOf(item)})">复制</button>
|
||||||
</div>
|
</div>
|
||||||
<pre class="code-block">${formatJson(item)}</pre>`;
|
<pre class="code-block">${escapeJson(item)}</pre>`;
|
||||||
default:
|
default:
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
@ -1579,8 +1596,8 @@
|
|||||||
|
|
||||||
function switchTab(tab) {
|
function switchTab(tab) {
|
||||||
activeTab = tab;
|
activeTab = tab;
|
||||||
const item = capturedData[selectedIndex];
|
const item = getItemById(selectedId);
|
||||||
if (item) renderDetail(item, selectedIndex);
|
if (item) renderDetail(item, getIndexById(selectedId));
|
||||||
}
|
}
|
||||||
function togglePathDisplay(index) {
|
function togglePathDisplay(index) {
|
||||||
showRawPath = !showRawPath;
|
showRawPath = !showRawPath;
|
||||||
@ -1597,9 +1614,7 @@
|
|||||||
|
|
||||||
// ======================== 复制 ========================
|
// ======================== 复制 ========================
|
||||||
function copyToClipboard(text, label) {
|
function copyToClipboard(text, label) {
|
||||||
navigator.clipboard.writeText(text).then(() => {
|
const fallback = () => {
|
||||||
showToast(label || '已复制', 'success');
|
|
||||||
}).catch(() => {
|
|
||||||
const ta = document.createElement('textarea');
|
const ta = document.createElement('textarea');
|
||||||
ta.value = text;
|
ta.value = text;
|
||||||
ta.style.position = 'fixed';
|
ta.style.position = 'fixed';
|
||||||
@ -1607,13 +1622,18 @@
|
|||||||
document.body.appendChild(ta);
|
document.body.appendChild(ta);
|
||||||
ta.select();
|
ta.select();
|
||||||
try {
|
try {
|
||||||
document.execCommand('copy');
|
if (!document.execCommand('copy')) throw new Error('copy rejected');
|
||||||
showToast(label || '已复制', 'success');
|
showToast(label || '已复制', 'success');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showToast('复制失败,请手动复制', 'error');
|
showToast('复制失败,请手动复制', 'error');
|
||||||
}
|
} finally {
|
||||||
document.body.removeChild(ta);
|
document.body.removeChild(ta);
|
||||||
});
|
}
|
||||||
|
};
|
||||||
|
if (!navigator.clipboard || typeof navigator.clipboard.writeText !== 'function') return fallback();
|
||||||
|
navigator.clipboard.writeText(text).then(() => {
|
||||||
|
showToast(label || '已复制', 'success');
|
||||||
|
}).catch(fallback);
|
||||||
}
|
}
|
||||||
function copyPath(index) {
|
function copyPath(index) {
|
||||||
const item = capturedData[index];
|
const item = capturedData[index];
|
||||||
@ -1629,8 +1649,8 @@
|
|||||||
}
|
}
|
||||||
function copyResponse(index) {
|
function copyResponse(index) {
|
||||||
const item = capturedData[index];
|
const item = capturedData[index];
|
||||||
if (!item || !item.response) return showToast('无响应数据可复制', 'error');
|
if (!item || (item.response == null && item.responseBody == null)) return showToast('无响应数据可复制', 'error');
|
||||||
copyToClipboard(formatJson(item.response), '响应已复制');
|
copyToClipboard(item.response != null ? formatJson(item.response) : String(item.responseBody), '响应已复制');
|
||||||
}
|
}
|
||||||
function copyRaw(index) {
|
function copyRaw(index) {
|
||||||
const item = capturedData[index];
|
const item = capturedData[index];
|
||||||
@ -1652,7 +1672,8 @@
|
|||||||
if (!item) return showToast('数据不完整', 'error');
|
if (!item) return showToast('数据不完整', 'error');
|
||||||
const method = (item.method || 'POST').toUpperCase();
|
const method = (item.method || 'POST').toUpperCase();
|
||||||
let url = item.isNetease ? ('https://music.163.com' + (getDisplayPath(item) || '')) : (item.path || '');
|
let url = item.isNetease ? ('https://music.163.com' + (getDisplayPath(item) || '')) : (item.path || '');
|
||||||
let curl = "curl -X " + method + " '" + url + "'";
|
if (!/^https?:\/\//i.test(url) && item.hostname) url = 'https://' + item.hostname + (url.startsWith('/') ? url : '/' + url);
|
||||||
|
let curl = "curl -X " + method + " '" + url.replace(/'/g, "'\\''") + "'";
|
||||||
if (item.requestHeaders) {
|
if (item.requestHeaders) {
|
||||||
Object.entries(item.requestHeaders).forEach(([k, v]) => {
|
Object.entries(item.requestHeaders).forEach(([k, v]) => {
|
||||||
if (['host', 'content-length', 'proxy-connection'].includes(k.toLowerCase())) return;
|
if (['host', 'content-length', 'proxy-connection'].includes(k.toLowerCase())) return;
|
||||||
@ -1675,29 +1696,25 @@
|
|||||||
if (!item) return;
|
if (!item) return;
|
||||||
const path = item.path || 'Unknown';
|
const path = item.path || 'Unknown';
|
||||||
showConfirm('删除抓包记录', '确定要删除 "' + path + '" 的抓包记录吗?', () => {
|
showConfirm('删除抓包记录', '确定要删除 "' + path + '" 的抓包记录吗?', () => {
|
||||||
capturedData.splice(index, 1);
|
deleteCaptures([item.id]);
|
||||||
if (selectedIndex === index) {
|
|
||||||
selectedIndex = -1;
|
|
||||||
renderEmptyDetail();
|
|
||||||
} else if (selectedIndex > index) {
|
|
||||||
selectedIndex--;
|
|
||||||
}
|
|
||||||
renderCaptureList();
|
|
||||||
showToast('已删除该记录', 'info');
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
function deleteCaptures(ids) {
|
||||||
|
fetch('/api/delete', {
|
||||||
|
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ids })
|
||||||
|
}).then(checkResponse).then(r => r.json()).then(data => {
|
||||||
|
ids.forEach(id => selectedItems.delete(id));
|
||||||
|
if (ids.includes(selectedId)) { selectedId = null; renderEmptyDetail(); }
|
||||||
|
showToast('已永久删除 ' + data.deleted + ' 条记录', 'info');
|
||||||
|
}).catch(e => showToast('删除失败: ' + e.message, 'error'));
|
||||||
|
}
|
||||||
function confirmClear() {
|
function confirmClear() {
|
||||||
if (!capturedData || capturedData.length === 0) return showToast('暂无数据可清空', 'info');
|
if (!capturedData || capturedData.length === 0) return showToast('暂无数据可清空', 'info');
|
||||||
showConfirm('清空所有数据', '确定要清空全部 ' + capturedData.length + ' 条抓包记录吗?此操作不可撤销。', () => {
|
showConfirm('清空所有数据', '确定要清空全部 ' + capturedData.length + ' 条抓包记录吗?此操作不可撤销。', () => {
|
||||||
capturedData = [];
|
|
||||||
selectedIndex = -1;
|
|
||||||
selectedItems.clear();
|
|
||||||
renderCaptureList();
|
|
||||||
renderEmptyDetail();
|
|
||||||
fetch('/api/clear', { method: 'POST' })
|
fetch('/api/clear', { method: 'POST' })
|
||||||
.then(r => r.json())
|
.then(checkResponse).then(r => r.json())
|
||||||
.then(d => { if (d.success) showToast('已清空所有数据', 'info'); })
|
.then(d => { if (d.success) { selectedId = null; selectedItems.clear(); renderEmptyDetail(); showToast('已清空所有数据', 'info'); } })
|
||||||
.catch(() => showToast('清空请求失败', 'error'));
|
.catch(e => showToast('清空请求失败: ' + e.message, 'error'));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1720,9 +1737,14 @@
|
|||||||
document.getElementById('replay-crypto').textContent = getCryptoName(item.crypto);
|
document.getElementById('replay-crypto').textContent = getCryptoName(item.crypto);
|
||||||
document.getElementById('replay-params').value = formatJson(replayFormData);
|
document.getElementById('replay-params').value = formatJson(replayFormData);
|
||||||
overlay.classList.add('show');
|
overlay.classList.add('show');
|
||||||
|
overlay.setAttribute('aria-hidden', 'false');
|
||||||
|
lastFocusedElement = document.activeElement;
|
||||||
|
document.getElementById('replay-params').focus();
|
||||||
}
|
}
|
||||||
function closeReplayModal() {
|
function closeReplayModal() {
|
||||||
document.getElementById('replay-modal').classList.remove('show');
|
document.getElementById('replay-modal').classList.remove('show');
|
||||||
|
document.getElementById('replay-modal').setAttribute('aria-hidden', 'true');
|
||||||
|
if (lastFocusedElement) lastFocusedElement.focus();
|
||||||
}
|
}
|
||||||
function executeReplay() {
|
function executeReplay() {
|
||||||
const item = capturedData[replayIndex];
|
const item = capturedData[replayIndex];
|
||||||
@ -1749,7 +1771,7 @@
|
|||||||
requestHeaders: item.requestHeaders,
|
requestHeaders: item.requestHeaders,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.then(r => r.json())
|
.then(checkResponse).then(r => r.json())
|
||||||
.then(result => {
|
.then(result => {
|
||||||
closeReplayModal();
|
closeReplayModal();
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
@ -1861,7 +1883,7 @@
|
|||||||
const status = Object.entries(stats.statusGroups || {}).sort((a, b) => b[1] - a[1]);
|
const status = Object.entries(stats.statusGroups || {}).sort((a, b) => b[1] - a[1]);
|
||||||
const block = (label, arr) => {
|
const block = (label, arr) => {
|
||||||
if (!arr.length) return '';
|
if (!arr.length) return '';
|
||||||
return '<span class="sb-block">' + label + ' <b>' + arr.slice(0, 3).map(([k, v]) => k + '=' + v).join(' · ') + '</b></span>';
|
return '<span class="sb-block">' + escapeHtml(label) + ' <b>' + arr.slice(0, 3).map(([k, v]) => escapeHtml(k) + '=' + Number(v)).join(' · ') + '</b></span>';
|
||||||
};
|
};
|
||||||
el.innerHTML = block('M', methods) + block('E', cryptos) + block('S', status);
|
el.innerHTML = block('M', methods) + block('E', cryptos) + block('S', status);
|
||||||
}
|
}
|
||||||
@ -1998,18 +2020,19 @@
|
|||||||
setConnUI(true);
|
setConnUI(true);
|
||||||
const prevLen = capturedData.length;
|
const prevLen = capturedData.length;
|
||||||
capturedData = data;
|
capturedData = data;
|
||||||
|
selectedItems = new Set([...selectedItems].filter(id => capturedData.some(item => item.id === id)));
|
||||||
renderCaptureList();
|
renderCaptureList();
|
||||||
|
|
||||||
if (autoScroll && data.length > prevLen && data.length > 0) {
|
if (autoScroll && data.length > prevLen && data.length > 0) {
|
||||||
selectCapture(data.length - 1, true);
|
selectCapture(data.length - 1, true);
|
||||||
} else if (selectedIndex === -1 && data.length > 0) {
|
} else if (!selectedId && data.length > 0) {
|
||||||
selectCapture(0);
|
selectCapture(0);
|
||||||
} else if (selectedIndex >= data.length) {
|
} else if (selectedId && !getItemById(selectedId)) {
|
||||||
selectedIndex = -1;
|
selectedId = null;
|
||||||
renderEmptyDetail();
|
renderEmptyDetail();
|
||||||
} else if (selectedIndex >= 0) {
|
} else if (selectedId) {
|
||||||
const item = capturedData[selectedIndex];
|
const item = getItemById(selectedId);
|
||||||
if (item) renderDetail(item, selectedIndex);
|
if (item) renderDetail(item, getIndexById(selectedId));
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('SSE parse error:', e);
|
console.error('SSE parse error:', e);
|
||||||
@ -2044,16 +2067,17 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function registerSW() {
|
function unregisterLegacyServiceWorkers() {
|
||||||
if ('serviceWorker' in navigator) {
|
if ('serviceWorker' in navigator) {
|
||||||
navigator.serviceWorker.register('/sw.js').catch(function(err) {
|
navigator.serviceWorker.getRegistrations()
|
||||||
console.log('ServiceWorker registration failed:', err);
|
.then(registrations => Promise.all(registrations.map(registration => registration.unregister())))
|
||||||
});
|
.catch(() => {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
fetchVersion();
|
fetchVersion();
|
||||||
|
unregisterLegacyServiceWorkers();
|
||||||
fetchStats();
|
fetchStats();
|
||||||
fetchSettings();
|
fetchSettings();
|
||||||
setupSSE();
|
setupSSE();
|
||||||
@ -2061,14 +2085,19 @@
|
|||||||
initColumnResize();
|
initColumnResize();
|
||||||
renderEmptyDetail();
|
renderEmptyDetail();
|
||||||
renderCaptureList();
|
renderCaptureList();
|
||||||
registerSW();
|
|
||||||
setInterval(fetchStats, 5000);
|
setInterval(fetchStats, 5000);
|
||||||
|
|
||||||
document.getElementById('drawer-open-btn').addEventListener('click', () => toggleDrawer(true));
|
document.getElementById('drawer-open-btn').addEventListener('click', () => toggleDrawer(true));
|
||||||
document.getElementById('drawer-close-btn').addEventListener('click', () => toggleDrawer(false));
|
document.getElementById('drawer-close-btn').addEventListener('click', () => toggleDrawer(false));
|
||||||
|
|
||||||
// 初始化排序按钮
|
// 初始化排序按钮
|
||||||
setSort('time');
|
setSort('time', true);
|
||||||
|
});
|
||||||
|
document.addEventListener('keydown', function(event) {
|
||||||
|
if (event.key !== 'Escape') return;
|
||||||
|
if (document.getElementById('replay-modal').classList.contains('show')) closeReplayModal();
|
||||||
|
else if (document.getElementById('modal-overlay').classList.contains('show')) closeModal();
|
||||||
|
else if (document.getElementById('list-panel').classList.contains('open')) toggleDrawer(false);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@ -1,22 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "网易云音乐抓包工具",
|
|
||||||
"short_name": "NCM Clawer",
|
|
||||||
"description": "网易云音乐客户端抓包与调试工具",
|
|
||||||
"start_url": "/",
|
|
||||||
"display": "standalone",
|
|
||||||
"background_color": "#171717",
|
|
||||||
"theme_color": "#171717",
|
|
||||||
"categories": ["developer tools", "utilities"],
|
|
||||||
"icons": [
|
|
||||||
{
|
|
||||||
"src": "/icon-192.png",
|
|
||||||
"sizes": "192x192",
|
|
||||||
"type": "image/png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "/icon-512.png",
|
|
||||||
"sizes": "512x512",
|
|
||||||
"type": "image/png"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
// 无缓存透传 Service Worker
|
|
||||||
// 仅用于 PWA 安装能力,不缓存任何资源
|
|
||||||
|
|
||||||
self.addEventListener('install', function() {
|
|
||||||
// 立即激活,不等待页面关闭
|
|
||||||
self.skipWaiting();
|
|
||||||
});
|
|
||||||
|
|
||||||
self.addEventListener('activate', function() {
|
|
||||||
// 接管所有客户端
|
|
||||||
self.clients.claim();
|
|
||||||
});
|
|
||||||
|
|
||||||
self.addEventListener('fetch', function(event) {
|
|
||||||
// 纯透传:不缓存任何内容,每次都请求网络
|
|
||||||
// 如果网络不可用,请求失败由浏览器自行处理
|
|
||||||
event.respondWith(fetch(event.request));
|
|
||||||
});
|
|
||||||
@ -450,6 +450,7 @@ hook.request.after = (ctx) => {
|
|||||||
duration,
|
duration,
|
||||||
requestHeaders: ctx.requestHeaders,
|
requestHeaders: ctx.requestHeaders,
|
||||||
responseHeaders,
|
responseHeaders,
|
||||||
|
isNetease: true,
|
||||||
};
|
};
|
||||||
axios.post(`http://localhost:${process.env.PORT || 3000}/api/capture`, dataToSend)
|
axios.post(`http://localhost:${process.env.PORT || 3000}/api/capture`, dataToSend)
|
||||||
.catch(err => logger.error('Failed to send data to frontend:', err));
|
.catch(err => logger.error('Failed to send data to frontend:', err));
|
||||||
@ -469,6 +470,7 @@ hook.request.after = (ctx) => {
|
|||||||
duration,
|
duration,
|
||||||
requestHeaders: ctx.requestHeaders,
|
requestHeaders: ctx.requestHeaders,
|
||||||
responseHeaders,
|
responseHeaders,
|
||||||
|
isNetease: true,
|
||||||
};
|
};
|
||||||
axios.post(`http://localhost:${process.env.PORT || 3000}/api/capture`, dataToSend)
|
axios.post(`http://localhost:${process.env.PORT || 3000}/api/capture`, dataToSend)
|
||||||
.catch(err => logger.error('Failed to send data to frontend:', err));
|
.catch(err => logger.error('Failed to send data to frontend:', err));
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user