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.
Files changed (38) hide show
  1. package/COLUMN_RESIZE_IMPLEMENTATION.md +162 -0
  2. package/TEST_GUIDE.md +156 -0
  3. package/VERSION.md +57 -3
  4. package/app/cgi-bin/proxy-fetch.js +117 -0
  5. package/app/cgi-bin/settings.js +37 -0
  6. package/app/cgi-bin/stats.js +149 -0
  7. package/app/public/index.html +1 -1
  8. package/app/public/js/index.js +1 -1
  9. package/app/public/js/index.js.LICENSE.txt +27 -2
  10. package/lib/models.js +9 -0
  11. package/lib/routes/interfaces.js +6 -3
  12. package/lib/routes/settings.js +51 -0
  13. package/lib/ruleManager.js +122 -63
  14. package/lib/server.js +4 -0
  15. package/lib/ui-server.js +20 -4
  16. package/package.json +2 -1
  17. package/ui/src/App.css +1 -1
  18. package/ui/src/components/AppLayout.js +317 -80
  19. package/ui/src/components/FeatureCard.js +7 -2
  20. package/ui/src/components/KeyboardShortcutsHelp.js +58 -0
  21. package/ui/src/components/VersionModal.js +4 -41
  22. package/ui/src/components/interface-management/DataManagementDrawer.js +115 -0
  23. package/ui/src/components/interface-management/PreviewModal.js +4 -6
  24. package/ui/src/components/interface-management/RealResponseRecorder.js +165 -0
  25. package/ui/src/components/interface-management/ResponseContentEditor.js +123 -177
  26. package/ui/src/components/interface-management/index.js +1 -0
  27. package/ui/src/hooks/useKeyboardShortcuts.js +99 -0
  28. package/ui/src/index.html +5 -2
  29. package/ui/src/index.js +12 -2
  30. package/ui/src/pages/InterfaceManagementV2.js +563 -147
  31. package/ui/src/pages/MockDataV2.js +255 -83
  32. package/ui/src/pages/Settings.js +275 -8
  33. package/ui/src/pages/Sniffer.js +521 -0
  34. package/ui/src/pages/Stats.js +288 -0
  35. package/ui/src/styles/cyber-theme.css +1234 -0
  36. package/ui/src/styles/earth-theme.css +618 -0
  37. package/ui/src/styles/interface-management.css +37 -1
  38. 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
+ };
@@ -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 数据工厂</title><script defer="defer" src="/js/index.js"></script></head><body><div id="root"></div></body></html>
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>