whistle.mock-plugins 1.2.2-beta.23 → 1.2.2-beta.28
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/VERSION.md +78 -2
- package/app/cgi-bin/proxy-fetch.js +117 -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 +18 -0
- package/lib/ruleManager.js +27 -0
- package/lib/ui-server.js +20 -4
- package/package.json +1 -1
- package/ui/src/App.css +1 -1
- package/ui/src/components/AppLayout.js +213 -79
- package/ui/src/components/FeatureCard.js +1 -1
- package/ui/src/components/KeyboardShortcutsHelp.js +58 -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 +4 -7
- package/ui/src/hooks/useKeyboardShortcuts.js +99 -0
- package/ui/src/index.html +5 -2
- package/ui/src/index.js +9 -2
- package/ui/src/pages/InterfaceManagementV2.js +29 -6
- package/ui/src/pages/MockDataV2.js +20 -12
- package/ui/src/pages/Settings.js +35 -1
- 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/VERSION.md
CHANGED
|
@@ -1,5 +1,70 @@
|
|
|
1
1
|
# 🔄 版本更新记录
|
|
2
|
+
|
|
3
|
+
## v1.2.2-beta.28
|
|
4
|
+
|
|
5
|
+
### ✨ Feature
|
|
6
|
+
- **请求统计-最近命中记录增强**:增加"模块"列展示命中接口所属功能模块;去掉冗余的"响应"列
|
|
7
|
+
- **Top 10 功能模块命中**:原"Top 10 未命中 URL"(实际无数据)改为"Top 10 功能模块命中排行",按模块聚合命中次数
|
|
8
|
+
- **最近命中记录筛选**:去掉分页,固定展示 20 条;新增日期选择器和 URL 关键字输入框,支持按日期精确筛选和 URL 模糊匹配
|
|
9
|
+
|
|
10
|
+
### 🎨 UI
|
|
11
|
+
- **亮色主题颜色优化**:将组件中硬编码的刺眼霓虹绿 `#00ff41` 替换为标准柔和绿 `#52c41a`,在亮色主题下视觉更舒适(赛博朋克主题仍通过 CSS 覆盖保持霓虹效果)
|
|
12
|
+
- **去掉 V2 标签**:功能模块管理页面右上角 "V2" 标签移除
|
|
13
|
+
|
|
14
|
+
### 🔧 Technical
|
|
15
|
+
- **后端日志字段扩展**:`ruleManager.js` 记录 `mock_hit` 日志时新增 `featureName`(功能模块名称)和 `responseName`(命中响应名称)字段
|
|
16
|
+
- **stats API 增强**:`cgi-bin/stats?type=recent` 支持 `date`(YYYY-MM-DD)和 `url`(模糊匹配)查询参数过滤
|
|
17
|
+
|
|
18
|
+
## v1.2.2-beta.26
|
|
19
|
+
|
|
20
|
+
### ✨ Feature
|
|
21
|
+
- **主题切换免刷新**:关闭 Settings 中的强制刷新逻辑,主题切换通过 `storage` 事件 + `body.cyber-theme` class 动态切换即时生效
|
|
22
|
+
- **请求统计面板**:新增「请求统计」页面(Ctrl+4),展示近7天命中趋势、Top 10 命中接口、Top 10 未命中 URL、最近命中记录
|
|
23
|
+
- **从真实接口导入**:接口编辑表单新增「从真实接口导入」区域,输入真实 API URL 后一键抓取响应,自动生成 Mock.js 模板并应用到当前响应
|
|
24
|
+
- **全局键盘快捷键**:
|
|
25
|
+
- `Ctrl+1/2/3/4` — 快速切换功能模块/接口管理/系统设置/请求统计
|
|
26
|
+
- `Ctrl+S` — 保存当前表单(Modal/Drawer 打开时)
|
|
27
|
+
- `Ctrl+Shift+N` — 新建接口/功能模块
|
|
28
|
+
- `Ctrl+K` — 聚焦搜索框
|
|
29
|
+
- `Shift+?` — 显示快捷键帮助
|
|
30
|
+
|
|
31
|
+
### 🔧 Technical
|
|
32
|
+
- **新增 `app/cgi-bin/stats.js`**:后端统计 API,支持 overview/top/timeline/recent 四种查询维度
|
|
33
|
+
- **新增 `app/cgi-bin/proxy-fetch.js`**:后端代理抓取真实接口响应
|
|
34
|
+
- **新增 `ui/src/pages/Stats.js`**:请求统计前端页面
|
|
35
|
+
- **新增 `ui/src/hooks/useKeyboardShortcuts.js`**:全局键盘快捷键 Hook
|
|
36
|
+
- **新增 `ui/src/components/KeyboardShortcutsHelp.js`**:快捷键帮助弹窗
|
|
37
|
+
- **新增 `ui/src/components/interface-management/RealResponseRecorder.js`**:真实响应抓取与 Mock 模板生成组件
|
|
38
|
+
|
|
39
|
+
## v1.2.2-beta.25
|
|
40
|
+
|
|
41
|
+
### 🐛 Fix
|
|
42
|
+
- **主题切换彻底修复**:`cyber-theme.css` 全部选择器添加 `body.cyber-theme` 前缀,`:root` CSS 变量移至 `body.cyber-theme` 作用域,关闭主题后不再污染亮色模式
|
|
43
|
+
- **动态 class 切换**:`AppLayout.js` 通过 `useEffect` 实时 toggle `body.cyber-theme` class,并监听 `storage` 事件实现跨组件同步
|
|
44
|
+
- **Modal/Drawer 亮色恢复**:移除 `InterfaceManagementV2.js`、`PreviewModal.js`、`ResponseContentEditor.js` 中硬编码的深色背景/边框/文字色,恢复 Ant Design 默认亮色样式
|
|
45
|
+
- **顶部栏/侧边栏/内容区亮色修复**:非赛博朋克模式下显式设置白色/浅灰色背景(`#ffffff` / `#f5f5f5`)和深色文字(`#000000d9`),避免 `ConfigProvider` 算法切换后样式残留
|
|
46
|
+
- **版本号可见性修复**:亮色模式下版本号按钮文字改为 `#000000d9`,确保在白色顶部栏上清晰可见
|
|
47
|
+
|
|
48
|
+
### 🔧 Technical
|
|
49
|
+
- **`index.js`**:改为始终加载 `cyber-theme.css`(规则带前缀,默认不生效)
|
|
50
|
+
- **`AppLayout.js`**:`enableCyber` 改为响应式 `useState`,配合 `useEffect` 动态管理主题状态
|
|
51
|
+
|
|
52
|
+
## v1.2.2-beta.24
|
|
53
|
+
|
|
54
|
+
### ✨ Feature
|
|
55
|
+
- **赛博朋克主题**:全新深色霓虹主题,采用青色 `#00f0ff` 为主色调,搭配绿色 `#00ff41`、黄色 `#f0e800`、洋红 `#ff00a0` 等霓虹色,科技感十足
|
|
56
|
+
- **主题切换开关**:系统设置页面新增「赛博朋克风格」开关,默认开启,关闭后恢复经典 Ant Design 亮色主题,切换后自动刷新页面生效
|
|
57
|
+
- **动态主题加载**:通过 `localStorage` 持久化主题偏好,`ConfigProvider` 动态切换 `darkAlgorithm` / `defaultAlgorithm`,CSS 条件加载实现零闪烁切换
|
|
58
|
+
- **字体升级**:引入 Orbitron(标题)、Share Tech Mono(代码/版本号)、Noto Sans SC(正文)三款字体,强化科技感
|
|
59
|
+
|
|
60
|
+
### 🎨 UI
|
|
61
|
+
- **全局深色化**:所有页面容器、卡片、表格、表单、弹窗、侧边栏、导航栏统一深色背景 `#0f0f1a`
|
|
62
|
+
- **霓虹发光效果**:主按钮、开关、标签、选中菜单项均带有青色微光阴影
|
|
63
|
+
- **状态标签霓虹化**:成功/警告/错误状态标签改为半透明霓虹底色 + 发光文字
|
|
64
|
+
- **代码区域深色化**:预览弹窗、测试结果等代码展示区域改为深色背景 + 等宽字体
|
|
65
|
+
|
|
2
66
|
## v1.2.2-beta.22
|
|
67
|
+
|
|
3
68
|
### ✨ Feature
|
|
4
69
|
- **入参匹配开关**:系统设置新增「接口入参匹配」和「响应入参匹配」两个显示开关,关闭后对应 UI 区域隐藏,同时后端运行时跳过对应匹配逻辑,直接使用默认响应
|
|
5
70
|
- **开关持久化**:开关状态通过 `app/cgi-bin/settings.js` 持久化到 `settings.json`,前端初始化时从后端同步,保证多端一致
|
|
@@ -12,6 +77,7 @@
|
|
|
12
77
|
- **`server.js`**:注册 `lib/routes/settings.js` 路由(`/api/settings`)
|
|
13
78
|
|
|
14
79
|
## v1.2.2-beta.18
|
|
80
|
+
|
|
15
81
|
### ✨ Feature
|
|
16
82
|
- **响应级入参匹配**:每个响应内容支持独立配置入参匹配规则,同一接口内不同入参可自动匹配返回不同响应,无需创建多个接口定义
|
|
17
83
|
- **动态响应选择**:运行时按响应列表顺序依次检查入参规则,第一个匹配成功的响应被返回;无规则的响应作为默认兜底
|
|
@@ -28,6 +94,7 @@
|
|
|
28
94
|
- **匹配逻辑升级**:`ruleManager.handleDataTemplate` 新增响应级入参匹配逻辑,命中后响应头 `X-Whistle-Mock-Response` 准确反映实际返回的响应
|
|
29
95
|
|
|
30
96
|
## v1.2.2-beta.7
|
|
97
|
+
|
|
31
98
|
### ✨ Feature
|
|
32
99
|
- **交互增强**:接口表单(添加/编辑接口)支持弹窗/抽屉模式切换
|
|
33
100
|
- **设置新增**:系统设置页面新增"接口表单交互"开关,支持弹窗/抽屉模式切换
|
|
@@ -41,6 +108,7 @@
|
|
|
41
108
|
- **表单复用**:Modal 和 Drawer 共享相同的表单逻辑和验证规则
|
|
42
109
|
|
|
43
110
|
## v1.2.2-beta.4
|
|
111
|
+
|
|
44
112
|
### ✨ Feature
|
|
45
113
|
- **交互增强**:数据管理支持抽屉模式,从右侧滑出,宽度 1040px,上下撑满
|
|
46
114
|
- **设置新增**:系统设置页面新增"数据管理交互"开关,支持弹窗/抽屉模式切换
|
|
@@ -49,6 +117,7 @@
|
|
|
49
117
|
- **拖动优化**:拖动时实时更新列宽,提供流畅的视觉反馈
|
|
50
118
|
|
|
51
119
|
## v1.2.2-beta.3
|
|
120
|
+
|
|
52
121
|
### ✨ Feature
|
|
53
122
|
- **交互优化**:接口列表"当前响应"列支持拖动调整宽度
|
|
54
123
|
- **自适应优化**:列内下拉框根据列宽自动调整显示
|
|
@@ -62,6 +131,7 @@
|
|
|
62
131
|
- **样式优化**:优化拖动手柄样式,提供更好的视觉反馈
|
|
63
132
|
|
|
64
133
|
## v1.2.1
|
|
134
|
+
|
|
65
135
|
### ✨ Feature
|
|
66
136
|
- **界面优化**:功能模块页面支持列表/卡片视图切换,列表视图更加紧凑高效
|
|
67
137
|
- **代码优化**:下线 V1 版本代码,全面采用 V2 版本
|
|
@@ -72,21 +142,24 @@
|
|
|
72
142
|
- **问题修复**:请求入参匹配在编辑删除后保存不生效(V1/V2)。前端在提交时始终显式发送 `paramMatchers` 字段,空数组表示清空,后端得以覆盖旧值。
|
|
73
143
|
|
|
74
144
|
## v1.2.0
|
|
145
|
+
|
|
75
146
|
### ✨ Feature
|
|
76
147
|
- **代码优化**:下线 V1 版本代码,全面采用 V2 版本
|
|
77
148
|
- **界面简化**:移除版本切换功能,统一用户体验
|
|
78
149
|
|
|
79
150
|
## v1.2.1-beta.5
|
|
151
|
+
|
|
80
152
|
### ✨ Feature
|
|
81
153
|
- **新增功能**:新增mock数据管理弹窗
|
|
82
154
|
|
|
83
155
|
## v1.2.0
|
|
156
|
+
|
|
84
157
|
### ✨ Feature
|
|
85
158
|
- **界面优化**:模块管理接口管理页面细节调整(v2版本)
|
|
86
159
|
- **设置更新**:设置界面添加切换 UI 版本
|
|
87
160
|
- **界面优化**:功能模块支持列表展示(v2)
|
|
88
161
|
- **界面优化**:页面细节调整
|
|
89
|
-
-
|
|
162
|
+
- **新增**:接口管理页支持"复制新增"。可基于已配置接口一键打开新增弹窗并预填,响应模式会为每个响应生成全新ID并正确映射激活响应;其余配置(分组、参数匹配、重定向设定等)保留。保存后自动刷新缓存。
|
|
90
163
|
|
|
91
164
|
### 🐛 Fix
|
|
92
165
|
- **问题修复**:请求入参匹配在编辑删除后保存不生效(V1/V2)。前端在提交时始终显式发送 `paramMatchers` 字段,空数组表示清空,后端得以覆盖旧值。
|
|
@@ -94,10 +167,12 @@
|
|
|
94
167
|
- **问题修复**:编辑接口保存折叠内容丢失
|
|
95
168
|
|
|
96
169
|
## v1.1.2
|
|
170
|
+
|
|
97
171
|
### 🐛 Fix
|
|
98
172
|
- **问题修复**: 接口管理中禁用启用功能失效
|
|
99
173
|
|
|
100
174
|
## v1.1.1
|
|
175
|
+
|
|
101
176
|
### 🆕 更新功能
|
|
102
177
|
- **项目优化**:版本号同步显示在页面右上角,支持点击查看详细更新记录
|
|
103
178
|
- **项目优化**:新增模拟响应入参校验功能,提升数据准确性
|
|
@@ -116,6 +191,7 @@
|
|
|
116
191
|
|
|
117
192
|
|
|
118
193
|
## v1.1.0
|
|
194
|
+
|
|
119
195
|
### 🆕 更新功能
|
|
120
196
|
- **多响应管理系统**:每个接口支持配置多个不同的响应内容
|
|
121
197
|
- **响应切换功能**:可在多个响应之间快速切换,便于测试不同场景
|
|
@@ -124,4 +200,4 @@
|
|
|
124
200
|
- **响应管理界面**:全新的响应管理卡片式界面
|
|
125
201
|
- **接口匹配引擎重构**:大幅简化匹配逻辑,提升匹配性能
|
|
126
202
|
- **正则表达式缓存**:避免重复编译正则表达式,提升匹配效率
|
|
127
|
-
- **代码简化**:移除复杂的参数匹配功能,专注核心URL匹配
|
|
203
|
+
- **代码简化**:移除复杂的参数匹配功能,专注核心URL匹配
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
const axios = require('axios');
|
|
2
|
+
const { URL } = require('url');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 代理抓取真实接口响应
|
|
6
|
+
* POST /cgi-bin/proxy-fetch
|
|
7
|
+
* body: {
|
|
8
|
+
* url: string,
|
|
9
|
+
* method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS',
|
|
10
|
+
* headers?: Record<string, string>,
|
|
11
|
+
* queryParams?: Record<string, string>,
|
|
12
|
+
* body?: any,
|
|
13
|
+
* bodyType?: 'json' | 'form' | 'text' | 'none'
|
|
14
|
+
* }
|
|
15
|
+
*
|
|
16
|
+
* 注意:ui-server.js 已用 body-parser 解析 JSON,直接取 req.body
|
|
17
|
+
*/
|
|
18
|
+
module.exports = async (req, res) => {
|
|
19
|
+
if (req.method !== 'POST') {
|
|
20
|
+
res.statusCode = 405;
|
|
21
|
+
res.setHeader('Content-Type', 'application/json');
|
|
22
|
+
res.end(JSON.stringify({ code: 405, message: 'Method Not Allowed' }));
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const params = req.body || {};
|
|
28
|
+
const {
|
|
29
|
+
url: targetUrl,
|
|
30
|
+
method = 'GET',
|
|
31
|
+
headers = {},
|
|
32
|
+
queryParams = {},
|
|
33
|
+
body: requestBody,
|
|
34
|
+
bodyType = 'none'
|
|
35
|
+
} = params;
|
|
36
|
+
|
|
37
|
+
if (!targetUrl) {
|
|
38
|
+
res.setHeader('Content-Type', 'application/json');
|
|
39
|
+
res.end(JSON.stringify({ code: 400, message: '缺少 URL 参数' }));
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 构建带 query 参数的 URL
|
|
44
|
+
const parsed = new URL(targetUrl);
|
|
45
|
+
Object.entries(queryParams).forEach(([key, value]) => {
|
|
46
|
+
if (value !== undefined && value !== null && value !== '') {
|
|
47
|
+
parsed.searchParams.set(key, String(value));
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
const finalUrl = parsed.toString();
|
|
51
|
+
|
|
52
|
+
// 构造 axios 请求配置
|
|
53
|
+
const axiosConfig = {
|
|
54
|
+
url: finalUrl,
|
|
55
|
+
method: method.toUpperCase(),
|
|
56
|
+
headers: {
|
|
57
|
+
'Accept': 'application/json, text/plain, */*',
|
|
58
|
+
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
59
|
+
...headers
|
|
60
|
+
},
|
|
61
|
+
timeout: 15000,
|
|
62
|
+
maxRedirects: 5,
|
|
63
|
+
responseType: 'text',
|
|
64
|
+
decompress: true,
|
|
65
|
+
validateStatus: () => true,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// 移除禁止客户端发送的请求头(不区分大小写)
|
|
69
|
+
['host', 'content-length', 'transfer-encoding'].forEach(h => {
|
|
70
|
+
const key = Object.keys(axiosConfig.headers).find(k => k.toLowerCase() === h);
|
|
71
|
+
if (key) delete axiosConfig.headers[key];
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// 处理请求体
|
|
75
|
+
if (requestBody && bodyType !== 'none' && method.toUpperCase() !== 'GET' && method.toUpperCase() !== 'HEAD') {
|
|
76
|
+
if (bodyType === 'json') {
|
|
77
|
+
axiosConfig.headers['Content-Type'] = 'application/json; charset=utf-8';
|
|
78
|
+
axiosConfig.data = typeof requestBody === 'string' ? requestBody : JSON.stringify(requestBody);
|
|
79
|
+
} else if (bodyType === 'form') {
|
|
80
|
+
axiosConfig.headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=utf-8';
|
|
81
|
+
if (typeof requestBody === 'object' && requestBody !== null) {
|
|
82
|
+
const qs = new URLSearchParams();
|
|
83
|
+
Object.entries(requestBody).forEach(([k, v]) => qs.append(k, v));
|
|
84
|
+
axiosConfig.data = qs.toString();
|
|
85
|
+
} else {
|
|
86
|
+
axiosConfig.data = String(requestBody);
|
|
87
|
+
}
|
|
88
|
+
} else {
|
|
89
|
+
axiosConfig.headers['Content-Type'] = 'text/plain; charset=utf-8';
|
|
90
|
+
axiosConfig.data = String(requestBody);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
console.log('[proxy-fetch] 发起请求:', axiosConfig.method, finalUrl);
|
|
95
|
+
const proxyRes = await axios(axiosConfig);
|
|
96
|
+
console.log('[proxy-fetch] 请求完成:', proxyRes.status, finalUrl);
|
|
97
|
+
|
|
98
|
+
res.setHeader('Content-Type', 'application/json');
|
|
99
|
+
res.end(JSON.stringify({
|
|
100
|
+
code: 0,
|
|
101
|
+
data: {
|
|
102
|
+
status: proxyRes.status,
|
|
103
|
+
statusText: proxyRes.statusText,
|
|
104
|
+
headers: proxyRes.headers,
|
|
105
|
+
body: typeof proxyRes.data === 'string' ? proxyRes.data : JSON.stringify(proxyRes.data),
|
|
106
|
+
finalUrl: proxyRes.request?.res?.responseUrl || finalUrl
|
|
107
|
+
}
|
|
108
|
+
}));
|
|
109
|
+
} catch (err) {
|
|
110
|
+
console.error('[proxy-fetch] 请求失败:', err.message);
|
|
111
|
+
res.setHeader('Content-Type', 'application/json');
|
|
112
|
+
res.end(JSON.stringify({
|
|
113
|
+
code: 500,
|
|
114
|
+
message: '请求失败: ' + (err.response?.statusText || err.message)
|
|
115
|
+
}));
|
|
116
|
+
}
|
|
117
|
+
};
|
|
@@ -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>
|