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
|
@@ -0,0 +1,813 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* migrate 命令实现
|
|
3
|
+
* 从 Raw 文件夹生成 Mock 数据文件
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const readline = require('readline');
|
|
9
|
+
|
|
10
|
+
// 配置 - 使用当前工作目录(用户运行命令的目录)
|
|
11
|
+
// 这样数据会生成到用户的工作目录,而不是 jacky-proxy 项目目录
|
|
12
|
+
const WORK_DIR = process.cwd();
|
|
13
|
+
const BASE_DATA_DIR = path.join(WORK_DIR, 'base-data');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 查找默认的 Raw 文件夹(以 .folder 结尾的文件夹)
|
|
17
|
+
* 在当前工作目录查找
|
|
18
|
+
*/
|
|
19
|
+
function findDefaultRawFolder() {
|
|
20
|
+
try {
|
|
21
|
+
const files = fs.readdirSync(WORK_DIR, { withFileTypes: true });
|
|
22
|
+
const folderFiles = files
|
|
23
|
+
.filter(dirent => dirent.isDirectory() && dirent.name.endsWith('.folder'))
|
|
24
|
+
.map(dirent => path.join(WORK_DIR, dirent.name));
|
|
25
|
+
|
|
26
|
+
if (folderFiles.length > 0) {
|
|
27
|
+
folderFiles.sort();
|
|
28
|
+
return folderFiles[0];
|
|
29
|
+
}
|
|
30
|
+
} catch (error) {
|
|
31
|
+
// 忽略错误,返回 null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 正则表达式
|
|
38
|
+
const SOA_FILE_PATTERN = /.*soa2_\d+_\w+\.txt$/;
|
|
39
|
+
const SOA_EXTRACT_PATTERN = /soa2_(\d+)_(\w+)/;
|
|
40
|
+
const INDEX_PATTERN = /^\[(\d+)\]\s+(Request|Response)/;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 从文件中提取 JSON 数据
|
|
44
|
+
*/
|
|
45
|
+
function extractJsonFromFile(filePath) {
|
|
46
|
+
try {
|
|
47
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
48
|
+
const lines = content.split('\n');
|
|
49
|
+
|
|
50
|
+
// 找到第一个空行后的第一个 {
|
|
51
|
+
let jsonStartIndex = -1;
|
|
52
|
+
for (let i = 0; i < lines.length; i++) {
|
|
53
|
+
if (lines[i].trim() === '' && i + 1 < lines.length) {
|
|
54
|
+
const nextLine = lines[i + 1].trim();
|
|
55
|
+
if (nextLine.startsWith('{')) {
|
|
56
|
+
jsonStartIndex = i + 1;
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 如果没找到空行,直接找第一个 {
|
|
63
|
+
if (jsonStartIndex === -1) {
|
|
64
|
+
for (let i = 0; i < lines.length; i++) {
|
|
65
|
+
if (lines[i].trim().startsWith('{')) {
|
|
66
|
+
jsonStartIndex = i;
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (jsonStartIndex === -1) {
|
|
73
|
+
console.warn(`警告: 在文件 ${filePath} 中未找到 JSON 数据`);
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 从 jsonStartIndex 开始提取 JSON
|
|
78
|
+
const jsonContent = lines.slice(jsonStartIndex).join('\n').trim();
|
|
79
|
+
|
|
80
|
+
// 验证 JSON 格式
|
|
81
|
+
try {
|
|
82
|
+
return JSON.parse(jsonContent);
|
|
83
|
+
} catch (e) {
|
|
84
|
+
console.error(`错误: 文件 ${filePath} 的 JSON 格式无效:`, e.message);
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
} catch (error) {
|
|
88
|
+
console.error(`错误: 读取文件 ${filePath} 失败:`, error.message);
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* 从文件名中提取接口信息
|
|
95
|
+
*/
|
|
96
|
+
function extractInterfaceInfo(filename) {
|
|
97
|
+
const indexMatch = filename.match(INDEX_PATTERN);
|
|
98
|
+
if (!indexMatch) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const index = parseInt(indexMatch[1], 10);
|
|
103
|
+
const type = indexMatch[2].toLowerCase();
|
|
104
|
+
|
|
105
|
+
const soaMatch = filename.match(SOA_EXTRACT_PATTERN);
|
|
106
|
+
if (!soaMatch) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const interfaceId = soaMatch[1];
|
|
111
|
+
let interfaceName = soaMatch[2];
|
|
112
|
+
|
|
113
|
+
if (interfaceName.startsWith('json_')) {
|
|
114
|
+
interfaceName = interfaceName.substring(5);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return { index, type, interfaceId, interfaceName };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* 扫描 Raw 文件夹,提取所有 SOA2 接口文件
|
|
122
|
+
*/
|
|
123
|
+
function scanRawFolder(rawFolderPath) {
|
|
124
|
+
const files = fs.readdirSync(rawFolderPath);
|
|
125
|
+
const soaFiles = [];
|
|
126
|
+
|
|
127
|
+
for (const file of files) {
|
|
128
|
+
if (!SOA_FILE_PATTERN.test(file)) {
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const info = extractInterfaceInfo(file);
|
|
133
|
+
if (!info) {
|
|
134
|
+
console.warn(`警告: 无法解析文件 ${file}`);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
soaFiles.push({
|
|
139
|
+
index: info.index,
|
|
140
|
+
type: info.type,
|
|
141
|
+
interfaceName: info.interfaceName,
|
|
142
|
+
filePath: path.join(rawFolderPath, file),
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return soaFiles;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* 按接口名和序号分组文件
|
|
151
|
+
*/
|
|
152
|
+
function groupFilesByInterface(files) {
|
|
153
|
+
const grouped = {};
|
|
154
|
+
|
|
155
|
+
for (const file of files) {
|
|
156
|
+
const { interfaceName, index, type, filePath } = file;
|
|
157
|
+
|
|
158
|
+
if (!grouped[interfaceName]) {
|
|
159
|
+
grouped[interfaceName] = {};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (!grouped[interfaceName][index]) {
|
|
163
|
+
grouped[interfaceName][index] = {};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
grouped[interfaceName][index][type] = filePath;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return grouped;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* 确保目录存在
|
|
174
|
+
*/
|
|
175
|
+
function ensureDirectoryExists(dirPath) {
|
|
176
|
+
if (!fs.existsSync(dirPath)) {
|
|
177
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* 保存 Mock 数据文件
|
|
183
|
+
*/
|
|
184
|
+
function saveMockData(interfaceName, baseName, dataPairs) {
|
|
185
|
+
const interfaceDir = path.join(BASE_DATA_DIR, interfaceName);
|
|
186
|
+
ensureDirectoryExists(interfaceDir);
|
|
187
|
+
|
|
188
|
+
const requestFiles = [];
|
|
189
|
+
const responseFiles = [];
|
|
190
|
+
|
|
191
|
+
dataPairs.sort((a, b) => a.index - b.index);
|
|
192
|
+
|
|
193
|
+
for (let i = 0; i < dataPairs.length; i++) {
|
|
194
|
+
const { request, response, index } = dataPairs[i];
|
|
195
|
+
const fileIndex = i + 1;
|
|
196
|
+
|
|
197
|
+
const responseFileName = `${baseName}-${fileIndex}.json`;
|
|
198
|
+
const responseFilePath = path.join(interfaceDir, responseFileName);
|
|
199
|
+
|
|
200
|
+
if (!fs.existsSync(responseFilePath)) {
|
|
201
|
+
fs.writeFileSync(responseFilePath, JSON.stringify(response, null, 2), 'utf-8');
|
|
202
|
+
console.log(` 保存响应文件: ${responseFileName}`);
|
|
203
|
+
} else {
|
|
204
|
+
console.log(` 跳过已存在的响应文件: ${responseFileName}`);
|
|
205
|
+
}
|
|
206
|
+
responseFiles.push(responseFileName);
|
|
207
|
+
|
|
208
|
+
const requestFileName = `${baseName}-${fileIndex}-request.json`;
|
|
209
|
+
const requestFilePath = path.join(interfaceDir, requestFileName);
|
|
210
|
+
|
|
211
|
+
if (!fs.existsSync(requestFilePath)) {
|
|
212
|
+
fs.writeFileSync(requestFilePath, JSON.stringify(request, null, 2), 'utf-8');
|
|
213
|
+
console.log(` 保存请求文件: ${requestFileName}`);
|
|
214
|
+
} else {
|
|
215
|
+
console.log(` 跳过已存在的请求文件: ${requestFileName}`);
|
|
216
|
+
}
|
|
217
|
+
requestFiles.push(requestFileName);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return { requestFiles, responseFiles };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* 查找项目根目录(包含 utils 目录的目录)
|
|
225
|
+
*/
|
|
226
|
+
function findProjectRoot() {
|
|
227
|
+
// 从当前文件所在目录开始向上查找
|
|
228
|
+
let currentDir = __dirname;
|
|
229
|
+
|
|
230
|
+
// 当前文件在 src/commands/migrate.js,所以需要向上查找
|
|
231
|
+
while (currentDir !== path.dirname(currentDir)) {
|
|
232
|
+
const utilsPath = path.join(currentDir, 'utils');
|
|
233
|
+
const packagePath = path.join(currentDir, 'package.json');
|
|
234
|
+
|
|
235
|
+
// 检查是否有 utils 目录或 package.json(jacky-proxy 项目)
|
|
236
|
+
if (fs.existsSync(utilsPath) || fs.existsSync(packagePath)) {
|
|
237
|
+
try {
|
|
238
|
+
if (fs.existsSync(packagePath)) {
|
|
239
|
+
const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf-8'));
|
|
240
|
+
if (packageJson.name === 'jacky-proxy') {
|
|
241
|
+
return currentDir;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
} catch (e) {
|
|
245
|
+
// 忽略解析错误,继续查找
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
currentDir = path.dirname(currentDir);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// 如果没找到,返回默认的项目根目录(从 __dirname 向上两级)
|
|
253
|
+
return path.resolve(__dirname, '../..');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* 生成 Mock 文件内容
|
|
258
|
+
*/
|
|
259
|
+
function generateMockFileContent(interfaceName, requestFiles, responseFiles, targetFolder) {
|
|
260
|
+
// 计算从 Mock 文件到 base-data 的相对路径(在工作目录中)
|
|
261
|
+
const folderParts = targetFolder.split(path.sep).filter(p => p);
|
|
262
|
+
const depth = folderParts.length;
|
|
263
|
+
const baseDataRelativePath = '../'.repeat(depth) + 'base-data';
|
|
264
|
+
|
|
265
|
+
// 计算从 Mock 文件到项目根目录的相对路径
|
|
266
|
+
const projectRoot = findProjectRoot();
|
|
267
|
+
const mockFileDir = path.join(WORK_DIR, targetFolder);
|
|
268
|
+
const utilsPath = path.join(projectRoot, 'utils/common');
|
|
269
|
+
|
|
270
|
+
// 计算相对路径
|
|
271
|
+
let utilsRelativePath = path.relative(mockFileDir, utilsPath);
|
|
272
|
+
|
|
273
|
+
// 将路径转换为使用 / 分隔符(适用于 import 语句)
|
|
274
|
+
let utilsImportPath = utilsRelativePath.split(path.sep).join('/');
|
|
275
|
+
|
|
276
|
+
// 如果路径不是以 . 开头,说明路径向上超出了工作目录
|
|
277
|
+
// 在这种情况下,我们需要使用绝对路径
|
|
278
|
+
// 使用环境变量 JACKY_PROXY_ROOT 来动态获取项目根目录
|
|
279
|
+
if (!utilsImportPath.startsWith('.')) {
|
|
280
|
+
// 使用环境变量 + 相对路径的方式
|
|
281
|
+
// 在 server.js 中会设置 JACKY_PROXY_ROOT 环境变量
|
|
282
|
+
utilsImportPath = `\${process.env.JACKY_PROXY_ROOT || '${projectRoot}'}/utils/common`;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const normalizedUtilsPath = utilsImportPath;
|
|
286
|
+
|
|
287
|
+
const imports = [];
|
|
288
|
+
for (let i = 0; i < requestFiles.length; i++) {
|
|
289
|
+
const requestFile = requestFiles[i];
|
|
290
|
+
const responseFile = responseFiles[i];
|
|
291
|
+
const num = i + 1;
|
|
292
|
+
imports.push(`import response${num} from '${baseDataRelativePath}/${interfaceName}/${responseFile}';`);
|
|
293
|
+
imports.push(`import request${num} from '${baseDataRelativePath}/${interfaceName}/${requestFile}';`);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const requestListItems = requestFiles.map((_, i) => `request${i + 1}`).join(', ');
|
|
297
|
+
const responseListItems = responseFiles.map((_, i) => `response${i + 1}`).join(', ');
|
|
298
|
+
|
|
299
|
+
return `/**
|
|
300
|
+
* ${interfaceName} 接口 Mock 文件
|
|
301
|
+
* 自动生成于 ${new Date().toISOString()}
|
|
302
|
+
*/
|
|
303
|
+
|
|
304
|
+
// 从 base-data 导入请求和响应数据
|
|
305
|
+
${imports.join('\n')}
|
|
306
|
+
|
|
307
|
+
const requestList = [${requestListItems}];
|
|
308
|
+
const responseList = [${responseListItems}];
|
|
309
|
+
|
|
310
|
+
// 使用动态路径导入,支持跨目录的模块解析
|
|
311
|
+
const path = require('path');
|
|
312
|
+
const matchResponsePath = process.env.JACKY_PROXY_ROOT
|
|
313
|
+
? path.join(process.env.JACKY_PROXY_ROOT, 'utils/common/match-response')
|
|
314
|
+
: path.resolve(__dirname, '${normalizedUtilsPath}/match-response');
|
|
315
|
+
const { matchResponse } = require(matchResponsePath);
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Mock 处理函数
|
|
319
|
+
*/
|
|
320
|
+
export default async (request: any) => {
|
|
321
|
+
const response = matchResponse(request, requestList, responseList, {
|
|
322
|
+
interfaceName: '${interfaceName}',
|
|
323
|
+
deepIgnore: true
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
return {
|
|
327
|
+
status: 200,
|
|
328
|
+
headers: { 'Content-Type': 'application/json' },
|
|
329
|
+
body: response,
|
|
330
|
+
};
|
|
331
|
+
};
|
|
332
|
+
`;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* 创建 Mock 文件
|
|
337
|
+
*/
|
|
338
|
+
function createMockFiles(targetFolder, interfaceData) {
|
|
339
|
+
const { interfaceName, requestFiles, responseFiles } = interfaceData;
|
|
340
|
+
|
|
341
|
+
const targetDir = path.join(WORK_DIR, targetFolder);
|
|
342
|
+
ensureDirectoryExists(targetDir);
|
|
343
|
+
|
|
344
|
+
let mockFileName = `${interfaceName}.mock.ts`;
|
|
345
|
+
let mockFilePath = path.join(targetDir, mockFileName);
|
|
346
|
+
|
|
347
|
+
if (fs.existsSync(mockFilePath)) {
|
|
348
|
+
let counter = 2;
|
|
349
|
+
while (fs.existsSync(path.join(targetDir, `${interfaceName}-${counter}.mock.ts`))) {
|
|
350
|
+
counter++;
|
|
351
|
+
}
|
|
352
|
+
mockFileName = `${interfaceName}-${counter}.mock.ts`;
|
|
353
|
+
mockFilePath = path.join(targetDir, mockFileName);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const mockContent = generateMockFileContent(
|
|
357
|
+
interfaceName,
|
|
358
|
+
requestFiles,
|
|
359
|
+
responseFiles,
|
|
360
|
+
targetFolder
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
fs.writeFileSync(mockFilePath, mockContent, 'utf-8');
|
|
364
|
+
|
|
365
|
+
console.log(`✓ 创建 Mock 文件: ${mockFileName}`);
|
|
366
|
+
|
|
367
|
+
return { mockFilePath };
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* 交互式输入函数
|
|
372
|
+
*/
|
|
373
|
+
function askQuestion(rl, question, defaultValue = '') {
|
|
374
|
+
return new Promise((resolve) => {
|
|
375
|
+
const prompt = defaultValue
|
|
376
|
+
? `${question} [默认: ${defaultValue}]: `
|
|
377
|
+
: `${question}: `;
|
|
378
|
+
|
|
379
|
+
rl.question(prompt, (answer) => {
|
|
380
|
+
const result = answer.trim() || defaultValue;
|
|
381
|
+
resolve(result);
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* 交互式收集参数
|
|
388
|
+
*/
|
|
389
|
+
async function collectParameters() {
|
|
390
|
+
process.stdout.write('═══════════════════════════════════════════════════════\n');
|
|
391
|
+
process.stdout.write(' 数据迁移脚本 - jacky-proxy 版本\n');
|
|
392
|
+
process.stdout.write('═══════════════════════════════════════════════════════\n');
|
|
393
|
+
process.stdout.write('\n');
|
|
394
|
+
|
|
395
|
+
const rl = readline.createInterface({
|
|
396
|
+
input: process.stdin,
|
|
397
|
+
output: process.stdout,
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
try {
|
|
401
|
+
console.log('💡 提示:场景名称示例:活动详情页秒杀场景、商品列表页、订单详情页等');
|
|
402
|
+
const scenarioName = await askQuestion(rl, '请输入场景名称', 'mock-case');
|
|
403
|
+
if (!scenarioName) {
|
|
404
|
+
console.error('错误: 场景名称不能为空');
|
|
405
|
+
rl.close();
|
|
406
|
+
process.exit(1);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const targetFolder = await askQuestion(rl, '请输入目标文件夹路径', 'mocks/test-folder');
|
|
410
|
+
if (!targetFolder) {
|
|
411
|
+
console.error('错误: 目标文件夹路径不能为空');
|
|
412
|
+
rl.close();
|
|
413
|
+
process.exit(1);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const DEFAULT_RAW_FOLDER = findDefaultRawFolder();
|
|
417
|
+
const defaultFolderDisplay = DEFAULT_RAW_FOLDER || '未找到(需要手动输入)';
|
|
418
|
+
const rawFolderPath = await askQuestion(rl, '请输入 Proxyman 下载后的文件夹路径(包含请求和响应)', defaultFolderDisplay);
|
|
419
|
+
|
|
420
|
+
console.log('');
|
|
421
|
+
|
|
422
|
+
return { scenarioName, targetFolder, rawFolderPath, rl };
|
|
423
|
+
} catch (error) {
|
|
424
|
+
rl.close();
|
|
425
|
+
throw error;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* 交互式选择要处理的接口
|
|
431
|
+
*/
|
|
432
|
+
async function selectInterfaces(rl, interfaceNames) {
|
|
433
|
+
console.log('');
|
|
434
|
+
console.log('═══════════════════════════════════════════════════════');
|
|
435
|
+
console.log(' 接口选择');
|
|
436
|
+
console.log('═══════════════════════════════════════════════════════');
|
|
437
|
+
console.log('');
|
|
438
|
+
console.log('找到以下接口:');
|
|
439
|
+
interfaceNames.forEach((name, index) => {
|
|
440
|
+
console.log(` ${index + 1}. ${name}`);
|
|
441
|
+
});
|
|
442
|
+
console.log('');
|
|
443
|
+
console.log('提示:');
|
|
444
|
+
console.log(' - 直接回车:处理所有接口');
|
|
445
|
+
console.log(' - 输入接口编号(用逗号分隔):如 1,3,5 表示只处理第1、3、5个接口');
|
|
446
|
+
console.log(' - 输入接口名(用逗号分隔):如 getProductInfo,saveLogInfo');
|
|
447
|
+
console.log(' - 输入 skip:接口名(用逗号分隔):如 skip:saveLogInfo 表示排除这些接口');
|
|
448
|
+
console.log('');
|
|
449
|
+
|
|
450
|
+
const answer = await askQuestion(rl, '请选择要处理的接口', 'all');
|
|
451
|
+
const trimmedAnswer = answer.trim().toLowerCase();
|
|
452
|
+
|
|
453
|
+
if (!trimmedAnswer || trimmedAnswer === 'all') {
|
|
454
|
+
return interfaceNames;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
let selectedInterfaces = [];
|
|
458
|
+
|
|
459
|
+
if (trimmedAnswer.startsWith('skip:')) {
|
|
460
|
+
const skipNames = trimmedAnswer.substring(5).split(',').map(s => s.trim());
|
|
461
|
+
selectedInterfaces = interfaceNames.filter(name => !skipNames.includes(name));
|
|
462
|
+
if (selectedInterfaces.length === 0) {
|
|
463
|
+
console.error('错误: 排除所有接口后没有可处理的接口');
|
|
464
|
+
rl.close();
|
|
465
|
+
process.exit(1);
|
|
466
|
+
}
|
|
467
|
+
console.log(`已排除: ${skipNames.join(', ')}`);
|
|
468
|
+
console.log(`将处理: ${selectedInterfaces.join(', ')}`);
|
|
469
|
+
} else if (/^\d+([,\d]+)?$/.test(trimmedAnswer.replace(/\s/g, ''))) {
|
|
470
|
+
const indices = trimmedAnswer.split(',').map(s => parseInt(s.trim(), 10) - 1);
|
|
471
|
+
const invalidIndices = indices.filter(idx => idx < 0 || idx >= interfaceNames.length);
|
|
472
|
+
if (invalidIndices.length > 0) {
|
|
473
|
+
console.error(`错误: 无效的接口编号: ${invalidIndices.map(i => i + 1).join(', ')}`);
|
|
474
|
+
rl.close();
|
|
475
|
+
process.exit(1);
|
|
476
|
+
}
|
|
477
|
+
selectedInterfaces = indices.map(idx => interfaceNames[idx]);
|
|
478
|
+
console.log(`将处理: ${selectedInterfaces.join(', ')}`);
|
|
479
|
+
} else {
|
|
480
|
+
const names = trimmedAnswer.split(',').map(s => s.trim());
|
|
481
|
+
const invalidNames = names.filter(name => !interfaceNames.includes(name));
|
|
482
|
+
if (invalidNames.length > 0) {
|
|
483
|
+
console.error(`错误: 无效的接口名: ${invalidNames.join(', ')}`);
|
|
484
|
+
rl.close();
|
|
485
|
+
process.exit(1);
|
|
486
|
+
}
|
|
487
|
+
selectedInterfaces = names;
|
|
488
|
+
console.log(`将处理: ${selectedInterfaces.join(', ')}`);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
console.log('');
|
|
492
|
+
return selectedInterfaces;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* 初始化 match-rules.json 配置文件(如果不存在)
|
|
497
|
+
* @param {Array<string>} interfaceNames - 接口名称列表
|
|
498
|
+
*/
|
|
499
|
+
function initMatchRulesConfig(interfaceNames = []) {
|
|
500
|
+
const configDir = path.join(WORK_DIR, 'config');
|
|
501
|
+
const configPath = path.join(configDir, 'match-rules.json');
|
|
502
|
+
|
|
503
|
+
let config = {
|
|
504
|
+
global: {
|
|
505
|
+
ignoreProps: [
|
|
506
|
+
"clientInfo",
|
|
507
|
+
"enviroment",
|
|
508
|
+
"head",
|
|
509
|
+
"tags",
|
|
510
|
+
"traceId",
|
|
511
|
+
"timestamp",
|
|
512
|
+
"ctime",
|
|
513
|
+
"cid",
|
|
514
|
+
"channelId",
|
|
515
|
+
"clientId"
|
|
516
|
+
],
|
|
517
|
+
description: "全局忽略属性列表,所有接口都会过滤这些随机参数"
|
|
518
|
+
},
|
|
519
|
+
interfaces: []
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
// 如果配置文件已存在,读取现有配置
|
|
523
|
+
if (fs.existsSync(configPath)) {
|
|
524
|
+
try {
|
|
525
|
+
const existingConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
526
|
+
config = existingConfig;
|
|
527
|
+
} catch (error) {
|
|
528
|
+
console.warn(`警告: 读取现有配置文件失败,将创建新配置: ${error.message}`);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// 确保 interfaces 数组存在
|
|
533
|
+
if (!config.interfaces) {
|
|
534
|
+
config.interfaces = [];
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// 为每个接口创建配置项(如果不存在)
|
|
538
|
+
const existingInterfaceNames = new Set(config.interfaces.map(i => i.interfaceName));
|
|
539
|
+
let addedCount = 0;
|
|
540
|
+
|
|
541
|
+
interfaceNames.forEach(interfaceName => {
|
|
542
|
+
if (!existingInterfaceNames.has(interfaceName)) {
|
|
543
|
+
config.interfaces.push({
|
|
544
|
+
interfaceName: interfaceName,
|
|
545
|
+
ignoreProps: [],
|
|
546
|
+
description: `${interfaceName} 接口的匹配规则,可在 ignoreProps 中添加需要忽略的属性`
|
|
547
|
+
});
|
|
548
|
+
addedCount++;
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
// 确保目录存在
|
|
553
|
+
ensureDirectoryExists(configDir);
|
|
554
|
+
|
|
555
|
+
// 保存配置文件
|
|
556
|
+
try {
|
|
557
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
558
|
+
if (addedCount > 0) {
|
|
559
|
+
console.log(`✓ 已更新匹配规则配置文件: ${configPath}`);
|
|
560
|
+
console.log(` 已添加 ${addedCount} 个接口配置,可在 config/match-rules.json 中配置接口匹配规则`);
|
|
561
|
+
} else if (!fs.existsSync(configPath) || interfaceNames.length === 0) {
|
|
562
|
+
console.log(`✓ 已创建匹配规则配置文件: ${configPath}`);
|
|
563
|
+
console.log(` 提示: 可以在 config/match-rules.json 中配置接口匹配规则`);
|
|
564
|
+
}
|
|
565
|
+
} catch (error) {
|
|
566
|
+
console.warn(`警告: 保存匹配规则配置文件失败: ${error.message}`);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* 更新 proxy.config.json,添加新的场景配置
|
|
572
|
+
*/
|
|
573
|
+
function updateMockConfig(targetFolder, scenarioName) {
|
|
574
|
+
const configPath = path.join(WORK_DIR, 'proxy.config.json');
|
|
575
|
+
let config = {
|
|
576
|
+
libraryId: 2773,
|
|
577
|
+
folders: {
|
|
578
|
+
list: []
|
|
579
|
+
}
|
|
580
|
+
};
|
|
581
|
+
|
|
582
|
+
// 如果配置文件存在,读取现有配置
|
|
583
|
+
if (fs.existsSync(configPath)) {
|
|
584
|
+
try {
|
|
585
|
+
const configContent = fs.readFileSync(configPath, 'utf-8');
|
|
586
|
+
config = JSON.parse(configContent);
|
|
587
|
+
} catch (error) {
|
|
588
|
+
console.warn(`警告: 读取配置文件失败,将创建新配置: ${error.message}`);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// 检查是否已存在相同的路径
|
|
593
|
+
const existingFolder = config.folders.list.find(f => f.path === targetFolder);
|
|
594
|
+
if (existingFolder) {
|
|
595
|
+
console.log(`✓ 配置已存在: ID ${existingFolder.id}, 路径: ${targetFolder}`);
|
|
596
|
+
return existingFolder.id;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// 计算新的 ID(使用最大 ID + 1)
|
|
600
|
+
const maxId = config.folders.list.length > 0
|
|
601
|
+
? Math.max(...config.folders.list.map(f => f.id))
|
|
602
|
+
: 0;
|
|
603
|
+
const newId = maxId + 1;
|
|
604
|
+
|
|
605
|
+
// 添加新配置
|
|
606
|
+
const folderName = scenarioName || path.basename(targetFolder);
|
|
607
|
+
config.folders.list.push({
|
|
608
|
+
id: newId,
|
|
609
|
+
path: targetFolder,
|
|
610
|
+
name: folderName
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
// 保存配置文件
|
|
614
|
+
try {
|
|
615
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
616
|
+
console.log(`✓ 已更新 proxy.config.json: 新增场景 ID ${newId} (${targetFolder})`);
|
|
617
|
+
return newId;
|
|
618
|
+
} catch (error) {
|
|
619
|
+
console.error(`错误: 保存配置文件失败: ${error.message}`);
|
|
620
|
+
return null;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* 执行迁移任务
|
|
626
|
+
*/
|
|
627
|
+
async function executeMigration(scenarioName, targetFolder, rawFolderPath, rl = null, ignoreInterfaces = []) {
|
|
628
|
+
if (!fs.existsSync(rawFolderPath)) {
|
|
629
|
+
console.error(`错误: Raw 文件夹不存在: ${rawFolderPath}`);
|
|
630
|
+
process.exit(1);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
console.log('开始迁移数据...');
|
|
634
|
+
console.log(`场景名称: ${scenarioName}`);
|
|
635
|
+
console.log(`目标文件夹: ${targetFolder}`);
|
|
636
|
+
console.log(`Raw 文件夹: ${rawFolderPath}`);
|
|
637
|
+
if (ignoreInterfaces.length > 0) {
|
|
638
|
+
console.log(`忽略接口: ${ignoreInterfaces.join(', ')}`);
|
|
639
|
+
}
|
|
640
|
+
console.log('');
|
|
641
|
+
|
|
642
|
+
console.log('扫描 Raw 文件夹...');
|
|
643
|
+
const files = scanRawFolder(rawFolderPath);
|
|
644
|
+
console.log(`找到 ${files.length} 个 SOA2 接口文件`);
|
|
645
|
+
|
|
646
|
+
if (files.length === 0) {
|
|
647
|
+
console.error('错误: 未找到任何 SOA2 接口文件');
|
|
648
|
+
process.exit(1);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
const grouped = groupFilesByInterface(files);
|
|
652
|
+
let allInterfaceNames = Object.keys(grouped);
|
|
653
|
+
|
|
654
|
+
// 应用 ignore 过滤
|
|
655
|
+
if (ignoreInterfaces.length > 0) {
|
|
656
|
+
allInterfaceNames = allInterfaceNames.filter(name => !ignoreInterfaces.includes(name));
|
|
657
|
+
console.log(`过滤后剩余 ${allInterfaceNames.length} 个接口`);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
console.log(`找到 ${allInterfaceNames.length} 个不同的接口: ${allInterfaceNames.join(', ')}`);
|
|
661
|
+
|
|
662
|
+
let interfaceNames = allInterfaceNames;
|
|
663
|
+
if (rl) {
|
|
664
|
+
interfaceNames = await selectInterfaces(rl, allInterfaceNames);
|
|
665
|
+
if (interfaceNames.length === 0) {
|
|
666
|
+
console.error('错误: 没有选择任何接口');
|
|
667
|
+
rl.close();
|
|
668
|
+
process.exit(1);
|
|
669
|
+
}
|
|
670
|
+
} else {
|
|
671
|
+
console.log('');
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
let sharedBaseName = scenarioName;
|
|
675
|
+
if (interfaceNames.length > 0) {
|
|
676
|
+
const firstInterfaceDir = path.join(BASE_DATA_DIR, interfaceNames[0]);
|
|
677
|
+
if (fs.existsSync(firstInterfaceDir)) {
|
|
678
|
+
const testFileName = path.join(firstInterfaceDir, `${scenarioName}-1.json`);
|
|
679
|
+
if (fs.existsSync(testFileName)) {
|
|
680
|
+
let counter = 1;
|
|
681
|
+
while (fs.existsSync(path.join(firstInterfaceDir, `${scenarioName}-${counter + 1}-1.json`))) {
|
|
682
|
+
counter++;
|
|
683
|
+
}
|
|
684
|
+
sharedBaseName = `${scenarioName}-${counter + 1}`;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
for (const interfaceName of interfaceNames) {
|
|
690
|
+
console.log(`处理接口: ${interfaceName}`);
|
|
691
|
+
|
|
692
|
+
const interfaceFiles = grouped[interfaceName];
|
|
693
|
+
const indices = Object.keys(interfaceFiles).map(Number).sort((a, b) => a - b);
|
|
694
|
+
|
|
695
|
+
const dataPairs = [];
|
|
696
|
+
|
|
697
|
+
for (const index of indices) {
|
|
698
|
+
const filePair = interfaceFiles[index];
|
|
699
|
+
|
|
700
|
+
if (!filePair.request || !filePair.response) {
|
|
701
|
+
console.warn(`警告: 序号 ${index} 的请求/响应文件不完整,跳过`);
|
|
702
|
+
continue;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
const requestData = extractJsonFromFile(filePair.request);
|
|
706
|
+
const responseData = extractJsonFromFile(filePair.response);
|
|
707
|
+
|
|
708
|
+
if (!requestData || !responseData) {
|
|
709
|
+
console.warn(`警告: 序号 ${index} 的数据提取失败,跳过`);
|
|
710
|
+
continue;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
dataPairs.push({ request: requestData, response: responseData, index });
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
if (dataPairs.length === 0) {
|
|
717
|
+
console.warn(`警告: 接口 ${interfaceName} 没有有效的数据对,跳过`);
|
|
718
|
+
continue;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
console.log(` 找到 ${dataPairs.length} 组有效数据`);
|
|
722
|
+
|
|
723
|
+
const { requestFiles, responseFiles } = saveMockData(
|
|
724
|
+
interfaceName,
|
|
725
|
+
sharedBaseName,
|
|
726
|
+
dataPairs
|
|
727
|
+
);
|
|
728
|
+
|
|
729
|
+
console.log(` 保存了 ${requestFiles.length} 个请求文件和 ${responseFiles.length} 个响应文件`);
|
|
730
|
+
|
|
731
|
+
createMockFiles(targetFolder, {
|
|
732
|
+
interfaceName,
|
|
733
|
+
requestFiles,
|
|
734
|
+
responseFiles,
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
console.log('');
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// 收集所有处理的接口名称(在处理完所有接口后)
|
|
741
|
+
const processedInterfaceNames = interfaceNames;
|
|
742
|
+
|
|
743
|
+
// 初始化 match-rules.json(如果不存在),并添加接口配置
|
|
744
|
+
console.log('');
|
|
745
|
+
console.log('初始化配置文件...');
|
|
746
|
+
initMatchRulesConfig(processedInterfaceNames);
|
|
747
|
+
|
|
748
|
+
// 更新 proxy.config.json
|
|
749
|
+
const scenarioId = updateMockConfig(targetFolder, scenarioName);
|
|
750
|
+
|
|
751
|
+
console.log('');
|
|
752
|
+
console.log('迁移完成!');
|
|
753
|
+
console.log('');
|
|
754
|
+
console.log('提示:');
|
|
755
|
+
console.log('1. Mock 数据已保存到 base-data/ 目录');
|
|
756
|
+
console.log('2. Mock 文件已创建在 ' + targetFolder + ' 目录');
|
|
757
|
+
if (scenarioId) {
|
|
758
|
+
console.log(`3. 已自动添加到 proxy.config.json,场景 ID: ${scenarioId}`);
|
|
759
|
+
console.log(`4. 可以使用以下命令启动: jacky-proxy start ${scenarioId}`);
|
|
760
|
+
} else {
|
|
761
|
+
console.log('3. 在 proxy.config.json 中添加接口集配置后即可使用');
|
|
762
|
+
}
|
|
763
|
+
console.log('5. 可以在 config/match-rules.json 中配置接口匹配规则');
|
|
764
|
+
|
|
765
|
+
return scenarioId;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/**
|
|
769
|
+
* migrate 命令主函数
|
|
770
|
+
*/
|
|
771
|
+
async function migrateCommand(options) {
|
|
772
|
+
const DEFAULT_RAW_FOLDER = findDefaultRawFolder();
|
|
773
|
+
|
|
774
|
+
let scenarioName, targetFolder, rawFolderPath, rl = null;
|
|
775
|
+
|
|
776
|
+
// 解析 ignore 参数
|
|
777
|
+
const ignoreInterfaces = options.ignore
|
|
778
|
+
? options.ignore.split(',').map(s => s.trim()).filter(s => s)
|
|
779
|
+
: [];
|
|
780
|
+
|
|
781
|
+
// 判断是否进入交互式模式
|
|
782
|
+
// 如果 interactive 为 true(默认),且用户没有提供任何参数,则进入交互式模式
|
|
783
|
+
const hasProvidedParams = !!(options.scenario || options.target || options.raw);
|
|
784
|
+
const shouldUseInteractive = options.interactive !== false && !hasProvidedParams;
|
|
785
|
+
|
|
786
|
+
if (shouldUseInteractive) {
|
|
787
|
+
// 完全交互式模式
|
|
788
|
+
const params = await collectParameters();
|
|
789
|
+
scenarioName = params.scenarioName;
|
|
790
|
+
targetFolder = params.targetFolder;
|
|
791
|
+
rawFolderPath = params.rawFolderPath;
|
|
792
|
+
rl = params.rl;
|
|
793
|
+
} else {
|
|
794
|
+
// 命令行参数模式
|
|
795
|
+
scenarioName = options.scenario || 'mock-case';
|
|
796
|
+
targetFolder = options.target || 'mocks/test-folder';
|
|
797
|
+
rawFolderPath = options.raw || DEFAULT_RAW_FOLDER;
|
|
798
|
+
|
|
799
|
+
if (!rawFolderPath) {
|
|
800
|
+
console.error('错误: 未找到 Raw 文件夹,请使用 --raw 参数指定');
|
|
801
|
+
process.exit(1);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
await executeMigration(scenarioName, targetFolder, rawFolderPath, rl, ignoreInterfaces);
|
|
806
|
+
|
|
807
|
+
if (rl) {
|
|
808
|
+
rl.close();
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
module.exports = migrateCommand;
|
|
813
|
+
|