whistle.mock-plugins 1.2.2-beta.9 → 1.3.1-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/COLUMN_RESIZE_IMPLEMENTATION.md +162 -0
- package/TEST_GUIDE.md +156 -0
- package/VERSION.md +57 -3
- package/app/cgi-bin/proxy-fetch.js +117 -0
- package/app/cgi-bin/settings.js +37 -0
- package/app/cgi-bin/stats.js +149 -0
- package/app/public/index.html +1 -1
- package/app/public/js/index.js +1 -1
- package/app/public/js/index.js.LICENSE.txt +27 -2
- package/lib/models.js +9 -0
- package/lib/routes/interfaces.js +6 -3
- package/lib/routes/settings.js +51 -0
- package/lib/ruleManager.js +122 -63
- package/lib/server.js +4 -0
- package/lib/ui-server.js +20 -4
- package/package.json +2 -1
- package/ui/src/App.css +1 -1
- package/ui/src/components/AppLayout.js +317 -80
- package/ui/src/components/FeatureCard.js +7 -2
- package/ui/src/components/KeyboardShortcutsHelp.js +58 -0
- package/ui/src/components/VersionModal.js +4 -41
- package/ui/src/components/interface-management/DataManagementDrawer.js +115 -0
- package/ui/src/components/interface-management/PreviewModal.js +4 -6
- package/ui/src/components/interface-management/RealResponseRecorder.js +165 -0
- package/ui/src/components/interface-management/ResponseContentEditor.js +123 -177
- package/ui/src/components/interface-management/index.js +1 -0
- package/ui/src/hooks/useKeyboardShortcuts.js +99 -0
- package/ui/src/index.html +5 -2
- package/ui/src/index.js +12 -2
- package/ui/src/pages/InterfaceManagementV2.js +563 -147
- package/ui/src/pages/MockDataV2.js +255 -83
- package/ui/src/pages/Settings.js +275 -8
- package/ui/src/pages/Sniffer.js +521 -0
- package/ui/src/pages/Stats.js +288 -0
- package/ui/src/styles/cyber-theme.css +1234 -0
- package/ui/src/styles/earth-theme.css +618 -0
- package/ui/src/styles/interface-management.css +37 -1
- package/ui/src/styles/settings.css +45 -1
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const storage = require('../../lib/storage');
|
|
4
|
+
|
|
5
|
+
const DATA_DIR = storage.DATA_DIR;
|
|
6
|
+
const LOGS_FILE = path.join(DATA_DIR, 'logs.json');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 统计接口
|
|
10
|
+
* GET /cgi-bin/stats?type=overview|hits|misses|top
|
|
11
|
+
*/
|
|
12
|
+
module.exports = (req, res) => {
|
|
13
|
+
try {
|
|
14
|
+
const type = req.query.type || 'overview';
|
|
15
|
+
|
|
16
|
+
if (!fs.existsSync(LOGS_FILE)) {
|
|
17
|
+
return res.json({ code: 0, data: { logs: [], stats: {} } });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const logsData = fs.readJsonSync(LOGS_FILE);
|
|
21
|
+
const logs = logsData.logs || [];
|
|
22
|
+
|
|
23
|
+
// 只取最近 7 天的日志做统计(避免数据量过大)
|
|
24
|
+
const now = new Date();
|
|
25
|
+
const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
26
|
+
const recentLogs = logs.filter(log => {
|
|
27
|
+
const t = new Date(log.timestamp);
|
|
28
|
+
return t >= sevenDaysAgo;
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// 分离命中和未命中
|
|
32
|
+
const hitLogs = recentLogs.filter(log => log.eventType === 'mock_hit');
|
|
33
|
+
const missLogs = recentLogs.filter(log => log.eventType === 'mock_miss' || log.eventType === 'proxy_pass');
|
|
34
|
+
|
|
35
|
+
let data = {};
|
|
36
|
+
|
|
37
|
+
switch (type) {
|
|
38
|
+
case 'overview': {
|
|
39
|
+
// 接口总数
|
|
40
|
+
const interfacesFile = path.join(DATA_DIR, 'interfaces.json');
|
|
41
|
+
let totalInterfaces = 0;
|
|
42
|
+
let activeInterfaces = 0;
|
|
43
|
+
if (fs.existsSync(interfacesFile)) {
|
|
44
|
+
const intfData = fs.readJsonSync(interfacesFile);
|
|
45
|
+
const list = intfData.data || intfData.interfaces || intfData || [];
|
|
46
|
+
totalInterfaces = Array.isArray(list) ? list.length : 0;
|
|
47
|
+
activeInterfaces = Array.isArray(list) ? list.filter(i => i.active).length : 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
data = {
|
|
51
|
+
totalRequests: recentLogs.length,
|
|
52
|
+
totalHits: hitLogs.length,
|
|
53
|
+
totalMisses: missLogs.length,
|
|
54
|
+
totalInterfaces,
|
|
55
|
+
activeInterfaces,
|
|
56
|
+
hitRate: recentLogs.length > 0 ? ((hitLogs.length / recentLogs.length) * 100).toFixed(1) : 0,
|
|
57
|
+
todayHits: hitLogs.filter(log => {
|
|
58
|
+
const t = new Date(log.timestamp);
|
|
59
|
+
return t.toDateString() === now.toDateString();
|
|
60
|
+
}).length,
|
|
61
|
+
};
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
case 'top': {
|
|
66
|
+
// Top 10 命中接口
|
|
67
|
+
const interfaceHitMap = {};
|
|
68
|
+
hitLogs.forEach(log => {
|
|
69
|
+
const id = log.interfaceId || log.interfaceName || 'unknown';
|
|
70
|
+
const name = log.interfaceName || id;
|
|
71
|
+
if (!interfaceHitMap[id]) {
|
|
72
|
+
interfaceHitMap[id] = { id, name, count: 0 };
|
|
73
|
+
}
|
|
74
|
+
interfaceHitMap[id].count++;
|
|
75
|
+
});
|
|
76
|
+
const topInterfaces = Object.values(interfaceHitMap)
|
|
77
|
+
.sort((a, b) => b.count - a.count)
|
|
78
|
+
.slice(0, 10);
|
|
79
|
+
|
|
80
|
+
// Top 10 命中功能模块
|
|
81
|
+
const featureHitMap = {};
|
|
82
|
+
hitLogs.forEach(log => {
|
|
83
|
+
const id = log.featureName || log.featureId || 'unknown';
|
|
84
|
+
const name = log.featureName || '未分类';
|
|
85
|
+
if (!featureHitMap[id]) {
|
|
86
|
+
featureHitMap[id] = { id, name, count: 0 };
|
|
87
|
+
}
|
|
88
|
+
featureHitMap[id].count++;
|
|
89
|
+
});
|
|
90
|
+
const topFeatures = Object.values(featureHitMap)
|
|
91
|
+
.sort((a, b) => b.count - a.count)
|
|
92
|
+
.slice(0, 10);
|
|
93
|
+
|
|
94
|
+
data = { topInterfaces, topFeatures };
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
case 'timeline': {
|
|
99
|
+
// 最近 7 天每天的命中数
|
|
100
|
+
const timeline = {};
|
|
101
|
+
for (let i = 6; i >= 0; i--) {
|
|
102
|
+
const d = new Date(now.getTime() - i * 24 * 60 * 60 * 1000);
|
|
103
|
+
const key = d.toISOString().slice(0, 10);
|
|
104
|
+
timeline[key] = 0;
|
|
105
|
+
}
|
|
106
|
+
hitLogs.forEach(log => {
|
|
107
|
+
const key = new Date(log.timestamp).toISOString().slice(0, 10);
|
|
108
|
+
if (timeline[key] !== undefined) {
|
|
109
|
+
timeline[key]++;
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
data = { timeline };
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
case 'recent': {
|
|
117
|
+
let filteredLogs = hitLogs;
|
|
118
|
+
const filterDate = req.query.date;
|
|
119
|
+
const filterUrl = req.query.url;
|
|
120
|
+
|
|
121
|
+
if (filterDate) {
|
|
122
|
+
filteredLogs = filteredLogs.filter(log => {
|
|
123
|
+
const logDate = new Date(log.timestamp).toISOString().slice(0, 10);
|
|
124
|
+
return logDate === filterDate;
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (filterUrl) {
|
|
129
|
+
const lowerUrl = filterUrl.toLowerCase();
|
|
130
|
+
filteredLogs = filteredLogs.filter(log => {
|
|
131
|
+
const logUrl = (log.url || '').toLowerCase();
|
|
132
|
+
return logUrl.includes(lowerUrl);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
data = { logs: filteredLogs.slice(0, 20) };
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
default:
|
|
141
|
+
data = {};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
res.json({ code: 0, data });
|
|
145
|
+
} catch (err) {
|
|
146
|
+
console.error('Stats error:', err);
|
|
147
|
+
res.status(500).json({ code: 500, message: err.message });
|
|
148
|
+
}
|
|
149
|
+
};
|
package/app/public/index.html
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
<!doctype html><html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Whistle Mock
|
|
1
|
+
<!doctype html><html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Whistle Mock Plugin</title><link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;700;900&family=Orbitron:wght@400;500;600;700;800;900&family=Share+Tech+Mono&display=swap" rel="stylesheet"><script defer="defer" src="/js/index.js"></script></head><body><div id="root"></div></body></html>
|