-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapi-dot.js
More file actions
2148 lines (1889 loc) · 87.7 KB
/
Copy pathapi-dot.js
File metadata and controls
2148 lines (1889 loc) · 87.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/**
* API-DOT - API Detection and Optimization Tool
* 用于扫描项目中的API端点并进行连接测试
*/
const ApiDot = (() => {
// 配置项
const config = {
timeout: 3000, // API请求超时时间(毫秒)
maxDepth: 3, // 文件扫描最大深度
mockFallback: true, // 连接失败时是否使用模拟数据
forceMock: false, // 强制使用模拟数据
autoDetect: true, // 是否自动检测当前工作目录
projectType: null, // 项目类型(react、vue、angular等)
includeExtensions: [".js", ".jsx", ".ts", ".tsx", ".html", ".vue", ".php", ".py", ".java", ".cs", ".go"], // 要扫描的文件扩展名
excludePatterns: ["node_modules", ".git", "dist", "build", ".cache"], // 排除的目录
theme: "dark", // 主题模式
testMethod: "head", // API测试方法
exportFormat: "json", // 默认导出格式
autoExport: false, // 是否自动导出
autoTest: true, // 是否自动测试API
apiPatterns: [ // API调用模式的正则表达式
// JavaScript/TypeScript Fetch API
{ pattern: /fetch\(['"](https?:\/\/[^'"]+)['"]/, group: 1, method: 'GET', language: 'js' },
{ pattern: /fetch\(['"](https?:\/\/[^'"]+)['"].*?method:\s*['"]([A-Z]+)['"]/, group: 1, methodGroup: 2, language: 'js' },
{ pattern: /fetch\(['"](https?:\/\/[^'"]+)['"].*?method:\s*[^'"]([A-Z]+)[^'"]/, group: 1, methodGroup: 2, language: 'js' },
// Axios
{ pattern: /axios\.(get|post|put|delete|patch)\(['"](https?:\/\/[^'"]+)['"]/, group: 2, methodGroup: 1, language: 'js' },
{ pattern: /axios\(['"](https?:\/\/[^'"]+)['"].*?method:\s*['"]([A-Z]+)['"]/, group: 1, methodGroup: 2, language: 'js' },
{ pattern: /axios\(\{[^}]*url:\s*['"]([^'"]+)['"]/, group: 1, method: 'GET', language: 'js' },
{ pattern: /axios\(\{[^}]*url:\s*['"]([^'"]+)['"].*?method:\s*['"]([A-Z]+)['"]/, group: 1, methodGroup: 2, language: 'js' },
// jQuery AJAX
{ pattern: /\$\.ajax\(\{[^}]*url:\s*['"]([^'"]+)['"]/, group: 1, method: 'GET', language: 'js' },
{ pattern: /\$\.ajax\(\{[^}]*url:\s*['"]([^'"]+)['"].*?type:\s*['"]([A-Z]+)['"]/, group: 1, methodGroup: 2, language: 'js' },
{ pattern: /\$\.ajax\(\{[^}]*url:\s*['"]([^'"]+)['"].*?method:\s*['"]([A-Z]+)['"]/, group: 1, methodGroup: 2, language: 'js' },
{ pattern: /\$\.get\(['"](https?:\/\/[^'"]+)['"]/, group: 1, method: 'GET', language: 'js' },
{ pattern: /\$\.post\(['"](https?:\/\/[^'"]+)['"]/, group: 1, method: 'POST', language: 'js' },
// XMLHttpRequest
{ pattern: /new\s+XMLHttpRequest\(\)[^}]*\.open\(['"](GET|POST|PUT|DELETE|PATCH)['"],\s*['"]([^'"]+)['"]/, group: 2, methodGroup: 1, language: 'js' },
// React/Next.js
{ pattern: /getServerSideProps[^{]*\{[^}]*fetch\(['"](https?:\/\/[^'"]+)['"]/, group: 1, method: 'GET', language: 'js' },
{ pattern: /useSWR\(['"](https?:\/\/[^'"]+)['"]/, group: 1, method: 'GET', language: 'js' },
{ pattern: /useQuery\(['"](https?:\/\/[^'"]+)['"]/, group: 1, method: 'GET', language: 'js' },
// Angular HttpClient
{ pattern: /http\.(get|post|put|delete|patch)\(['"](https?:\/\/[^'"]+)['"]/, group: 2, methodGroup: 1, language: 'js' },
{ pattern: /httpClient\.(get|post|put|delete|patch)\(['"](https?:\/\/[^'"]+)['"]/, group: 2, methodGroup: 1, language: 'js' },
// Vue/Nuxt
{ pattern: /this\.\$axios\.(get|post|put|delete|patch)\(['"](https?:\/\/[^'"]+)['"]/, group: 2, methodGroup: 1, language: 'js' },
{ pattern: /\$fetch\(['"](https?:\/\/[^'"]+)['"]/, group: 1, method: 'GET', language: 'js' },
// GraphQL
{ pattern: /useQuery\(\s*\(?gql`[^`]*`[^)]*\)/, isGraphQL: true, method: 'POST', language: 'js' },
{ pattern: /useMutation\(\s*\(?gql`[^`]*`[^)]*\)/, isGraphQL: true, method: 'POST', language: 'js' },
{ pattern: /(query|mutation)\s+\w+\s*\([^)]*\)\s*\{/, isGraphQL: true, method: 'POST', language: 'js' },
// Python requests
{ pattern: /requests\.(get|post|put|delete|patch)\(['"]([^'"]+)['"]/, group: 2, methodGroup: 1, language: 'py' },
{ pattern: /requests\.request\(['"]([A-Z]+)['"],\s*['"]([^'"]+)['"]/, group: 2, methodGroup: 1, language: 'py' },
// PHP curl
{ pattern: /curl_init\(['"]([^'"]+)['"]/, group: 1, method: 'GET', language: 'php' },
{ pattern: /curl_setopt\(\$ch,\s*CURLOPT_URL,\s*['"]([^'"]+)['"]/, group: 1, method: 'GET', language: 'php' },
// C# HttpClient
{ pattern: /HttpClient\(\).*(GetAsync|GetStringAsync)\(['"]([^'"]+)['"]/, group: 2, method: 'GET', language: 'cs' },
{ pattern: /HttpClient\(\).*(PostAsync|PutAsync|DeleteAsync)\(['"]([^'"]+)['"]/, group: 2, methodGroup: 1, language: 'cs' },
// Go http
{ pattern: /http\.(Get|Post|Put|Delete)\(['"]([^'"]+)['"]/, group: 2, methodGroup: 1, language: 'go' },
{ pattern: /http\.NewRequest\(['"]([A-Z]+)['"],\s*['"]([^'"]+)['"]/, group: 2, methodGroup: 1, language: 'go' },
// REST API Controllers
{ pattern: /\@(Get|Post|Put|Delete|Patch)\(['"]([^'"]+)['"]/, group: 2, methodGroup: 1, isController: true, language: 'ts' },
{ pattern: /\@RequestMapping\(.*value\s*=\s*['"]([^'"]+)['"].*method\s*=\s*RequestMethod\.([A-Z]+)/, group: 1, methodGroup: 2, isController: true, language: 'java' },
// Base URLs and API configuration
{ pattern: /baseURL:\s*['"]([^'"]+)['"]/, group: 1, isConfig: true, language: 'js' },
{ pattern: /BASE_URL\s*=\s*['"]([^'"]+)['"]/, group: 1, isConfig: true, language: 'js' },
{ pattern: /API_URL\s*=\s*['"]([^'"]+)['"]/, group: 1, isConfig: true, language: 'js' },
{ pattern: /env\.([^\s.]+_URL)\s*=\s*['"]([^'"]+)['"]/, group: 2, isConfig: true, language: 'js' },
// Relative endpoints
{ pattern: /\.(get|post|put|delete|patch)\(['"]([^\s'"]+)['"]/, group: 2, methodGroup: 1, isRelative: true, language: 'js' },
{ pattern: /\/api\/([\w-\/]+)/, group: 0, isRelative: true, language: 'js' }
],
apiBaseUrls: [], // 扫描到的API基础URL
issuePatterns: [ // 检测常见问题的模式
{
pattern: /http:\/\/(?!localhost|127\.0\.0\.1)/,
issue: {
type: '安全问题',
severity: 'high',
description: '使用不安全的HTTP协议',
solution: '将HTTP URL替换为HTTPS URL以确保安全'
}
},
{
pattern: /"[^"]*token[^"]*":\s*"[^"]{5,}"/i,
issue: {
type: '安全问题',
severity: 'high',
description: '发现硬编码的API令牌',
solution: '使用环境变量或安全存储来管理敏感凭证'
}
},
{
pattern: /fetch\([^\)]*\)(?!\s*\.then|\s*\.catch)/,
issue: {
type: '质量问题',
severity: 'medium',
description: 'API调用缺少错误处理',
solution: '添加.then()和.catch()处理成功和错误情况'
}
},
{
pattern: /axios\([^\)]*\)(?!\s*\.then|\s*\.catch)/,
issue: {
type: '质量问题',
severity: 'medium',
description: 'API调用缺少错误处理',
solution: '添加.then()和.catch()处理成功和错误情况'
}
},
{
pattern: /password["']?\s*:\s*["']([^"\']+)["']/i,
issue: {
type: '安全问题',
severity: 'critical',
description: '发现硬编码的密码',
solution: '使用环境变量或安全存储来管理敏感凭证'
}
},
{
pattern: /api[_-]?key["']?\s*:\s*["']([^"\']{10,})["']/i,
issue: {
type: '安全问题',
severity: 'high',
description: '发现硬编码的API Key',
solution: '使用环境变量或安全存储来管理敏感凭证'
}
},
{
pattern: /auth["']?\s*:\s*["']([^"\']+)["']/i,
issue: {
type: '安全问题',
severity: 'high',
description: '发现硬编码的认证信息',
solution: '使用环境变量或安全存储来管理敏感凭证'
}
},
{
pattern: /cors/i,
issue: {
type: '安全问题',
severity: 'medium',
description: '可能存在CORS问题',
solution: '检查CORS配置并确保安全'
}
},
{
pattern: /cache-control/i,
issue: {
type: '性能问题',
severity: 'low',
description: '可能需要优化缓存控制',
solution: '添加合适的Cache-Control头以提高性能'
}
},
{
pattern: /try\s*{[^}]*}\s*catch\s*\([^)]*\)\s*{\s*}/,
issue: {
type: '质量问题',
severity: 'medium',
description: '可能存在空的catch块',
solution: '在catch块中添加错误处理代码'
}
},
{
pattern: /Authorization:\s*["']?Bearer\s+([^\s"']+)/i,
issue: {
type: '安全问题',
severity: 'high',
description: '发现硬编码的Bearer令牌',
solution: '使用环境变量或安全存储来管理敏感凭证'
}
},
{
pattern: /setTimeout\(\s*[^,]+,\s*(\d{5,})/,
issue: {
type: '性能问题',
severity: 'medium',
description: '可能存在超时问题',
solution: '检查并优化超时时间'
}
},
{
pattern: /retries|retry/i,
issue: {
type: '可靠性问题',
severity: 'info',
description: '可能需要重试机制',
solution: '添加重试机制以提高可靠性'
}
},
{
pattern: /url:\s*`/,
issue: {
type: '安全问题',
severity: 'medium',
description: '可能存在URL注入风险',
solution: '检查并确保URL安全'
}
}
],
};
// 状态管理
const state = {
isScanning: false, // 是否正在扫描
projectDir: null, // 项目目录句柄
endpoints: [], // 扫描到的API端点
testResults: { // 测试结果
total: 0,
passed: 0,
failed: 0,
pending: 0,
startTime: null,
endTime: null
},
logs: [], // 日志信息
servers: { // 服务器状态管理
backend: {
running: false,
url: 'http://localhost:8080',
name: '后端',
process: null,
startCommand: '',
lastCheck: null
},
frontend: {
running: false,
url: 'http://localhost:3000',
name: '前端',
process: null,
startCommand: '',
lastCheck: null
}
}
};
// 语言模块
const langModule = {
// 当前语言
current: localStorage.getItem('apidot-language') || 'zh',
// 翻译字典
translations: {
'zh': {
'dashboard': '总览',
'dashboard-title': 'API-DOT',
'endpoints': 'API端点',
'endpoints-title': 'API端点列表',
'endpoints-list': '所有API端点',
'issues': '问题诊断',
'issues-title': '问题诊断',
'issues-list': '检测到的问题',
'logs': '日志记录',
'logs-title': '日志记录',
'logs-list': '日志列表',
'settings': '设置',
'settings-title': '设置',
'general-settings': '通用设置',
'scan-settings': '扫描设置',
'export-settings': '导出设置',
'test-settings': '测试设置',
'theme': '主题',
'language-setting': '语言',
'scan-depth': '扫描深度',
'request-timeout': '请求超时(毫秒)',
'exclude-patterns': '排除模式',
'export-format': '默认导出格式',
'auto-export': '自动导出',
'auto-test': '自动测试API连接',
'test-method': '测试方法',
'save-settings': '保存设置',
'reset-settings': '重置设置',
'scan-button': '扫描项目',
'total-endpoints': 'API端点总数',
'total-issues': '发现问题总数',
'health-score': 'API健康评分',
'server-status': '服务器状态',
'backend-server': '后端服务器',
'frontend-server': '前端服务器',
'check-status': '检查状态',
'start-server': '启动',
'restart-server': '重启',
'stop-server': '停止',
'use-mock-data': '使用模拟数据',
'export-json': '导出报告 (JSON)',
'export-txt': '导出报告 (TXT)',
'recent-endpoints': '最近扫描的API端点',
'endpoint-url': 'URL',
'endpoint-method': '方法',
'endpoint-status': '状态',
'endpoint-issues': '问题',
'endpoint-actions': '操作',
'endpoint-details': '端点详情',
'endpoint-issues-list': '问题列表',
'issue-type': '问题类型',
'issue-severity': '严重程度',
'issue-endpoint': '相关端点',
'issue-solution': '解决方案',
'log-time': '日志时间',
'log-content': '日志内容',
'language': 'EN/中文',
'mock-toggle': '模拟数据模式',
'file-drop-hint': '拖放项目文件夹或点击选择',
'file-drop-sub': '我们将扫描项目中的API端点',
'view-details': '查看详情',
'api-test-result': 'API测试结果',
'scanning': '正在扫描...',
'test-result': '测试结果',
'not-found': '未找到',
'loading': '加载中...',
'mock-data-desc': '启用后将使用模拟数据进行测试,关闭则使用真实API数据',
'auto-detect': '自动检测项目',
'auto-detect-desc': '启动时自动检测并分析当前目录下的项目结构',
'update-url': '更新'
},
'en': {
'dashboard': 'Dashboard',
'dashboard-title': 'API-DOT',
'endpoints': 'API Endpoints',
'endpoints-title': 'API Endpoints List',
'endpoints-list': 'All API Endpoints',
'issues': 'Issues',
'issues-title': 'Issue Diagnostics',
'issues-list': 'Detected Issues',
'logs': 'Logs',
'logs-title': 'Log Records',
'logs-list': 'Log List',
'settings': 'Settings',
'settings-title': 'Settings',
'general-settings': 'General Settings',
'scan-settings': 'Scan Settings',
'export-settings': 'Export Settings',
'test-settings': 'Test Settings',
'theme': 'Theme',
'language-setting': 'Language',
'scan-depth': 'Scan Depth',
'request-timeout': 'Request Timeout(ms)',
'exclude-patterns': 'Exclude Patterns',
'export-format': 'Default Export Format',
'auto-export': 'Auto Export',
'auto-test': 'Auto Test API Connection',
'test-method': 'Test Method',
'save-settings': 'Save Settings',
'reset-settings': 'Reset Settings',
'scan-button': 'Scan Project',
'total-endpoints': 'Total API Endpoints',
'total-issues': 'Total Issues Found',
'health-score': 'API Health Score',
'server-status': 'Server Status',
'backend-server': 'Backend Server',
'frontend-server': 'Frontend Server',
'check-status': 'Check Status',
'start-server': 'Start',
'restart-server': 'Restart',
'stop-server': 'Stop',
'use-mock-data': 'Use Mock Data',
'export-json': 'Export Report (JSON)',
'export-txt': 'Export Report (TXT)',
'recent-endpoints': 'Recently Scanned Endpoints',
'endpoint-url': 'URL',
'endpoint-method': 'Method',
'endpoint-status': 'Status',
'endpoint-issues': 'Issues',
'endpoint-actions': 'Actions',
'endpoint-details': 'Endpoint Details',
'endpoint-issues-list': 'Issues List',
'issue-type': 'Issue Type',
'issue-severity': 'Severity',
'issue-endpoint': 'Related Endpoint',
'issue-solution': 'Solution',
'log-time': 'Time',
'log-content': 'Content',
'language': '中文/EN',
'mock-toggle': 'Mock Data Mode',
'file-drop-hint': 'Drop project folder here or click to select',
'file-drop-sub': 'We will scan your project for API endpoints',
'view-details': 'View Details',
'api-test-result': 'API Test Results',
'scanning': 'Scanning...',
'test-result': 'Test Result',
'not-found': 'Not Found',
'loading': 'Loading...',
'mock-data-desc': 'When enabled, mock data will be used for testing. When disabled, real API data will be used.',
'auto-detect': 'Auto Detect Project',
'auto-detect-desc': 'Automatically detect and analyze project structure in current directory when started',
'update-url': 'Update'
}
},
// 切换语言
switchTo: function(lang) {
console.log('ApiDot.lang.switchTo:', lang);
if (!this.translations[lang]) return;
this.current = lang;
localStorage.setItem('apidot-language', lang);
try {
// 更新所有标有data-lang-key属性的元素的文本内容
const elements = document.querySelectorAll('[data-lang-key]');
console.log(`Found ${elements.length} elements with data-lang-key attribute`);
elements.forEach(element => {
const key = element.getAttribute('data-lang-key');
if (this.translations[lang] && this.translations[lang][key]) {
element.textContent = this.translations[lang][key];
} else {
console.warn(`No translation found for key: ${key} in language: ${lang}`);
}
});
// 更新语言切换按钮文本
const langButton = document.getElementById('lang-text');
if (langButton) {
const langText = lang === 'zh' ? 'EN/中文' : '中文/EN';
langButton.textContent = langText;
}
} catch (error) {
console.error('Error switching language:', error);
}
// 更新语言切换状态
if (typeof log === 'function') {
log(`语言已切换为 ${lang === 'zh' ? '中文' : 'English'}`, 'info');
}
},
// 初始化
init: function() {
console.log('Initializing language system with language:', this.current);
// 添加语言切换按钮事件
const langToggle = document.getElementById('lang-toggle');
if (langToggle) {
langToggle.addEventListener('click', () => {
const newLang = this.current === 'zh' ? 'en' : 'zh';
this.switchTo(newLang);
});
console.log('Language toggle button initialized');
} else {
console.error('Language toggle button not found');
}
// 应用当前语言
setTimeout(() => {
this.switchTo(this.current);
}, 300);
}
};
// 日志工具
function log(message, type = 'info') {
const logItem = {
message,
type,
timestamp: new Date().toISOString()
};
state.logs.push(logItem);
console.log(`[${type.toUpperCase()}] ${message}`);
// 向UI发送日志
if (window.updateLog) {
window.updateLog(logItem);
}
}
// 选择项目文件夹
async function selectProjectFolder() {
try {
state.isScanning = true;
log('请选择项目文件夹...', 'info');
// 使用文件系统访问API
state.projectDir = await window.showDirectoryPicker();
log(`已选择文件夹: ${state.projectDir.name}`, 'success');
// 开始扫描项目
await scanProject();
} catch (error) {
// 如果用户取消选择或出现其他错误
log(`无法访问项目文件夹: ${error.message}`, 'error');
loadExampleData(); // 加载示例数据作为备选
} finally {
state.isScanning = false;
updateUI();
}
}
// 处理拖放的文件/文件夹
async function handleDroppedFiles(files) {
try {
state.isScanning = true;
log('正在处理拖放的文件...', 'info');
if (files.length === 0) {
throw new Error('未检测到文件');
}
// 检查拖放的是否是文件夹
const items = [];
for (let i = 0; i < files.length; i++) {
if (files[i].webkitGetAsEntry) {
const entry = files[i].webkitGetAsEntry();
if (entry.isDirectory) {
items.push(entry);
}
} else {
// 创建临时FileSystem对象来处理文件
const tmpDir = await navigator.storage.getDirectory();
for (const file of files) {
const newHandle = await tmpDir.getFileHandle(file.name, { create: true });
const writable = await newHandle.createWritable();
await writable.write(file);
await writable.close();
items.push(newHandle);
}
}
}
if (items.length > 0) {
log(`已检测到 ${items.length} 个文件/文件夹`, 'success');
// 如果拖放的是文件夹,尝试使用文件系统API访问
try {
state.projectDir = await window.showDirectoryPicker();
log(`已选择文件夹: ${state.projectDir.name}`, 'success');
await scanProject();
} catch (error) {
// 如果无法使用文件系统API,直接解析文件内容
state.endpoints = [];
// 处理文件
for (const file of files) {
try {
const content = await file.text();
const fileExt = '.' + file.name.split('.').pop().toLowerCase();
if (config.includeExtensions.includes(fileExt)) {
log(`正在解析文件: ${file.name}`, 'info');
analyzeFileContent(content, file.name, getLanguageFromExt(fileExt));
}
} catch (e) {
log(`无法读取文件 ${file.name}: ${e.message}`, 'warning');
}
}
// 规范化和去重API端点
normalizeEndpoints();
// 测试API连接
await testApiConnections();
log(`扫描完成!发现 ${state.endpoints.length} 个API端点`, 'success');
}
} else {
throw new Error('未检测到有效的文件或文件夹');
}
} catch (error) {
log(`处理文件失败: ${error.message}`, 'error');
loadExampleData(); // 加载示例数据作为备选
} finally {
state.isScanning = false;
updateUI();
}
}
// 自动扫描当前目录
async function autoScanCurrentDirectory() {
try {
// 获取当前目录URL信息
const currentUrl = window.location.href;
log(`正在自动扫描当前目录: ${currentUrl}`, 'info');
// 先查找常见的API文件
const rootDir = currentUrl.substring(0, currentUrl.lastIndexOf('/'));
log(`探测项目根目录: ${rootDir}`, 'info');
// 在页面加载时直接触发文件选择对话框(更好的用户体验)
log('请选择项目根目录...', 'info');
try {
await selectProjectFolder();
} catch (e) {
// 如果自动选择失败,使用示例数据
log('自动扫描失败,使用示例数据', 'warning');
loadExampleData();
}
} catch (error) {
log(`自动扫描失败: ${error.message}`, 'error');
loadExampleData();
}
}
// 扫描项目文件
async function scanProject() {
if (!state.projectDir) return;
state.endpoints = [];
state.testResults = {
total: 0,
passed: 0,
failed: 0,
pending: 0,
startTime: new Date(),
endTime: null
};
log('开始扫描项目文件...', 'info');
try {
// 扫描文件
await scanDirectory(state.projectDir, 0);
// 规范化和去重API端点
normalizeEndpoints();
// 测试API连接
await testApiConnections();
state.testResults.endTime = new Date();
const duration = (state.testResults.endTime - state.testResults.startTime) / 1000;
log(`扫描完成!发现 ${state.endpoints.length} 个API端点,用时 ${duration} 秒`, 'success');
} catch (error) {
log(`扫描过程中出错: ${error.message}`, 'error');
loadExampleData(); // 加载示例数据作为备选
}
}
// 递归扫描目录
async function scanDirectory(dirHandle, depth) {
if (depth > config.maxDepth) return;
for await (const entry of dirHandle.values()) {
try {
if (entry.kind === 'directory') {
// 跳过node_modules和.git等目录
if (config.excludePatterns.includes(entry.name)) {
continue;
}
const subDirHandle = await dirHandle.getDirectoryHandle(entry.name);
await scanDirectory(subDirHandle, depth + 1);
} else if (entry.kind === 'file') {
// 检查文件扩展名
const fileName = entry.name.toLowerCase();
const fileExt = '.' + fileName.split('.').pop();
if (config.includeExtensions.includes(fileExt)) {
await scanFile(entry, dirHandle, getLanguageFromExt(fileExt));
}
}
} catch (error) {
log(`无法处理 ${entry.name}: ${error.message}`, 'warning');
}
}
}
// 根据扩展名判断语言
function getLanguageFromExt(ext) {
const langMap = {
'.js': 'js',
'.jsx': 'js',
'.ts': 'js',
'.tsx': 'js',
'.vue': 'js',
'.html': 'html',
'.php': 'php',
'.py': 'py',
'.java': 'java',
'.cs': 'cs',
'.go': 'go'
};
return langMap[ext] || 'js'; // 默认为js
}
// 扫描单个文件
async function scanFile(fileEntry, dirHandle, language) {
try {
const fileHandle = await dirHandle.getFileHandle(fileEntry.name);
const file = await fileHandle.getFile();
const content = await file.text();
// 分析文件内容
analyzeFileContent(content, file.name, language);
} catch (error) {
log(`无法读取文件 ${fileEntry.name}: ${error.message}`, 'warning');
}
}
// 分析文件内容,提取API端点
function analyzeFileContent(content, fileName, language) {
// 首先查找API基础URL配置
for (const pattern of config.apiPatterns) {
if (pattern.isConfig && (!pattern.language || pattern.language === language)) {
const matches = content.match(new RegExp(pattern.pattern, 'g'));
if (matches) {
matches.forEach(match => {
const baseUrlMatch = match.match(pattern.pattern);
if (baseUrlMatch && baseUrlMatch[pattern.group]) {
const baseUrl = baseUrlMatch[pattern.group].trim();
if (!config.apiBaseUrls.includes(baseUrl) && isValidUrl(baseUrl)) {
config.apiBaseUrls.push(baseUrl);
log(`发现API基础URL: ${baseUrl}`, 'info');
}
}
});
}
}
}
// 然后查找API端点
for (const pattern of config.apiPatterns) {
// 跳过不匹配当前语言的模式
if (pattern.isConfig || (pattern.language && pattern.language !== language)) continue;
const matches = content.match(new RegExp(pattern.pattern, 'g'));
if (matches) {
matches.forEach(match => {
const urlMatch = match.match(pattern.pattern);
if (urlMatch && urlMatch[pattern.group]) {
let url = urlMatch[pattern.group].trim();
let method = pattern.method || 'GET'; // 默认为GET
// 如果模式包含方法组
if (pattern.methodGroup && urlMatch[pattern.methodGroup]) {
method = urlMatch[pattern.methodGroup].toUpperCase();
}
// 处理相对URL
if (pattern.isRelative && url && !url.includes('://')) {
// 尝试与已知的baseURL组合
if (config.apiBaseUrls.length > 0) {
config.apiBaseUrls.forEach(baseUrl => {
try {
const combinedUrl = new URL(url.startsWith('/') ? url : `/${url}`, baseUrl).toString();
addEndpoint(combinedUrl, method, fileName, match, language);
} catch (e) {
// 忽略无效URL
}
});
} else {
// 如果没有基础URL,仍然记录这个相对端点
addEndpoint(url, method, fileName, match, language, true);
}
} else if (url && isValidUrl(url)) {
// 处理完整URL
addEndpoint(url, method, fileName, match, language);
}
}
});
}
}
// 特殊情况:处理GraphQL请求
if (content.includes('graphql') || content.includes('apollo') || content.includes('gql`')) {
const graphqlEndpoints = extractGraphQLEndpoints(content);
graphqlEndpoints.forEach(endpoint => {
addEndpoint(endpoint.url, 'POST', fileName, endpoint.query, language, false, true);
});
}
}
// 提取GraphQL端点
function extractGraphQLEndpoints(content) {
const endpoints = [];
// 检测GraphQL URL
const urlPatterns = [
/new\s+ApolloClient\s*\(\s*\{[^}]*uri\s*:\s*['"]([^'"]+)['"]/,
/graphQLClient\s*\(\s*['"]([^'"]+)['"]/,
/endpoint\s*:\s*['"]([^'"]+graphql[^'"]*)['"]/,
];
let graphqlUrl = null;
for (const pattern of urlPatterns) {
const match = content.match(pattern);
if (match && match[1]) {
graphqlUrl = match[1];
break;
}
}
// 如果找到URL,提取查询
if (graphqlUrl) {
// 提取所有 gql` ` 模板字符串
const gqlPattern = /gql\s*`([^`]+)`/g;
let gqlMatch;
while ((gqlMatch = gqlPattern.exec(content)) !== null) {
if (gqlMatch[1]) {
endpoints.push({
url: graphqlUrl,
query: gqlMatch[0]
});
}
}
// 提取query和mutation关键字
const queryPattern = /(query|mutation)\s+(\w+)[^{]*\{/g;
let queryMatch;
while ((queryMatch = queryPattern.exec(content)) !== null) {
endpoints.push({
url: graphqlUrl,
query: queryMatch[0]
});
}
}
return endpoints;
}
// 检查URL是否有效
function isValidUrl(urlString) {
try {
// 检查完整URL
if (urlString.includes('://')) {
new URL(urlString);
return true;
}
// 如果是以/开头的相对路径也视为有效
return urlString.startsWith('/');
} catch (e) {
return false;
}
}
// 添加端点到列表
function addEndpoint(url, method, fileName, codeSnippet, language, isRelative = false, isGraphQL = false) {
// 检查是否已存在相同的端点
const existing = state.endpoints.find(e => e.url === url && e.method === method);
if (existing) return;
// 检测该端点的潜在问题
const issues = detectIssues(url, codeSnippet, isRelative, isGraphQL, language);
// 添加新端点
state.endpoints.push({
url,
method,
fileName,
language,
isRelative,
isGraphQL,
status: 'pending', // 初始状态为待测试
code: codeSnippet,
issues
});
log(`发现API端点: ${method} ${url}${isGraphQL ? ' (GraphQL)' : ''}`, 'info');
}
// 检测端点潜在问题
function detectIssues(url, codeSnippet, isRelative, isGraphQL, language) {
const issues = [];
// 基于URL检测问题
if (!isRelative) {
for (const pattern of config.issuePatterns) {
if (pattern.pattern.test(url) || pattern.pattern.test(codeSnippet)) {
issues.push({ ...pattern.issue });
}
}
// 检测HTTP协议安全问题
if (url.startsWith('http:') && !url.includes('localhost') && !url.includes('127.0.0.1')) {
issues.push({
type: '安全问题',
severity: 'high',
description: '使用不安全的HTTP协议',
solution: '将HTTP URL替换为HTTPS URL以确保安全'
});
}
}
// GraphQL特定问题
if (isGraphQL) {
// 检查是否有GraphQL验证
if (!codeSnippet.includes('validate') && !codeSnippet.includes('schema')) {
issues.push({
type: '安全问题',
severity: 'medium',
description: 'GraphQL查询可能缺少验证',
solution: '添加GraphQL查询验证和模式检查'
});
}
}
// 检查错误处理
if (language === 'js' && !isRelative) {
if (!codeSnippet.includes('.catch') && !codeSnippet.includes('try') &&
!codeSnippet.includes('onError') && !codeSnippet.includes('errorHandler')) {
issues.push({
type: '质量问题',
severity: 'medium',
description: 'API调用可能缺少错误处理',
solution: '添加错误处理机制,如try/catch或.catch()'
});
}
// 检查是否有硬编码敏感信息
if ((codeSnippet.includes('token') || codeSnippet.includes('key') || codeSnippet.includes('secret')) &&
(codeSnippet.includes('"') || codeSnippet.includes('\'')) &&
!codeSnippet.includes('process.env') && !codeSnippet.includes('import')) {
issues.push({
type: '安全问题',
severity: 'high',
description: '可能存在硬编码的敏感信息',
solution: '使用环境变量或安全存储来管理敏感凭证'
});
}
}
return issues;
}
// 规范化和去重端点
function normalizeEndpoints() {
// 规范化URL,去除查询参数等
state.endpoints = state.endpoints.map(endpoint => {
try {
const url = new URL(endpoint.url);
// 保留路径,去除查询参数和hash
const normalizedUrl = `${url.protocol}//${url.host}${url.pathname}`;
return { ...endpoint, url: normalizedUrl };
} catch (e) {
return endpoint;
}
});
// 去重
const uniqueEndpoints = [];
const seen = new Set();
state.endpoints.forEach(endpoint => {
const key = `${endpoint.method}:${endpoint.url}`;
if (!seen.has(key)) {
seen.add(key);
uniqueEndpoints.push(endpoint);
}
});
state.endpoints = uniqueEndpoints;
}
// 测试API连接
async function testApiConnections() {
if (state.endpoints.length === 0) return;
state.testResults.total = state.endpoints.length;
state.testResults.pending = state.endpoints.length;
state.testResults.passed = 0;
state.testResults.failed = 0;
log(`开始测试 ${state.endpoints.length} 个API端点...`, 'info');
// 更新UI以显示测试正在进行
updateUI();
// 并发测试所有端点
const testPromises = state.endpoints.map(endpoint => testEndpoint(endpoint));
await Promise.all(testPromises);
log(`测试完成: ${state.testResults.passed}/${state.testResults.total} 通过`, state.testResults.failed > 0 ? 'warning' : 'success');
}
// 测试单个端点
async function testEndpoint(endpoint) {
// 更新状态为测试中
endpoint.status = 'testing';
updateUI();
try {
// 如果启用了强制模拟数据模式,直接返回成功
if (config.forceMock) {
endpoint.status = 'success';
state.testResults.passed++;
log(`✅ ${endpoint.method} ${endpoint.url}: 成功 (模拟模式)`, 'success');
state.testResults.pending--;
updateUI();
return;
}