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/tsconfig.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "commonjs",
5
+ "lib": ["ES2020"],
6
+ "outDir": "./dist",
7
+ "rootDir": "./",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "resolveJsonModule": true,
13
+ "moduleResolution": "node"
14
+ },
15
+ "include": ["**/*.ts"],
16
+ "exclude": ["node_modules", "dist"]
17
+ }
18
+
@@ -0,0 +1,491 @@
1
+ /**
2
+ * 请求匹配核心函数 - 根据请求 payload 动态匹配 Mock 响应
3
+ * 支持配置化的匹配规则(忽略属性、深度忽略、排序等)
4
+ */
5
+
6
+ import * as _ from 'lodash';
7
+ import * as fs from 'fs';
8
+ import * as path from 'path';
9
+
10
+ export interface MatchConfig {
11
+ interfaceName?: string; // 接口标识符(用于查找匹配规则配置)
12
+ essentialProps?: string[]; // 必需属性列表(不会被过滤)
13
+ deepIgnore?: boolean; // 是否深度忽略(递归删除嵌套属性)
14
+ bodyToObject?: boolean; // 是否将 body 字符串转为对象
15
+ sortProps?: Array<{ // 排序配置(用于数组排序)
16
+ prop: string;
17
+ compareKey: string;
18
+ }>;
19
+ needContainProps?: string[]; // 必须包含的属性
20
+ }
21
+
22
+ interface MatchRulesConfig {
23
+ global: {
24
+ ignoreProps: string[];
25
+ description?: string;
26
+ };
27
+ interfaces: Array<{
28
+ interfaceName: string;
29
+ ignoreProps?: string[];
30
+ essentialProps?: string[];
31
+ deepIgnore?: boolean;
32
+ sortProps?: Array<{ prop: string; compareKey: string }>;
33
+ deepMerge?: Record<string, any>;
34
+ channelSpecific?: {
35
+ [channel: string]: {
36
+ ignoreProps?: string[];
37
+ };
38
+ };
39
+ description?: string;
40
+ }>;
41
+ }
42
+
43
+ // 缓存匹配规则配置
44
+ let matchRulesConfig: MatchRulesConfig | null = null;
45
+
46
+ /**
47
+ * 加载匹配规则配置
48
+ * 配置文件从工作目录读取(WORK_DIR 环境变量或当前工作目录)
49
+ */
50
+ function loadMatchRules(): MatchRulesConfig {
51
+ if (!matchRulesConfig) {
52
+ try {
53
+ // 从工作目录读取配置文件
54
+ // 优先使用 WORK_DIR 环境变量,否则使用当前工作目录
55
+ const workDir = process.env.WORK_DIR || process.cwd();
56
+ const configPath = path.join(workDir, 'config/match-rules.json');
57
+
58
+ if (fs.existsSync(configPath)) {
59
+ const configContent = fs.readFileSync(configPath, 'utf-8');
60
+ matchRulesConfig = JSON.parse(configContent);
61
+ } else {
62
+ // 配置文件不存在时使用默认规则(不警告,因为可能用户还没有创建)
63
+ matchRulesConfig = {
64
+ global: { ignoreProps: [] },
65
+ interfaces: []
66
+ };
67
+ }
68
+ } catch (error) {
69
+ console.warn('无法加载匹配规则配置文件,使用默认规则', error);
70
+ matchRulesConfig = {
71
+ global: { ignoreProps: [] },
72
+ interfaces: []
73
+ };
74
+ }
75
+ }
76
+ return matchRulesConfig!; // 此时 matchRulesConfig 一定不为 null
77
+ }
78
+
79
+ /**
80
+ * 获取接口的匹配配置
81
+ */
82
+ function getMatchConfig(interfaceName: string): MatchRulesConfig['interfaces'][0] | null {
83
+ const config = loadMatchRules();
84
+ return config.interfaces.find(c => c.interfaceName === interfaceName) || null;
85
+ }
86
+
87
+ /**
88
+ * 深度忽略属性(递归删除嵌套对象中的指定属性)
89
+ */
90
+ function deepIgnoreProps(obj: any, propsToIgnore: string[]): any {
91
+ if (!obj || typeof obj !== 'object') {
92
+ return obj;
93
+ }
94
+
95
+ if (Array.isArray(obj)) {
96
+ return obj.map(item => deepIgnoreProps(item, propsToIgnore));
97
+ }
98
+
99
+ const result: any = {};
100
+ for (const key in obj) {
101
+ if (propsToIgnore.includes(key)) {
102
+ continue;
103
+ }
104
+ result[key] = deepIgnoreProps(obj[key], propsToIgnore);
105
+ }
106
+ return result;
107
+ }
108
+
109
+ /**
110
+ * 对数组进行排序
111
+ */
112
+ function sortArrayProps(obj: any, sortConfig: Array<{ prop: string; compareKey: string }>): any {
113
+ if (!obj || typeof obj !== 'object') {
114
+ return obj;
115
+ }
116
+
117
+ const result = { ...obj };
118
+ for (const config of sortConfig) {
119
+ if (result[config.prop] && Array.isArray(result[config.prop])) {
120
+ result[config.prop] = [...result[config.prop]].sort((a, b) => {
121
+ const aVal = a[config.compareKey];
122
+ const bVal = b[config.compareKey];
123
+ if (aVal < bVal) return -1;
124
+ if (aVal > bVal) return 1;
125
+ return 0;
126
+ });
127
+ }
128
+ }
129
+ return result;
130
+ }
131
+
132
+ /**
133
+ * 差异信息接口
134
+ */
135
+ export interface DiffInfo {
136
+ path: string; // 字段路径,如 "body.searchParams[0].id"
137
+ type: 'missing-in-mock' | 'missing-in-request' | 'value-different'; // 差异类型
138
+ requestValue?: any; // 请求中的值
139
+ mockValue?: any; // Mock 中的值
140
+ }
141
+
142
+ /**
143
+ * 获取两个对象的详细差异
144
+ * @param requestObj - 请求对象
145
+ * @param mockObj - Mock 对象
146
+ * @param pathPrefix - 路径前缀(用于递归)
147
+ * @returns 差异信息数组
148
+ */
149
+ function getDetailedDiff(
150
+ requestObj: any,
151
+ mockObj: any,
152
+ pathPrefix: string = ''
153
+ ): DiffInfo[] {
154
+ const diffs: DiffInfo[] = [];
155
+
156
+ // 处理 null/undefined 的情况
157
+ if (requestObj === null || requestObj === undefined) {
158
+ if (mockObj !== null && mockObj !== undefined) {
159
+ diffs.push({
160
+ path: pathPrefix || 'root',
161
+ type: 'missing-in-request',
162
+ mockValue: mockObj
163
+ });
164
+ }
165
+ return diffs;
166
+ }
167
+
168
+ if (mockObj === null || mockObj === undefined) {
169
+ if (requestObj !== null && requestObj !== undefined) {
170
+ diffs.push({
171
+ path: pathPrefix || 'root',
172
+ type: 'missing-in-mock',
173
+ requestValue: requestObj
174
+ });
175
+ }
176
+ return diffs;
177
+ }
178
+
179
+ // 处理基本类型
180
+ if (typeof requestObj !== 'object' || typeof mockObj !== 'object') {
181
+ if (requestObj !== mockObj) {
182
+ diffs.push({
183
+ path: pathPrefix || 'root',
184
+ type: 'value-different',
185
+ requestValue: requestObj,
186
+ mockValue: mockObj
187
+ });
188
+ }
189
+ return diffs;
190
+ }
191
+
192
+ // 处理数组
193
+ if (Array.isArray(requestObj) || Array.isArray(mockObj)) {
194
+ if (!Array.isArray(requestObj)) {
195
+ diffs.push({
196
+ path: pathPrefix || 'root',
197
+ type: 'value-different',
198
+ requestValue: requestObj,
199
+ mockValue: mockObj
200
+ });
201
+ return diffs;
202
+ }
203
+ if (!Array.isArray(mockObj)) {
204
+ diffs.push({
205
+ path: pathPrefix || 'root',
206
+ type: 'value-different',
207
+ requestValue: requestObj,
208
+ mockValue: mockObj
209
+ });
210
+ return diffs;
211
+ }
212
+
213
+ // 比较数组长度
214
+ const maxLength = Math.max(requestObj.length, mockObj.length);
215
+ for (let i = 0; i < maxLength; i++) {
216
+ const itemPath = pathPrefix ? `${pathPrefix}[${i}]` : `[${i}]`;
217
+ if (i >= requestObj.length) {
218
+ diffs.push({
219
+ path: itemPath,
220
+ type: 'missing-in-request',
221
+ mockValue: mockObj[i]
222
+ });
223
+ } else if (i >= mockObj.length) {
224
+ diffs.push({
225
+ path: itemPath,
226
+ type: 'missing-in-mock',
227
+ requestValue: requestObj[i]
228
+ });
229
+ } else {
230
+ // 递归比较数组元素
231
+ diffs.push(...getDetailedDiff(requestObj[i], mockObj[i], itemPath));
232
+ }
233
+ }
234
+ return diffs;
235
+ }
236
+
237
+ // 处理对象
238
+ const allKeys = new Set([
239
+ ...Object.keys(requestObj),
240
+ ...Object.keys(mockObj)
241
+ ]);
242
+
243
+ for (const key of allKeys) {
244
+ const currentPath = pathPrefix ? `${pathPrefix}.${key}` : key;
245
+ const requestValue = requestObj[key];
246
+ const mockValue = mockObj[key];
247
+
248
+ if (!(key in requestObj)) {
249
+ // 只在 mock 中存在
250
+ diffs.push({
251
+ path: currentPath,
252
+ type: 'missing-in-request',
253
+ mockValue: mockValue
254
+ });
255
+ } else if (!(key in mockObj)) {
256
+ // 只在请求中存在
257
+ diffs.push({
258
+ path: currentPath,
259
+ type: 'missing-in-mock',
260
+ requestValue: requestValue
261
+ });
262
+ } else {
263
+ // 都存在,递归比较
264
+ diffs.push(...getDetailedDiff(requestValue, mockValue, currentPath));
265
+ }
266
+ }
267
+
268
+ return diffs;
269
+ }
270
+
271
+ /**
272
+ * 格式化差异信息为可读字符串
273
+ */
274
+ export function formatDiffInfo(diffs: DiffInfo[]): string {
275
+ if (diffs.length === 0) {
276
+ return '无差异';
277
+ }
278
+
279
+ const sections: string[] = [];
280
+
281
+ // 按类型分组
282
+ const missingInMock = diffs.filter(d => d.type === 'missing-in-mock');
283
+ const missingInRequest = diffs.filter(d => d.type === 'missing-in-request');
284
+ const valueDifferent = diffs.filter(d => d.type === 'value-different');
285
+
286
+ if (missingInMock.length > 0) {
287
+ sections.push('\n❌ 只在请求中存在的字段(Mock 中缺失):');
288
+ missingInMock.forEach(diff => {
289
+ sections.push(` • ${diff.path}: ${JSON.stringify(diff.requestValue)}`);
290
+ });
291
+ }
292
+
293
+ if (missingInRequest.length > 0) {
294
+ sections.push('\n⚠️ 只在 Mock 中存在的字段(请求中缺失):');
295
+ missingInRequest.forEach(diff => {
296
+ sections.push(` • ${diff.path}: ${JSON.stringify(diff.mockValue)}`);
297
+ });
298
+ }
299
+
300
+ if (valueDifferent.length > 0) {
301
+ sections.push('\n🔴 值不同的字段:');
302
+ valueDifferent.forEach(diff => {
303
+ sections.push(` • ${diff.path}:`);
304
+ sections.push(` 请求值: ${JSON.stringify(diff.requestValue)}`);
305
+ sections.push(` Mock值: ${JSON.stringify(diff.mockValue)}`);
306
+ });
307
+ }
308
+
309
+ return sections.join('\n');
310
+ }
311
+
312
+ /**
313
+ * 处理请求对象(应用忽略、排序等规则)
314
+ */
315
+ function processRequest(
316
+ request: any,
317
+ ignoreProps: string[],
318
+ essentialProps: string[],
319
+ deepIgnore: boolean,
320
+ sortProps: Array<{ prop: string; compareKey: string }>
321
+ ): any {
322
+ let processed = { ...request };
323
+
324
+ // 处理 body
325
+ if (processed.body) {
326
+ let body = processed.body;
327
+
328
+ // 如果是字符串,尝试转换为对象
329
+ if (typeof body === 'string') {
330
+ try {
331
+ body = JSON.parse(body);
332
+ } catch (e) {
333
+ // 如果解析失败,保持原样
334
+ }
335
+ }
336
+
337
+ // 深度忽略属性
338
+ if (deepIgnore) {
339
+ body = deepIgnoreProps(body, ignoreProps);
340
+ } else {
341
+ // 浅层忽略属性
342
+ for (const prop of ignoreProps) {
343
+ if (!essentialProps.includes(prop)) {
344
+ delete body[prop];
345
+ }
346
+ }
347
+ }
348
+
349
+ // 排序数组属性
350
+ if (sortProps.length > 0) {
351
+ body = sortArrayProps(body, sortProps);
352
+ }
353
+
354
+ processed.body = body;
355
+ }
356
+
357
+ return processed;
358
+ }
359
+
360
+ /**
361
+ * 匹配请求并返回对应的响应
362
+ * @param request - 真实请求对象 { body, options: { headers } }
363
+ * @param requestList - Mock 请求列表(从 base-data 导入)
364
+ * @param responseList - Mock 响应列表(从 base-data 导入)
365
+ * @param options - 匹配配置选项
366
+ * @returns 匹配的响应数据,如果匹配失败则返回错误信息
367
+ */
368
+ export function matchResponse(
369
+ request: any,
370
+ requestList: any[],
371
+ responseList: any[],
372
+ options: MatchConfig = {}
373
+ ): any {
374
+ const {
375
+ interfaceName = '',
376
+ essentialProps = [],
377
+ deepIgnore = false,
378
+ bodyToObject = false,
379
+ sortProps = [],
380
+ needContainProps = []
381
+ } = options;
382
+
383
+ // 加载匹配规则配置
384
+ const config = loadMatchRules();
385
+ const interfaceConfig = getMatchConfig(interfaceName);
386
+
387
+ // 合并全局和接口级别的忽略属性
388
+ const globalIgnoreProps = config.global.ignoreProps || [];
389
+ const interfaceIgnoreProps = interfaceConfig?.ignoreProps || [];
390
+ const allIgnoreProps = [...globalIgnoreProps, ...interfaceIgnoreProps];
391
+
392
+ // 过滤掉必需属性
393
+ const ignoreProps = allIgnoreProps.filter(prop => !essentialProps.includes(prop));
394
+
395
+ // 应用接口级别的配置
396
+ const finalDeepIgnore = interfaceConfig?.deepIgnore !== undefined
397
+ ? interfaceConfig.deepIgnore
398
+ : deepIgnore;
399
+
400
+ const finalSortProps = interfaceConfig?.sortProps || sortProps;
401
+
402
+ // 处理真实请求
403
+ let processedRequest = processRequest(
404
+ request,
405
+ ignoreProps,
406
+ essentialProps,
407
+ finalDeepIgnore,
408
+ finalSortProps
409
+ );
410
+
411
+ // 处理 bodyToObject
412
+ if (bodyToObject && processedRequest.body && typeof processedRequest.body === 'string') {
413
+ try {
414
+ processedRequest.body = JSON.parse(processedRequest.body);
415
+ } catch (e) {
416
+ // 解析失败,保持原样
417
+ }
418
+ }
419
+
420
+ // 提取实际请求的 body 部分用于比较
421
+ // 如果请求有 body 字段,则使用 body;否则使用整个请求对象
422
+ let requestBodyForCompare = processedRequest.body !== undefined
423
+ ? processedRequest.body
424
+ : processedRequest;
425
+
426
+ // 检查必需属性
427
+ if (needContainProps.length > 0) {
428
+ const body = requestBodyForCompare || {};
429
+ for (const prop of needContainProps) {
430
+ if (!(prop in body)) {
431
+ return {
432
+ error: true,
433
+ message: `缺少必需属性: ${prop}`,
434
+ request: processedRequest
435
+ };
436
+ }
437
+ }
438
+ }
439
+
440
+ // 遍历 Mock 请求列表,找到匹配的请求
441
+ for (let i = 0; i < requestList.length; i++) {
442
+ const mockRequest = requestList[i];
443
+
444
+ // 如果 mock 请求有 body 字段,则使用 body;否则将整个请求作为 body 处理
445
+ const mockRequestForProcess = mockRequest.body !== undefined
446
+ ? mockRequest
447
+ : { body: mockRequest };
448
+
449
+ let processedMockRequest = processRequest(
450
+ mockRequestForProcess,
451
+ ignoreProps,
452
+ essentialProps,
453
+ finalDeepIgnore,
454
+ finalSortProps
455
+ );
456
+
457
+ // 提取 mock 请求的 body 部分用于比较
458
+ let mockBodyForCompare = processedMockRequest.body !== undefined
459
+ ? processedMockRequest.body
460
+ : processedMockRequest;
461
+
462
+ // 使用 lodash 的 isEqualWith 进行深度比较(只比较 body 部分)
463
+ const isMatch = _.isEqualWith(
464
+ requestBodyForCompare,
465
+ mockBodyForCompare,
466
+ (objValue, srcValue) => {
467
+ // 自定义比较逻辑可以在这里添加
468
+ return undefined; // 使用默认比较
469
+ }
470
+ );
471
+
472
+ if (isMatch) {
473
+ // 找到匹配的请求,返回对应的响应
474
+ return responseList[i] || null;
475
+ }
476
+ }
477
+
478
+ // 没有找到匹配的请求,返回错误信息
479
+ // 注意:详细差异信息不再打印到控制台,避免终端输出过多信息
480
+
481
+ return {
482
+ error: true,
483
+ message: '未找到匹配的 Mock 请求',
484
+ request: JSON.stringify(requestBodyForCompare),
485
+ mockRequests: JSON.stringify(requestList.map((req, index) => ({
486
+ index,
487
+ request: req
488
+ })))
489
+ };
490
+ }
491
+
@@ -0,0 +1,130 @@
1
+ /**
2
+ * 接口识别工具 - 从请求中提取接口标识符(通用方案)
3
+ * 支持多种识别策略:URL 路径、请求头、请求体、查询参数、自定义函数
4
+ */
5
+
6
+ import { Request } from 'express';
7
+
8
+ export interface IdentifierStrategy {
9
+ type: 'urlPattern' | 'header' | 'body' | 'query' | 'custom';
10
+ pattern?: string; // URL 正则表达式(用于 urlPattern)
11
+ group?: number; // 正则捕获组索引(用于 urlPattern)
12
+ key?: string; // 键名(用于 header、query)
13
+ path?: string; // 路径(用于 body,支持嵌套路径如 'a.b.c')
14
+ function?: (req: Request) => string | null; // 自定义函数(用于 custom)
15
+ description?: string; // 策略描述
16
+ }
17
+
18
+ export interface InterfaceIdentifierConfig {
19
+ strategies: IdentifierStrategy[];
20
+ }
21
+
22
+ /**
23
+ * 从嵌套对象中获取值
24
+ */
25
+ function getNestedValue(obj: any, path: string): any {
26
+ if (!obj || !path) return null;
27
+ const keys = path.split('.');
28
+ let value = obj;
29
+ for (const key of keys) {
30
+ if (value == null) return null;
31
+ value = value[key];
32
+ }
33
+ return value;
34
+ }
35
+
36
+ /**
37
+ * 应用单个识别策略
38
+ */
39
+ function applyStrategy(req: Request, strategy: IdentifierStrategy): string | null {
40
+ try {
41
+ switch (strategy.type) {
42
+ case 'urlPattern': {
43
+ if (!strategy.pattern) return null;
44
+ const path = req.path || req.url.split('?')[0];
45
+ const match = path.match(new RegExp(strategy.pattern));
46
+ if (match && match[strategy.group || 1]) {
47
+ return match[strategy.group || 1];
48
+ }
49
+ return null;
50
+ }
51
+
52
+ case 'header': {
53
+ if (!strategy.key) return null;
54
+ const value = req.headers[strategy.key] || req.headers[strategy.key.toLowerCase()];
55
+ return value ? String(value) : null;
56
+ }
57
+
58
+ case 'body': {
59
+ if (!strategy.path) return null;
60
+ const value = getNestedValue(req.body, strategy.path);
61
+ return value ? String(value) : null;
62
+ }
63
+
64
+ case 'query': {
65
+ if (!strategy.key) return null;
66
+ const value = req.query[strategy.key];
67
+ return value ? String(value) : null;
68
+ }
69
+
70
+ case 'custom': {
71
+ if (typeof strategy.function === 'function') {
72
+ return strategy.function(req);
73
+ }
74
+ return null;
75
+ }
76
+
77
+ default:
78
+ return null;
79
+ }
80
+ } catch (error) {
81
+ console.warn(`应用识别策略失败: ${strategy.type}`, error);
82
+ return null;
83
+ }
84
+ }
85
+
86
+ /**
87
+ * 获取默认识别策略(兜底方案)
88
+ */
89
+ function getDefaultStrategies(): IdentifierStrategy[] {
90
+ return [
91
+ {
92
+ type: 'urlPattern',
93
+ pattern: '/([^/]+)$',
94
+ group: 1,
95
+ description: '提取 URL 最后一段作为接口标识符'
96
+ }
97
+ ];
98
+ }
99
+
100
+ /**
101
+ * 从请求中提取接口标识符(通用方案)
102
+ * @param req - Express 请求对象
103
+ * @param config - 接口识别配置(可选)
104
+ * @returns 接口标识符,如果无法识别则返回 null
105
+ */
106
+ export function extractInterfaceIdentifier(
107
+ req: Request,
108
+ config?: InterfaceIdentifierConfig
109
+ ): string | null {
110
+
111
+ const strategies = config?.strategies || getDefaultStrategies();
112
+
113
+ // 按顺序尝试每个策略
114
+ for (const strategy of strategies) {
115
+ const identifier = applyStrategy(req, strategy);
116
+ if (identifier) {
117
+ return identifier;
118
+ }
119
+ }
120
+
121
+ // 如果所有策略都失败,尝试从 URL 路径的最后一段提取(兜底方案)
122
+ const path = req.path || req.url.split('?')[0];
123
+ const pathParts = path.split('/').filter(p => p);
124
+ if (pathParts.length > 0) {
125
+ return pathParts[pathParts.length - 1];
126
+ }
127
+
128
+ return null;
129
+ }
130
+