jacky-proxy 1.0.0
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/README.md +410 -0
- package/bin/jacky-proxy.js +137 -0
- package/package.json +64 -0
- package/scripts/generate-config.js +337 -0
- package/server.js +1022 -0
- package/src/commands/config-generate.js +26 -0
- package/src/commands/config-merge.js +29 -0
- package/src/commands/config-validate.js +30 -0
- package/src/commands/migrate.js +813 -0
- package/src/commands/rules-add.js +82 -0
- package/src/commands/rules-list.js +67 -0
- package/src/commands/rules-remove.js +43 -0
- package/src/commands/rules-test.js +72 -0
- package/src/commands/start.js +203 -0
- package/templates/mock-admin.html +736 -0
- package/tsconfig.json +18 -0
- package/utils/common/match-response.ts +491 -0
- package/utils/interface-identifier.ts +130 -0
package/server.js
ADDED
|
@@ -0,0 +1,1022 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 通用 Mock 服务器 - 主服务器文件
|
|
3
|
+
* 接收 Proxyman 转发的请求,根据接口标识符匹配对应的 Mock 响应
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const express = require('express');
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
// 使用 ts-node 支持 TypeScript 文件
|
|
11
|
+
require('ts-node').register({
|
|
12
|
+
transpileOnly: true,
|
|
13
|
+
compilerOptions: {
|
|
14
|
+
module: 'commonjs',
|
|
15
|
+
baseUrl: __dirname,
|
|
16
|
+
paths: {
|
|
17
|
+
'*': ['*', 'utils/*', 'utils/common/*']
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// 设置项目根目录环境变量,供 Mock 文件使用
|
|
23
|
+
process.env.JACKY_PROXY_ROOT = __dirname;
|
|
24
|
+
|
|
25
|
+
const { extractInterfaceIdentifier } = require('./utils/interface-identifier');
|
|
26
|
+
const { matchResponse } = require('./utils/common/match-response');
|
|
27
|
+
const { generateConfig, validateConfig, mergeConfig } = require('./scripts/generate-config');
|
|
28
|
+
|
|
29
|
+
const app = express();
|
|
30
|
+
const PORT = process.env.PORT || 5001;
|
|
31
|
+
|
|
32
|
+
// 中间件:解析 JSON 和 URL 编码的请求体(必须在日志中间件之前,以便日志可以打印 body)
|
|
33
|
+
// 配置 strict: false 以允许控制字符(如换行符、制表符等)
|
|
34
|
+
app.use(express.json({
|
|
35
|
+
limit: '50mb',
|
|
36
|
+
strict: false
|
|
37
|
+
}));
|
|
38
|
+
app.use(express.urlencoded({ extended: true, limit: '50mb' }));
|
|
39
|
+
|
|
40
|
+
// JSON 解析错误处理中间件(必须在 express.json() 之后)
|
|
41
|
+
app.use((err, req, res, next) => {
|
|
42
|
+
if (err instanceof SyntaxError && err.status === 400 && 'body' in err) {
|
|
43
|
+
// 只在 debug 模式打印 JSON 解析错误
|
|
44
|
+
const isDebugMode = process.env.DEBUG === 'true' || process.env.DEBUG === '1';
|
|
45
|
+
if (isDebugMode) {
|
|
46
|
+
console.warn('⚠️ JSON 解析错误:', err.message);
|
|
47
|
+
console.warn(' 请求路径:', req.path);
|
|
48
|
+
console.warn(' 请求方法:', req.method);
|
|
49
|
+
}
|
|
50
|
+
// 设置空 body 并继续处理,避免中断请求
|
|
51
|
+
req.body = req.body || {};
|
|
52
|
+
next();
|
|
53
|
+
} else {
|
|
54
|
+
next(err);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* ANSI 颜色代码
|
|
60
|
+
*/
|
|
61
|
+
const colors = {
|
|
62
|
+
reset: '\x1b[0m',
|
|
63
|
+
bright: '\x1b[1m',
|
|
64
|
+
dim: '\x1b[2m',
|
|
65
|
+
red: '\x1b[31m',
|
|
66
|
+
green: '\x1b[32m',
|
|
67
|
+
yellow: '\x1b[33m',
|
|
68
|
+
blue: '\x1b[34m',
|
|
69
|
+
magenta: '\x1b[35m',
|
|
70
|
+
cyan: '\x1b[36m',
|
|
71
|
+
gray: '\x1b[90m',
|
|
72
|
+
white: '\x1b[37m'
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 获取格式化的时间戳(带颜色)
|
|
77
|
+
*/
|
|
78
|
+
function getTimestamp() {
|
|
79
|
+
const now = new Date();
|
|
80
|
+
const hours = String(now.getHours()).padStart(2, '0');
|
|
81
|
+
const minutes = String(now.getMinutes()).padStart(2, '0');
|
|
82
|
+
const seconds = String(now.getSeconds()).padStart(2, '0');
|
|
83
|
+
const milliseconds = String(now.getMilliseconds()).padStart(3, '0');
|
|
84
|
+
return `${colors.gray}[${hours}:${minutes}:${seconds}.${milliseconds}]${colors.reset}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 请求日志中间件
|
|
89
|
+
* 必须在所有路由之前应用,确保所有请求都会被记录
|
|
90
|
+
*/
|
|
91
|
+
function requestLogger(req, res, next) {
|
|
92
|
+
const isDebugMode = process.env.DEBUG === 'true' || process.env.DEBUG === '1';
|
|
93
|
+
const method = req.method;
|
|
94
|
+
const url = req.url;
|
|
95
|
+
const path = req.path;
|
|
96
|
+
|
|
97
|
+
if (isDebugMode) {
|
|
98
|
+
// Debug 模式:输出详细信息
|
|
99
|
+
const timestamp = new Date().toISOString();
|
|
100
|
+
const fullUrl = req.originalUrl || url;
|
|
101
|
+
const query = req.query;
|
|
102
|
+
const headers = req.headers;
|
|
103
|
+
const body = req.body;
|
|
104
|
+
const ip = req.ip || req.connection.remoteAddress;
|
|
105
|
+
|
|
106
|
+
console.log('\n========== 请求拦截 ==========');
|
|
107
|
+
console.log(`时间: ${timestamp}`);
|
|
108
|
+
console.log(`方法: ${method}`);
|
|
109
|
+
console.log(`URL: ${url}`);
|
|
110
|
+
console.log(`完整路径: ${fullUrl}`);
|
|
111
|
+
console.log(`路径: ${path}`);
|
|
112
|
+
console.log(`Headers:`, JSON.stringify(headers, null, 2));
|
|
113
|
+
if (Object.keys(query).length > 0) {
|
|
114
|
+
console.log(`Query参数:`, JSON.stringify(query, null, 2));
|
|
115
|
+
}
|
|
116
|
+
if (body && Object.keys(body).length > 0) {
|
|
117
|
+
console.log(`Body:`, JSON.stringify(body, null, 2));
|
|
118
|
+
}
|
|
119
|
+
console.log(`IP: ${ip}`);
|
|
120
|
+
console.log('================================\n');
|
|
121
|
+
} else {
|
|
122
|
+
// 精简模式:只输出关键信息(带时间戳和颜色)
|
|
123
|
+
const methodColor = method === 'GET' ? colors.cyan : method === 'POST' ? colors.blue : colors.white;
|
|
124
|
+
console.log(`${getTimestamp()} ${methodColor}${method}${colors.reset} ${colors.dim}${path}${colors.reset}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
next();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// 应用请求日志中间件(在所有路由之前)
|
|
131
|
+
app.use(requestLogger);
|
|
132
|
+
|
|
133
|
+
// 静态文件服务(Web 界面)- 放在日志中间件之后,这样静态文件请求也会被记录
|
|
134
|
+
app.use(express.static(path.join(__dirname, 'public')));
|
|
135
|
+
|
|
136
|
+
// 加载配置文件
|
|
137
|
+
let mockConfig = null;
|
|
138
|
+
let currentMockId = null;
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* 获取工作目录(数据存储目录)
|
|
142
|
+
* 如果设置了 WORK_DIR 环境变量,使用工作目录;否则使用项目根目录
|
|
143
|
+
*/
|
|
144
|
+
function getWorkDir() {
|
|
145
|
+
return process.env.WORK_DIR || __dirname;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* 加载 proxy.config.json 配置文件
|
|
150
|
+
*/
|
|
151
|
+
function loadMockConfig() {
|
|
152
|
+
try {
|
|
153
|
+
// 优先使用环境变量指定的配置文件路径
|
|
154
|
+
let configPath;
|
|
155
|
+
if (process.env.CONFIG_PATH) {
|
|
156
|
+
configPath = process.env.CONFIG_PATH;
|
|
157
|
+
} else {
|
|
158
|
+
// 否则使用工作目录的配置文件
|
|
159
|
+
const workDir = getWorkDir();
|
|
160
|
+
configPath = path.join(workDir, 'proxy.config.json');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (fs.existsSync(configPath)) {
|
|
164
|
+
const configContent = fs.readFileSync(configPath, 'utf-8');
|
|
165
|
+
mockConfig = JSON.parse(configContent);
|
|
166
|
+
return mockConfig;
|
|
167
|
+
} else {
|
|
168
|
+
console.warn(`配置文件不存在: ${configPath}`);
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
} catch (error) {
|
|
172
|
+
console.error('加载配置文件失败:', error);
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* 根据 mockId 获取接口集路径
|
|
179
|
+
*/
|
|
180
|
+
function getMockFolderPath(mockId) {
|
|
181
|
+
if (!mockConfig) {
|
|
182
|
+
loadMockConfig();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (!mockConfig || !mockConfig.folders || !mockConfig.folders.list) {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const folder = mockConfig.folders.list.find(f => f.id === parseInt(mockId));
|
|
190
|
+
if (!folder) {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// 使用工作目录作为基础路径
|
|
195
|
+
const workDir = getWorkDir();
|
|
196
|
+
const folderPath = path.join(workDir, folder.path);
|
|
197
|
+
|
|
198
|
+
return folderPath;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* 扫描目录,加载所有 .mock.ts 文件
|
|
203
|
+
*/
|
|
204
|
+
function loadMockFiles(folderPath) {
|
|
205
|
+
const mockFiles = {};
|
|
206
|
+
|
|
207
|
+
if (!fs.existsSync(folderPath)) {
|
|
208
|
+
console.warn(`${colors.yellow}⚠️ 接口集路径不存在:${colors.reset} ${colors.dim}${folderPath}${colors.reset}`);
|
|
209
|
+
return mockFiles;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// 获取项目根目录,用于模块解析
|
|
213
|
+
const projectRoot = __dirname;
|
|
214
|
+
|
|
215
|
+
// 保存原始的 NODE_PATH
|
|
216
|
+
const originalNodePath = process.env.NODE_PATH || '';
|
|
217
|
+
|
|
218
|
+
// 将项目根目录添加到 NODE_PATH,这样 Mock 文件可以导入项目根目录的模块
|
|
219
|
+
const nodePaths = originalNodePath ? originalNodePath.split(path.delimiter) : [];
|
|
220
|
+
if (!nodePaths.includes(projectRoot)) {
|
|
221
|
+
nodePaths.unshift(projectRoot);
|
|
222
|
+
process.env.NODE_PATH = nodePaths.join(path.delimiter);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function scanDirectory(dir) {
|
|
226
|
+
const files = fs.readdirSync(dir);
|
|
227
|
+
|
|
228
|
+
for (const file of files) {
|
|
229
|
+
const filePath = path.join(dir, file);
|
|
230
|
+
const stat = fs.statSync(filePath);
|
|
231
|
+
|
|
232
|
+
if (stat.isDirectory()) {
|
|
233
|
+
// 递归扫描子目录
|
|
234
|
+
scanDirectory(filePath);
|
|
235
|
+
} else if (file.endsWith('.mock.ts') || file.endsWith('.mock.js')) {
|
|
236
|
+
// 从文件名提取接口标识符(去掉 .mock.ts 后缀)
|
|
237
|
+
const interfaceName = file.replace(/\.mock\.(ts|js)$/, '');
|
|
238
|
+
|
|
239
|
+
try {
|
|
240
|
+
// 动态加载 Mock 文件
|
|
241
|
+
// 注意:这里需要支持 TypeScript,可以使用 ts-node 或先编译
|
|
242
|
+
delete require.cache[require.resolve(filePath)];
|
|
243
|
+
const mockModule = require(filePath);
|
|
244
|
+
|
|
245
|
+
// 支持 default 导出或直接导出函数
|
|
246
|
+
const mockHandler = mockModule.default || mockModule;
|
|
247
|
+
|
|
248
|
+
if (typeof mockHandler === 'function') {
|
|
249
|
+
mockFiles[interfaceName] = mockHandler;
|
|
250
|
+
// 保存该 handler 对应的本地文件路径(绝对路径)
|
|
251
|
+
mockFilePaths[interfaceName] = filePath;
|
|
252
|
+
console.log(`${colors.green}✓${colors.reset} ${colors.dim}加载 Mock 文件:${colors.reset} ${colors.cyan}${file}${colors.reset} ${colors.gray}->${colors.reset} ${colors.bright}${interfaceName}${colors.reset}`);
|
|
253
|
+
} else {
|
|
254
|
+
console.warn(`${colors.yellow}⚠${colors.reset} ${colors.yellow}Mock 文件 ${colors.cyan}${file}${colors.yellow} 未导出函数${colors.reset}`);
|
|
255
|
+
}
|
|
256
|
+
} catch (error) {
|
|
257
|
+
console.error(`${colors.red}✗${colors.reset} ${colors.red}加载 Mock 文件失败:${colors.reset} ${colors.cyan}${file}${colors.reset} ${colors.red}${error.message}${colors.reset}`);
|
|
258
|
+
// 打印更详细的错误信息
|
|
259
|
+
if (error.code === 'MODULE_NOT_FOUND') {
|
|
260
|
+
console.error(` 无法找到模块: ${error.message}`);
|
|
261
|
+
console.error(` Mock 文件路径: ${filePath}`);
|
|
262
|
+
console.error(` 项目根目录: ${projectRoot}`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
try {
|
|
270
|
+
scanDirectory(folderPath);
|
|
271
|
+
} finally {
|
|
272
|
+
// 恢复原始的 NODE_PATH
|
|
273
|
+
process.env.NODE_PATH = originalNodePath;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return mockFiles;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// 缓存 Mock 文件
|
|
280
|
+
let cachedMockFiles = {};
|
|
281
|
+
let cachedMockId = null;
|
|
282
|
+
// 存储 mock 处理器对应的本地文件绝对路径(用于前端复制)
|
|
283
|
+
let mockFilePaths = {};
|
|
284
|
+
|
|
285
|
+
// 存储动态切换的场景配置(用于接口的场景切换)
|
|
286
|
+
// 格式: { 'getProductInfo': '2' } 表示 getProductInfo 使用 getProductInfo-2.mock.ts
|
|
287
|
+
let mockScenarios = {};
|
|
288
|
+
|
|
289
|
+
// 存储被禁用的接口(Set)
|
|
290
|
+
// 格式: Set(['getProductInfo', 'productSearch']) 表示这些接口被禁用
|
|
291
|
+
let disabledInterfaces = new Set();
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* 获取或加载 Mock 文件
|
|
295
|
+
*/
|
|
296
|
+
function getMockFiles(mockId) {
|
|
297
|
+
if (cachedMockId === mockId && Object.keys(cachedMockFiles).length > 0) {
|
|
298
|
+
return cachedMockFiles;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const folderPath = getMockFolderPath(mockId);
|
|
302
|
+
if (!folderPath) {
|
|
303
|
+
console.error(`未找到 mockId ${mockId} 对应的接口集`);
|
|
304
|
+
return {};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
console.log(`\n${colors.blue}📂${colors.reset} ${colors.bright}加载接口集:${colors.reset} ${colors.dim}${folderPath}${colors.reset}`);
|
|
308
|
+
cachedMockFiles = loadMockFiles(folderPath);
|
|
309
|
+
cachedMockId = mockId;
|
|
310
|
+
|
|
311
|
+
console.log(`${colors.green}✅${colors.reset} ${colors.bright}共加载 ${colors.green}${Object.keys(cachedMockFiles).length}${colors.reset} ${colors.bright}个 Mock 接口${colors.reset}\n`);
|
|
312
|
+
return cachedMockFiles;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* 接口识别配置(可以从配置文件加载,这里使用默认配置)
|
|
317
|
+
*/
|
|
318
|
+
const interfaceIdentifierConfig = {
|
|
319
|
+
strategies: [
|
|
320
|
+
{
|
|
321
|
+
type: 'urlPattern',
|
|
322
|
+
pattern: '/([^/]+)$',
|
|
323
|
+
group: 1,
|
|
324
|
+
description: '提取 URL 最后一段作为接口标识符'
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
type: 'header',
|
|
328
|
+
key: 'X-Interface-Name',
|
|
329
|
+
description: '从请求头提取接口标识符'
|
|
330
|
+
}
|
|
331
|
+
]
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* 处理所有 HTTP 请求
|
|
336
|
+
*/
|
|
337
|
+
async function handleRequest(req, res) {
|
|
338
|
+
try {
|
|
339
|
+
// 从请求中提取接口标识符
|
|
340
|
+
const interfaceName = extractInterfaceIdentifier(req, interfaceIdentifierConfig);
|
|
341
|
+
|
|
342
|
+
if (!interfaceName) {
|
|
343
|
+
console.log('⚠️ 无法从请求中提取接口标识符,返回原始请求信息');
|
|
344
|
+
return res.status(400).json({
|
|
345
|
+
error: true,
|
|
346
|
+
message: '无法识别接口标识符',
|
|
347
|
+
request: {
|
|
348
|
+
method: req.method,
|
|
349
|
+
url: req.url,
|
|
350
|
+
path: req.path,
|
|
351
|
+
headers: req.headers,
|
|
352
|
+
query: req.query,
|
|
353
|
+
body: req.body
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// 只在 debug 模式打印接口标识符
|
|
359
|
+
const isDebugMode = process.env.DEBUG === 'true' || process.env.DEBUG === '1';
|
|
360
|
+
if (isDebugMode) {
|
|
361
|
+
console.log(`🔍 识别到接口标识符: ${interfaceName}`);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// 检查接口是否被禁用
|
|
365
|
+
if (disabledInterfaces.has(interfaceName)) {
|
|
366
|
+
if (isDebugMode) {
|
|
367
|
+
console.log(`🚫 接口 ${interfaceName} 已被禁用,跳过 mock 处理`);
|
|
368
|
+
}
|
|
369
|
+
return res.json({
|
|
370
|
+
message: `接口 ${interfaceName} 已被禁用`,
|
|
371
|
+
disabled: true,
|
|
372
|
+
interfaceName
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// 获取当前 mockId(从环境变量或命令行参数)
|
|
377
|
+
const mockId = currentMockId || process.env.MOCK_ID || '1';
|
|
378
|
+
|
|
379
|
+
// 获取 Mock 文件
|
|
380
|
+
const mockFiles = getMockFiles(mockId);
|
|
381
|
+
|
|
382
|
+
// 检查是否需要动态切换 mock 场景(通过 CLI 或 Web 界面设置)
|
|
383
|
+
// 支持所有接口的场景切换,例如: getProductInfo-2, getProductInfo-3 等
|
|
384
|
+
let actualInterfaceName = interfaceName;
|
|
385
|
+
const scenario = mockScenarios[interfaceName];
|
|
386
|
+
|
|
387
|
+
if (scenario) {
|
|
388
|
+
const scenarioInterfaceName = `${interfaceName}-${scenario}`;
|
|
389
|
+
if (mockFiles[scenarioInterfaceName]) {
|
|
390
|
+
actualInterfaceName = scenarioInterfaceName;
|
|
391
|
+
if (isDebugMode) {
|
|
392
|
+
console.log(`🔄 使用动态切换的 mock 场景: ${interfaceName} -> ${actualInterfaceName}`);
|
|
393
|
+
}
|
|
394
|
+
} else {
|
|
395
|
+
if (isDebugMode) {
|
|
396
|
+
console.log(`⚠️ 未找到场景 ${scenario} 的 mock 处理器 (${scenarioInterfaceName}),使用默认 ${interfaceName}`);
|
|
397
|
+
}
|
|
398
|
+
delete mockScenarios[interfaceName]; // 清除无效的场景配置
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// 查找对应的 mock 处理器
|
|
403
|
+
const mockHandler = mockFiles[actualInterfaceName];
|
|
404
|
+
|
|
405
|
+
if (!mockHandler) {
|
|
406
|
+
// 使用静态变量记录已警告的接口,避免重复打印
|
|
407
|
+
if (!handleRequest.warnedInterfaces) {
|
|
408
|
+
handleRequest.warnedInterfaces = new Set();
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (!handleRequest.warnedInterfaces.has(actualInterfaceName)) {
|
|
412
|
+
handleRequest.warnedInterfaces.add(actualInterfaceName);
|
|
413
|
+
console.log(`${getTimestamp()} ${colors.yellow}⚠️ ${actualInterfaceName} 未找到 mock 处理器${colors.reset}`);
|
|
414
|
+
|
|
415
|
+
// 只在 debug 模式打印可用的处理器列表
|
|
416
|
+
if (isDebugMode) {
|
|
417
|
+
console.log(`${colors.dim}📋 可用的 mock 处理器: ${Object.keys(mockFiles).join(', ')}${colors.reset}`);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return res.status(404).json({
|
|
422
|
+
error: true,
|
|
423
|
+
message: `未找到 ${actualInterfaceName} 的 Mock 文件`,
|
|
424
|
+
interfaceName,
|
|
425
|
+
actualInterfaceName,
|
|
426
|
+
availableInterfaces: Object.keys(mockFiles)
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// 确保 mockHandler 是函数
|
|
431
|
+
if (typeof mockHandler !== 'function') {
|
|
432
|
+
console.error(`❌ mockHandler 不是函数,实际类型: ${typeof mockHandler}`);
|
|
433
|
+
console.error(` 值:`, mockHandler);
|
|
434
|
+
return res.status(500).json({
|
|
435
|
+
error: 'Mock 处理器不是函数',
|
|
436
|
+
interfaceName: actualInterfaceName,
|
|
437
|
+
handlerType: typeof mockHandler
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// 构建请求对象
|
|
442
|
+
const request = {
|
|
443
|
+
body: req.body,
|
|
444
|
+
options: {
|
|
445
|
+
headers: req.headers
|
|
446
|
+
},
|
|
447
|
+
method: req.method,
|
|
448
|
+
url: req.url,
|
|
449
|
+
path: req.path,
|
|
450
|
+
query: req.query
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
// 调用 Mock 处理函数(只在 debug 模式打印详细信息)
|
|
454
|
+
if (isDebugMode) {
|
|
455
|
+
console.log(`🚀 调用 ${actualInterfaceName} 的 mock 处理器...`);
|
|
456
|
+
if (actualInterfaceName !== interfaceName) {
|
|
457
|
+
console.log(` (原始接口标识符: ${interfaceName})`);
|
|
458
|
+
}
|
|
459
|
+
console.log(` 处理器类型: ${typeof mockHandler}`);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
let result;
|
|
463
|
+
try {
|
|
464
|
+
result = await mockHandler(request);
|
|
465
|
+
} catch (error) {
|
|
466
|
+
console.error(`❌ Mock 处理器执行出错:`, error);
|
|
467
|
+
return res.status(500).json({
|
|
468
|
+
error: 'Mock 处理器执行出错',
|
|
469
|
+
message: error.message,
|
|
470
|
+
interfaceName: actualInterfaceName
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
// 检查是否匹配失败
|
|
476
|
+
if (result && result.body && result.body.error) {
|
|
477
|
+
console.log(`${getTimestamp()} ${colors.red}❌ ${actualInterfaceName} 匹配失败${colors.reset}`);
|
|
478
|
+
return res.status(404).json(result.body);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// 返回响应
|
|
482
|
+
if (result && result.body) {
|
|
483
|
+
// 设置响应头
|
|
484
|
+
if (result.headers) {
|
|
485
|
+
Object.keys(result.headers).forEach(key => {
|
|
486
|
+
res.setHeader(key, result.headers[key]);
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// 确保 Content-Type 是 application/json
|
|
491
|
+
if (!res.getHeader('Content-Type')) {
|
|
492
|
+
res.setHeader('Content-Type', 'application/json');
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// 优先使用 result.status,如果没有则尝试从响应体中提取
|
|
496
|
+
let status = result.status;
|
|
497
|
+
if (!status && result.body?.ResponseStatus?.Errors?.[0]?.ErrorCode) {
|
|
498
|
+
// ErrorCode 可能是字符串,需要转换为数字
|
|
499
|
+
const errorCode = result.body.ResponseStatus.Errors[0].ErrorCode;
|
|
500
|
+
status = typeof errorCode === 'string' ? parseInt(errorCode, 10) : errorCode;
|
|
501
|
+
}
|
|
502
|
+
status = status || 200;
|
|
503
|
+
|
|
504
|
+
// 精简输出:只显示接口名和状态码(带颜色)
|
|
505
|
+
const statusColor = status >= 200 && status < 300 ? colors.green : status >= 400 ? colors.red : colors.yellow;
|
|
506
|
+
console.log(`${getTimestamp()} ${colors.green}✅${colors.reset} ${colors.bright}${actualInterfaceName}${colors.reset} ${statusColor}(${status})${colors.reset}`);
|
|
507
|
+
|
|
508
|
+
// 对于 429 等特殊状态码,使用 send 而不是 json,确保返回实际的响应体内容
|
|
509
|
+
// 而不是 Express 默认的状态码文本(如 "Too Many Requests")
|
|
510
|
+
if (status === 429) {
|
|
511
|
+
return res.status(status).type('json').send(JSON.stringify(result.body));
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
return res.status(status).json(result.body);
|
|
515
|
+
} else if (result && result.status) {
|
|
516
|
+
// 兼容旧格式:只有 status 没有 body
|
|
517
|
+
if (result.headers) {
|
|
518
|
+
Object.keys(result.headers).forEach(key => {
|
|
519
|
+
res.setHeader(key, result.headers[key]);
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
return res.status(result.status).json(result.body || {});
|
|
523
|
+
} else {
|
|
524
|
+
// 如果没有标准格式,尝试直接返回结果
|
|
525
|
+
console.log('⚠️ mock 处理器返回的响应格式不标准,尝试直接返回');
|
|
526
|
+
return res.status(200).json(result);
|
|
527
|
+
}
|
|
528
|
+
} catch (error) {
|
|
529
|
+
console.error('❌ 处理请求失败:', error);
|
|
530
|
+
return res.status(500).json({
|
|
531
|
+
error: true,
|
|
532
|
+
message: error.message,
|
|
533
|
+
stack: process.env.NODE_ENV === 'development' ? error.stack : undefined
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// 配置管理 API - 获取配置列表
|
|
539
|
+
app.get('/api/config', (req, res) => {
|
|
540
|
+
try {
|
|
541
|
+
const workDir = getWorkDir();
|
|
542
|
+
const configPath = path.join(workDir, 'proxy.config.json');
|
|
543
|
+
if (fs.existsSync(configPath)) {
|
|
544
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
545
|
+
res.json(config);
|
|
546
|
+
} else {
|
|
547
|
+
res.json({ libraryId: 2773, folders: { list: [] } });
|
|
548
|
+
}
|
|
549
|
+
} catch (error) {
|
|
550
|
+
res.status(500).json({
|
|
551
|
+
success: false,
|
|
552
|
+
error: error.message
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
// 配置管理 API - 添加配置
|
|
558
|
+
app.post('/api/config', (req, res) => {
|
|
559
|
+
try {
|
|
560
|
+
const workDir = getWorkDir();
|
|
561
|
+
const configPath = path.join(workDir, 'proxy.config.json');
|
|
562
|
+
let config = { libraryId: 2773, folders: { list: [] } };
|
|
563
|
+
|
|
564
|
+
if (fs.existsSync(configPath)) {
|
|
565
|
+
config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const { id, path: folderPath, name } = req.body;
|
|
569
|
+
|
|
570
|
+
// 检查 ID 是否已存在
|
|
571
|
+
if (config.folders.list.find(f => f.id === parseInt(id))) {
|
|
572
|
+
return res.status(400).json({
|
|
573
|
+
success: false,
|
|
574
|
+
error: `ID ${id} 已存在`
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
config.folders.list.push({ id: parseInt(id), path: folderPath, name });
|
|
579
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
580
|
+
|
|
581
|
+
res.json({ success: true, config });
|
|
582
|
+
} catch (error) {
|
|
583
|
+
res.status(500).json({
|
|
584
|
+
success: false,
|
|
585
|
+
error: error.message
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
// 配置管理 API - 删除配置
|
|
591
|
+
app.delete('/api/config/:id', (req, res) => {
|
|
592
|
+
try {
|
|
593
|
+
const workDir = getWorkDir();
|
|
594
|
+
const configPath = path.join(workDir, 'proxy.config.json');
|
|
595
|
+
if (!fs.existsSync(configPath)) {
|
|
596
|
+
return res.status(404).json({
|
|
597
|
+
success: false,
|
|
598
|
+
error: '配置文件不存在'
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
603
|
+
const id = parseInt(req.params.id);
|
|
604
|
+
|
|
605
|
+
const index = config.folders.list.findIndex(f => f.id === id);
|
|
606
|
+
if (index === -1) {
|
|
607
|
+
return res.status(404).json({
|
|
608
|
+
success: false,
|
|
609
|
+
error: `ID ${id} 不存在`
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
config.folders.list.splice(index, 1);
|
|
614
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
615
|
+
|
|
616
|
+
res.json({ success: true, config });
|
|
617
|
+
} catch (error) {
|
|
618
|
+
res.status(500).json({
|
|
619
|
+
success: false,
|
|
620
|
+
error: error.message
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
// 配置管理 API - 生成配置
|
|
626
|
+
app.post('/api/config/generate', (req, res) => {
|
|
627
|
+
try {
|
|
628
|
+
const workDir = getWorkDir();
|
|
629
|
+
const options = {
|
|
630
|
+
rootDir: req.body.rootDir || workDir,
|
|
631
|
+
outputPath: path.join(workDir, 'proxy.config.json'),
|
|
632
|
+
libraryId: req.body.libraryId || 2773,
|
|
633
|
+
startId: req.body.startId || 1
|
|
634
|
+
};
|
|
635
|
+
|
|
636
|
+
const config = generateConfig(options);
|
|
637
|
+
res.json({ success: true, config });
|
|
638
|
+
} catch (error) {
|
|
639
|
+
res.status(500).json({
|
|
640
|
+
success: false,
|
|
641
|
+
error: error.message
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
app.get('/api/config/validate', (req, res) => {
|
|
647
|
+
try {
|
|
648
|
+
const workDir = getWorkDir();
|
|
649
|
+
const configPath = path.join(workDir, 'proxy.config.json');
|
|
650
|
+
const result = validateConfig(configPath);
|
|
651
|
+
res.json(result);
|
|
652
|
+
} catch (error) {
|
|
653
|
+
res.status(500).json({
|
|
654
|
+
success: false,
|
|
655
|
+
error: error.message
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
app.post('/api/config/merge', (req, res) => {
|
|
661
|
+
try {
|
|
662
|
+
const workDir = getWorkDir();
|
|
663
|
+
const configPath = path.join(workDir, 'proxy.config.json');
|
|
664
|
+
|
|
665
|
+
// 加载现有配置
|
|
666
|
+
let existingConfig = { libraryId: 2773, folders: { list: [] } };
|
|
667
|
+
if (fs.existsSync(configPath)) {
|
|
668
|
+
existingConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// 生成新配置
|
|
672
|
+
const options = {
|
|
673
|
+
rootDir: req.body.rootDir || workDir,
|
|
674
|
+
libraryId: existingConfig.libraryId
|
|
675
|
+
};
|
|
676
|
+
const newConfig = generateConfig({ ...options, outputPath: null });
|
|
677
|
+
|
|
678
|
+
// 合并配置
|
|
679
|
+
const mergedConfig = mergeConfig(existingConfig, newConfig);
|
|
680
|
+
|
|
681
|
+
// 保存合并后的配置
|
|
682
|
+
fs.writeFileSync(configPath, JSON.stringify(mergedConfig, null, 2), 'utf-8');
|
|
683
|
+
|
|
684
|
+
res.json({ success: true, config: mergedConfig });
|
|
685
|
+
} catch (error) {
|
|
686
|
+
res.status(500).json({
|
|
687
|
+
success: false,
|
|
688
|
+
error: error.message
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
// 服务器信息 API
|
|
694
|
+
app.get('/api/server/info', (req, res) => {
|
|
695
|
+
try {
|
|
696
|
+
res.json({
|
|
697
|
+
port: PORT,
|
|
698
|
+
mockId: currentMockId || process.env.MOCK_ID || '1',
|
|
699
|
+
loadedInterfaces: Object.keys(cachedMockFiles).length,
|
|
700
|
+
availableInterfaces: Object.keys(cachedMockFiles)
|
|
701
|
+
});
|
|
702
|
+
} catch (error) {
|
|
703
|
+
res.status(500).json({
|
|
704
|
+
success: false,
|
|
705
|
+
error: error.message
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
// ==================== Mock 场景管理 API ====================
|
|
711
|
+
/**
|
|
712
|
+
* 获取所有可用的 mockId 列表(支持搜索)
|
|
713
|
+
*/
|
|
714
|
+
app.get('/mock-admin/mockids', (req, res) => {
|
|
715
|
+
try {
|
|
716
|
+
if (!mockConfig) {
|
|
717
|
+
loadMockConfig();
|
|
718
|
+
}
|
|
719
|
+
const configs = mockConfig?.folders?.list || [];
|
|
720
|
+
const currentMockIdNum = currentMockId ? parseInt(currentMockId) : null;
|
|
721
|
+
const search = req.query.search || '';
|
|
722
|
+
|
|
723
|
+
let filteredConfigs = configs;
|
|
724
|
+
|
|
725
|
+
if (search) {
|
|
726
|
+
const searchLower = search.toLowerCase();
|
|
727
|
+
filteredConfigs = configs.filter(config => {
|
|
728
|
+
const pathMatch = config.path.toLowerCase().includes(searchLower);
|
|
729
|
+
const idMatch = String(config.id).includes(search);
|
|
730
|
+
return pathMatch || idMatch;
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
const workDir = getWorkDir();
|
|
735
|
+
const mockIds = filteredConfigs.map(config => {
|
|
736
|
+
const folderPath = path.join(workDir, config.path);
|
|
737
|
+
return {
|
|
738
|
+
id: config.id,
|
|
739
|
+
path: config.path,
|
|
740
|
+
isActive: currentMockIdNum === config.id,
|
|
741
|
+
exists: fs.existsSync(folderPath)
|
|
742
|
+
};
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
res.json({
|
|
746
|
+
current: currentMockIdNum,
|
|
747
|
+
available: mockIds,
|
|
748
|
+
total: configs.length,
|
|
749
|
+
filtered: filteredConfigs.length
|
|
750
|
+
});
|
|
751
|
+
} catch (error) {
|
|
752
|
+
res.status(500).json({
|
|
753
|
+
error: '获取 mockId 列表失败',
|
|
754
|
+
message: error.message
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
/**
|
|
760
|
+
* 切换 mockId
|
|
761
|
+
*/
|
|
762
|
+
app.post('/mock-admin/mockid/:mockId', async (req, res) => {
|
|
763
|
+
const { mockId: newMockId } = req.params;
|
|
764
|
+
|
|
765
|
+
try {
|
|
766
|
+
const folderPath = getMockFolderPath(newMockId);
|
|
767
|
+
if (!folderPath) {
|
|
768
|
+
return res.status(400).json({
|
|
769
|
+
error: '切换 mockId 失败',
|
|
770
|
+
message: `未找到 mockId ${newMockId} 对应的接口集`
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// 加载新的 mock 文件
|
|
775
|
+
getMockFiles(newMockId);
|
|
776
|
+
|
|
777
|
+
// 更新 mockId
|
|
778
|
+
currentMockId = newMockId;
|
|
779
|
+
|
|
780
|
+
console.log(`\n🔄 已切换 mockId 到 ${newMockId} (${folderPath})`);
|
|
781
|
+
|
|
782
|
+
res.json({
|
|
783
|
+
success: true,
|
|
784
|
+
message: `已切换到 mockId ${newMockId}`,
|
|
785
|
+
mockId: newMockId,
|
|
786
|
+
path: folderPath
|
|
787
|
+
});
|
|
788
|
+
} catch (error) {
|
|
789
|
+
res.status(400).json({
|
|
790
|
+
error: '切换 mockId 失败',
|
|
791
|
+
message: error.message
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
/**
|
|
797
|
+
* 启用/禁用接口
|
|
798
|
+
*/
|
|
799
|
+
app.post('/mock-admin/interfaces/:interfaceName/toggle', (req, res) => {
|
|
800
|
+
try {
|
|
801
|
+
const { interfaceName } = req.params;
|
|
802
|
+
const { enabled } = req.body;
|
|
803
|
+
|
|
804
|
+
if (enabled === true || enabled === 'true') {
|
|
805
|
+
disabledInterfaces.delete(interfaceName);
|
|
806
|
+
console.log(`✅ 接口 ${interfaceName} 已启用`);
|
|
807
|
+
res.json({
|
|
808
|
+
success: true,
|
|
809
|
+
message: `接口 ${interfaceName} 已启用`,
|
|
810
|
+
enabled: true
|
|
811
|
+
});
|
|
812
|
+
} else {
|
|
813
|
+
disabledInterfaces.add(interfaceName);
|
|
814
|
+
console.log(`🚫 接口 ${interfaceName} 已禁用`);
|
|
815
|
+
res.json({
|
|
816
|
+
success: true,
|
|
817
|
+
message: `接口 ${interfaceName} 已禁用`,
|
|
818
|
+
enabled: false
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
} catch (error) {
|
|
822
|
+
res.status(400).json({
|
|
823
|
+
error: '切换接口状态失败',
|
|
824
|
+
message: error.message
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
/**
|
|
830
|
+
* 获取所有接口的场景配置
|
|
831
|
+
*/
|
|
832
|
+
app.get('/mock-admin/scenarios', (req, res) => {
|
|
833
|
+
const scenarios = {};
|
|
834
|
+
Object.keys(mockScenarios).forEach(key => {
|
|
835
|
+
scenarios[key] = mockScenarios[key];
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
// 获取所有接口及其可用场景
|
|
839
|
+
const availableScenarios = {};
|
|
840
|
+
|
|
841
|
+
// 收集所有已加载的接口名称(不包含变体)
|
|
842
|
+
const interfaceNames = new Set();
|
|
843
|
+
Object.keys(cachedMockFiles).forEach(key => {
|
|
844
|
+
// 提取基础接口名(去掉 -2, -3 等后缀)
|
|
845
|
+
const baseName = key.replace(/-\d+$/, '');
|
|
846
|
+
interfaceNames.add(baseName);
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
// 为每个接口查找可用场景(包括只有一个场景的接口)
|
|
850
|
+
interfaceNames.forEach(interfaceName => {
|
|
851
|
+
const scenariosForInterface = [];
|
|
852
|
+
const isDisabled = disabledInterfaces.has(interfaceName);
|
|
853
|
+
|
|
854
|
+
// 检查默认场景(基础名称)
|
|
855
|
+
if (cachedMockFiles[interfaceName]) {
|
|
856
|
+
const currentScenario = mockScenarios[interfaceName];
|
|
857
|
+
const isActive = currentScenario === undefined || currentScenario === null;
|
|
858
|
+
|
|
859
|
+
scenariosForInterface.push({
|
|
860
|
+
id: 'default',
|
|
861
|
+
name: interfaceName,
|
|
862
|
+
label: interfaceName,
|
|
863
|
+
filePath: mockFilePaths[interfaceName] || null,
|
|
864
|
+
isActive
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
// 检查变体场景(-2, -3, ...)
|
|
869
|
+
for (let i = 2; i <= 10; i++) {
|
|
870
|
+
const scenarioInterfaceName = `${interfaceName}-${i}`;
|
|
871
|
+
if (cachedMockFiles[scenarioInterfaceName]) {
|
|
872
|
+
const currentScenario = mockScenarios[interfaceName];
|
|
873
|
+
const isActive = currentScenario === String(i);
|
|
874
|
+
|
|
875
|
+
scenariosForInterface.push({
|
|
876
|
+
id: String(i),
|
|
877
|
+
name: scenarioInterfaceName,
|
|
878
|
+
label: scenarioInterfaceName,
|
|
879
|
+
filePath: mockFilePaths[scenarioInterfaceName] || null,
|
|
880
|
+
isActive
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// 即使只有一个场景也返回(显示所有接口)
|
|
886
|
+
if (scenariosForInterface.length > 0) {
|
|
887
|
+
availableScenarios[interfaceName] = {
|
|
888
|
+
scenarios: scenariosForInterface,
|
|
889
|
+
disabled: isDisabled,
|
|
890
|
+
hasMultipleScenarios: scenariosForInterface.length > 1
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
});
|
|
894
|
+
|
|
895
|
+
const currentMockPath = currentMockId ? getMockFolderPath(currentMockId) : null;
|
|
896
|
+
|
|
897
|
+
res.json({
|
|
898
|
+
current: scenarios,
|
|
899
|
+
available: availableScenarios,
|
|
900
|
+
disabled: Array.from(disabledInterfaces),
|
|
901
|
+
mockId: currentMockId,
|
|
902
|
+
mockPath: currentMockPath
|
|
903
|
+
});
|
|
904
|
+
});
|
|
905
|
+
|
|
906
|
+
/**
|
|
907
|
+
* 切换场景(支持所有接口)
|
|
908
|
+
*/
|
|
909
|
+
app.post('/mock-admin/scenarios/:interfaceName', (req, res) => {
|
|
910
|
+
const { interfaceName } = req.params;
|
|
911
|
+
const { scenario } = req.body;
|
|
912
|
+
|
|
913
|
+
// 如果 scenario 为空或 'default',则清除配置,使用默认
|
|
914
|
+
if (!scenario || scenario === 'default' || scenario === '1') {
|
|
915
|
+
delete mockScenarios[interfaceName];
|
|
916
|
+
console.log(`\n🔄 已切换 ${interfaceName} 到默认场景`);
|
|
917
|
+
return res.json({
|
|
918
|
+
success: true,
|
|
919
|
+
message: `已切换到默认场景`,
|
|
920
|
+
current: null
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// 检查场景是否存在
|
|
925
|
+
const scenarioInterfaceName = `${interfaceName}-${scenario}`;
|
|
926
|
+
if (!cachedMockFiles[scenarioInterfaceName]) {
|
|
927
|
+
// 查找所有可用的场景
|
|
928
|
+
const available = [];
|
|
929
|
+
if (cachedMockFiles[interfaceName]) {
|
|
930
|
+
available.push({ id: 'default', name: interfaceName });
|
|
931
|
+
}
|
|
932
|
+
for (let i = 2; i <= 10; i++) {
|
|
933
|
+
const name = `${interfaceName}-${i}`;
|
|
934
|
+
if (cachedMockFiles[name]) {
|
|
935
|
+
available.push({ id: String(i), name });
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
return res.status(404).json({
|
|
940
|
+
error: `场景 ${scenario} 不存在`,
|
|
941
|
+
available
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
// 设置场景
|
|
946
|
+
mockScenarios[interfaceName] = scenario;
|
|
947
|
+
console.log(`\n🔄 已切换 ${interfaceName} 到场景 ${scenario} (${scenarioInterfaceName})`);
|
|
948
|
+
|
|
949
|
+
res.json({
|
|
950
|
+
success: true,
|
|
951
|
+
message: `已切换到场景 ${scenario}`,
|
|
952
|
+
current: scenario
|
|
953
|
+
});
|
|
954
|
+
});
|
|
955
|
+
|
|
956
|
+
/**
|
|
957
|
+
* 提供 Web 管理界面
|
|
958
|
+
*/
|
|
959
|
+
app.get('/mock-admin', (req, res) => {
|
|
960
|
+
const htmlPath = path.join(__dirname, 'templates', 'mock-admin.html');
|
|
961
|
+
if (fs.existsSync(htmlPath)) {
|
|
962
|
+
const html = fs.readFileSync(htmlPath, 'utf-8');
|
|
963
|
+
res.send(html);
|
|
964
|
+
} else {
|
|
965
|
+
res.status(404).send('Web 界面文件未找到');
|
|
966
|
+
}
|
|
967
|
+
});
|
|
968
|
+
|
|
969
|
+
// 通配符路由必须在最后,避免拦截 API 路由
|
|
970
|
+
// 支持所有 HTTP 方法
|
|
971
|
+
// app.get('*', handleRequest);
|
|
972
|
+
// app.post('*', handleRequest);
|
|
973
|
+
// app.put('*', handleRequest);
|
|
974
|
+
// app.delete('*', handleRequest);
|
|
975
|
+
// app.patch('*', handleRequest);
|
|
976
|
+
// app.options('*', handleRequest);
|
|
977
|
+
// app.head('*', handleRequest);
|
|
978
|
+
app.all('*', handleRequest);
|
|
979
|
+
|
|
980
|
+
// 启动服务器
|
|
981
|
+
function startServer(mockId) {
|
|
982
|
+
currentMockId = mockId;
|
|
983
|
+
|
|
984
|
+
// 预加载 Mock 文件
|
|
985
|
+
if (mockId) {
|
|
986
|
+
getMockFiles(mockId);
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
app.listen(PORT, () => {
|
|
990
|
+
console.log(`\n${colors.green}${colors.bright}🚀 服务器已启动${colors.reset}`);
|
|
991
|
+
console.log(`${colors.cyan}📡${colors.reset} ${colors.dim}监听端口:${colors.reset} ${colors.bright}${PORT}${colors.reset}`);
|
|
992
|
+
console.log(`${colors.cyan}📡${colors.reset} ${colors.dim}接收地址:${colors.reset} ${colors.blue}http://localhost:${PORT}${colors.reset}`);
|
|
993
|
+
console.log(`${colors.magenta}🌐${colors.reset} ${colors.dim}Web 管理界面:${colors.reset} ${colors.blue}http://localhost:${PORT}/mock-admin${colors.reset}`);
|
|
994
|
+
if (mockId) {
|
|
995
|
+
console.log(`${colors.yellow}📦${colors.reset} ${colors.dim}使用接口集 ID:${colors.reset} ${colors.bright}${mockId}${colors.reset}`);
|
|
996
|
+
}
|
|
997
|
+
console.log(`\n${colors.dim}等待 Proxyman 转发请求...${colors.reset}\n`);
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
// 从环境变量或命令行参数获取 mockId
|
|
1002
|
+
// 优先使用环境变量(由 start 命令设置)
|
|
1003
|
+
const mockId = process.env.MOCK_ID || process.argv[2];
|
|
1004
|
+
if (mockId) {
|
|
1005
|
+
startServer(mockId);
|
|
1006
|
+
} else {
|
|
1007
|
+
console.warn('⚠️ 未指定 mockId,使用默认值 1');
|
|
1008
|
+
console.warn('💡 使用方法: jacky-proxy start <mockId>');
|
|
1009
|
+
startServer('1');
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
// 优雅关闭
|
|
1013
|
+
process.on('SIGTERM', () => {
|
|
1014
|
+
console.log('\n收到 SIGTERM 信号,正在关闭服务器...');
|
|
1015
|
+
process.exit(0);
|
|
1016
|
+
});
|
|
1017
|
+
|
|
1018
|
+
process.on('SIGINT', () => {
|
|
1019
|
+
console.log('\n收到 SIGINT 信号,正在关闭服务器...');
|
|
1020
|
+
process.exit(0);
|
|
1021
|
+
});
|
|
1022
|
+
|