Repository navigation
Expand file tree
/
Copy path_worker.js
More file actions
4706 lines (4377 loc) · 217 KB
/
Copy path_worker.js
File metadata and controls
4706 lines (4377 loc) · 217 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
let __d1CounterSchemaReady = false;
async function d1EnsureSchema(db) {
if (__d1CounterSchemaReady) return;
await db.exec(
"CREATE TABLE IF NOT EXISTS visitors (ip TEXT PRIMARY KEY, first_seen INTEGER NOT NULL);" +
"CREATE TABLE IF NOT EXISTS online (ip TEXT PRIMARY KEY, last_seen INTEGER NOT NULL);" +
"CREATE INDEX IF NOT EXISTS idx_online_last_seen ON online(last_seen);" +
"CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value INTEGER NOT NULL);"
);
__d1CounterSchemaReady = true;
}
async function d1Heartbeat(db, ip) {
await d1EnsureSchema(db);
const nowSec = Math.floor(Date.now() / 1000);
await db.prepare(
"INSERT INTO online (ip, last_seen) VALUES (?1, ?2) " +
"ON CONFLICT(ip) DO UPDATE SET last_seen = excluded.last_seen " +
"WHERE online.last_seen < excluded.last_seen - 60"
).bind(ip, nowSec).run();
}
async function d1TrackAndGetStats(db, ip) {
await d1EnsureSchema(db);
const nowSec = Math.floor(Date.now() / 1000);
await db.prepare(
"INSERT INTO online (ip, last_seen) VALUES (?1, ?2) " +
"ON CONFLICT(ip) DO UPDATE SET last_seen = excluded.last_seen " +
"WHERE online.last_seen < excluded.last_seen - 60"
).bind(ip, nowSec).run();
const insRes = await db.prepare(
"INSERT OR IGNORE INTO visitors (ip, first_seen) VALUES (?1, ?2)"
).bind(ip, nowSec).run();
if (insRes && insRes.meta && insRes.meta.rows_written > 0) {
await db.prepare(
"INSERT INTO meta (key, value) VALUES ('uv', 1) " +
"ON CONFLICT(key) DO UPDATE SET value = value + 1"
).run();
}
const uvRow = await db.prepare("SELECT value FROM meta WHERE key = 'uv'").first();
const onlineRow = await db.prepare(
"SELECT COUNT(*) AS c FROM online WHERE last_seen > ?1"
).bind(nowSec - 120).first();
return {
visitCount: (uvRow && typeof uvRow.value === "number") ? uvRow.value : 0,
onlineCount: (onlineRow && typeof onlineRow.c === "number") ? onlineRow.c : 0
};
}
async function d1GetTotalVisitors(db) {
await d1EnsureSchema(db);
const row = await db.prepare("SELECT value FROM meta WHERE key = 'uv'").first();
return (row && typeof row.value === "number") ? row.value : 0;
}
const XDB_URLS = [
"https://raw.githubusercontent.com/lionsoul2014/ip2region/refs/heads/master/data/ip2region_v4.xdb",
"https://fastly.jsdelivr.net/gh/lionsoul2014/ip2region@master/data/ip2region_v4.xdb",
"https://cdn.jsdelivr.net/gh/lionsoul2014/ip2region@master/data/ip2region_v4.xdb"
];
const XDB_HEADER_LEN = 256;
const XDB_SEG_LEN = 14;
let __xdbBytes = null;
let __xdbDv = null;
let __xdbLoading = null;
let __xdbFailAt = 0;
async function loadXdb() {
if (__xdbBytes) return __xdbBytes;
if (__xdbLoading) return __xdbLoading;
if (__xdbFailAt && Date.now() - __xdbFailAt < 60000) {
throw new Error("ip2region 数据源暂不可用");
}
__xdbLoading = (async () => {
for (const u of XDB_URLS) {
try {
const res = await fetch(u, { cf: { cacheTtl: 604800, cacheEverything: true } });
if (!res.ok) continue;
const ab = await res.arrayBuffer();
if (ab.byteLength < 2000000) continue;
__xdbBytes = new Uint8Array(ab);
__xdbDv = new DataView(ab);
return __xdbBytes;
} catch (e) {
console.warn("[XDB] 拉取失败:", u, e && e.message);
}
}
__xdbFailAt = Date.now();
throw new Error("所有 ip2region 数据源均不可用");
})();
try {
return await __xdbLoading;
} finally {
__xdbLoading = null;
}
}
function ipv4ToUint32(ip) {
const parts = String(ip).split(".");
if (parts.length !== 4) return -1;
let n = 0;
for (let i = 0; i < 4; i++) {
if (parts[i] === "") return -1;
const p = Number(parts[i]);
if (!Number.isInteger(p) || p < 0 || p > 255) return -1;
n = (n << 8) + p;
}
return n >>> 0;
}
function xdbSearch(ip) {
const ipInt = ipv4ToUint32(ip);
if (ipInt < 0) return "";
const il0 = (ipInt >>> 24) & 0xFF;
const il1 = (ipInt >>> 16) & 0xFF;
const vi = XDB_HEADER_LEN + il0 * 2048 + il1 * 8;
const sPtr = __xdbDv.getUint32(vi, true);
const ePtr = __xdbDv.getUint32(vi + 4, true);
if (!sPtr || !ePtr) return "";
let l = 0, h = (ePtr - sPtr) / XDB_SEG_LEN;
while (l <= h) {
const m = (l + h) >> 1;
const off = sPtr + m * XDB_SEG_LEN;
const sip = __xdbDv.getUint32(off, true);
if (ipInt < sip) { h = m - 1; continue; }
const eip = __xdbDv.getUint32(off + 4, true);
if (ipInt > eip) { l = m + 1; continue; }
const dLen = __xdbDv.getUint16(off + 8, true);
const dPtr = __xdbDv.getUint32(off + 10, true);
return new TextDecoder("utf-8").decode(__xdbBytes.subarray(dPtr, dPtr + dLen));
}
return "";
}
function xdbRegionToLabel(region, countryMap) {
if (!region) return "通用";
const f = region.split("|");
const iso = (f[4] || "").toUpperCase();
if (!iso || iso === "0") return "通用";
if (iso === "CN") {
const prov = f[1] || "";
if (prov.indexOf("香港") >= 0) return "香港";
if (prov.indexOf("澳门") >= 0) return "澳门";
if (prov.indexOf("台湾") >= 0) return "台湾";
return "中国";
}
return countryMap[iso] || "通用";
}
const XDB_PRIVATE_CIDRS = [
"0.0.0.0/8", "10.0.0.0/8", "100.64.0.0/10", "127.0.0.0/8", "169.254.0.0/16",
"172.16.0.0/12", "192.0.0.0/24", "192.0.2.0/24", "192.168.0.0/16",
"198.18.0.0/15", "198.51.100.0/24", "203.0.113.0/24", "224.0.0.0/4", "240.0.0.0/4"
];
const XDB_PRIVATE_NETS = XDB_PRIVATE_CIDRS.map(function (cidr) {
const slash = cidr.indexOf("/");
const netIp = ipv4ToUint32(cidr.slice(0, slash));
const prefix = parseInt(cidr.slice(slash + 1), 10);
const mask = prefix === 0 ? 0 : (0xFFFFFFFF << (32 - prefix)) >>> 0;
return { net: netIp & mask, mask: mask };
});
function isPrivateIPv4(ip) {
const ipInt = ipv4ToUint32(ip);
if (ipInt < 0) return false;
for (const n of XDB_PRIVATE_NETS) {
if ((ipInt & n.mask) === n.net) return true;
}
return false;
}
const __dohCache = new Map();
async function dohResolve(domain) {
if (__dohCache.has(domain)) return __dohCache.get(domain);
const endpoints = [
"https://1.1.1.1/dns-query?name=" + encodeURIComponent(domain) + "&type=A",
"https://dns.google/resolve?name=" + encodeURIComponent(domain) + "&type=A"
];
for (const ep of endpoints) {
try {
const res = await fetch(ep, {
headers: { accept: "application/dns-json" },
cf: { cacheTtl: 300 }
});
if (!res.ok) continue;
const data = await res.json();
const answers = data.Answer || data.answer || [];
const a = answers.find(x => x.type === 1 && ipv4ToUint32(x.data) >= 0);
if (a) {
__dohCache.set(domain, a.data);
return a.data;
}
} catch (e) {}
}
__dohCache.set(domain, "");
return "";
}
async function lookupGeoTarget(target, countryMap) {
let ip = (ipv4ToUint32(target) >= 0) ? target : await dohResolve(target);
if (!ip) return { ip: "", label: "通用" };
if (isPrivateIPv4(ip)) return { ip, label: "通用" };
await loadXdb();
return { ip, label: xdbRegionToLabel(xdbSearch(ip), countryMap) };
}
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
const clientIp = request.headers.get("CF-Connecting-IP") || "127.0.0.1";
const COUNTRY_CN = {
CN:"中国", US:"美国", JP:"日本", KR:"韩国", SG:"新加坡", HK:"中国香港", TW:"中国台湾", MO:"中国澳门",
GB:"英国", DE:"德国", FR:"法国", RU:"俄罗斯", AU:"澳大利亚", CA:"加拿大", IN:"印度", BR:"巴西",
IT:"意大利", ES:"西班牙", MX:"墨西哥", ID:"印度尼西亚", TH:"泰国", VN:"越南", MY:"马来西亚", PH:"菲律宾",
TR:"土耳其", SA:"沙特阿拉伯", AE:"阿联酋", IL:"以色列", ZA:"南非", EG:"埃及", AR:"阿根廷", NL:"荷兰",
SE:"瑞典", CH:"瑞士", BE:"比利时", AT:"奥地利", NO:"挪威", DK:"丹麦", FI:"芬兰", PL:"波兰",
IE:"爱尔兰", PT:"葡萄牙", GR:"希腊", NZ:"新西兰", PK:"巴基斯坦", BD:"孟加拉国", NG:"尼日利亚", UA:"乌克兰",
CO:"哥伦比亚", CL:"智利", PE:"秘鲁", IR:"伊朗", IQ:"伊拉克", QA:"卡塔尔", KW:"科威特", OM:"阿曼",
JO:"约旦", LB:"黎巴嫩", SY:"叙利亚", YE:"也门", AF:"阿富汗", NP:"尼泊尔", LK:"斯里兰卡", MM:"缅甸",
KH:"柬埔寨", LA:"老挝", BN:"文莱", MN:"蒙古", KZ:"哈萨克斯坦", UZ:"乌兹别克斯坦", TM:"土库曼斯坦", KG:"吉尔吉斯斯坦",
TJ:"塔吉克斯坦", AZ:"阿塞拜疆", AM:"亚美尼亚", GE:"格鲁吉亚", BY:"白俄罗斯", MD:"摩尔多瓦", RO:"罗马尼亚", BG:"保加利亚",
HR:"克罗地亚", SI:"斯洛文尼亚", RS:"塞尔维亚", ME:"黑山", MK:"北马其顿", AL:"阿尔巴尼亚", BA:"波黑", XK:"科索沃",
EE:"爱沙尼亚", LV:"拉脱维亚", LT:"立陶宛", CZ:"捷克共和国", SK:"斯洛伐克", HU:"匈牙利", LU:"卢森堡", MC:"摩纳哥",
LI:"列支敦士登", AD:"安道尔", SM:"圣马力诺", VA:"梵蒂冈", MT:"马耳他", IS:"冰岛", CY:"塞浦路斯",
MA:"摩洛哥", DZ:"阿尔及利亚", TN:"突尼斯", LY:"利比亚", SD:"苏丹", SS:"南苏丹", ET:"埃塞俄比亚", SO:"索马里",
KE:"肯尼亚", TZ:"坦桑尼亚", UG:"乌干达", RW:"卢旺达", BI:"布隆迪", MZ:"莫桑比克", ZW:"津巴布韦", ZM:"赞比亚",
MW:"马拉维", AO:"安哥拉", NA:"纳米比亚", BW:"博茨瓦纳", LS:"莱索托", SZ:"斯威士兰", GM:"冈比亚", SN:"塞内加尔",
MR:"毛里塔尼亚", ML:"马里", BF:"布基纳法索", NE:"尼日尔", TD:"乍得", CF:"中非", CM:"喀麦隆", GQ:"赤道几内亚",
GA:"加蓬", CG:"刚果共和国", CD:"刚果民主共和国", ST:"圣多美和普林西比", GIN:"几内亚", SL:"塞拉利昂", LR:"利比里亚", CI:"科特迪瓦",
GH:"加纳", TG:"多哥", BJ:"贝宁", ER:"厄立特里亚", DJ:"吉布提", KM:"科摩罗", MU:"毛里求斯", SC:"塞舌尔",
CV:"佛得角", RE:"留尼汪", YT:"马约特", EH:"西撒哈拉",
VE:"委内瑞拉", EC:"厄瓜多尔", BO:"玻利维亚", PY:"巴拉圭", UY:"乌拉圭", GY:"圭亚那", SR:"苏里南", GF:"法属圭亚那",
CU:"古巴", JM:"牙买加", HT:"海地", DO:"多米尼加", PR:"波多黎各", TT:"特立尼达和多巴哥", PA:"巴拿马", CR:"哥斯达黎加",
NI:"尼加拉瓜", HN:"洪都拉斯", SV:"萨尔瓦多", GT:"危地马拉", BZ:"伯利兹", BS:"巴哈马", BB:"巴巴多斯", DM:"多米尼克",
LC:"圣卢西亚", VC:"圣文森特和格林纳丁斯", GD:"格林纳达", AG:"安提瓜和巴布达", KN:"圣基茨和尼维斯",
FJ:"斐济", PG:"巴布亚新几内亚", SB:"所罗门群岛", VU:"瓦努阿图", NC:"新喀里多尼亚", PF:"法属波利尼西亚", WS:"萨摩亚", TO:"汤加",
TV:"图瓦卢", KI:"基里巴斯", MH:"马绍尔群岛", FM:"密克罗尼西亚", PW:"帕劳", NR:"瑙鲁", GU:"关岛", AS:"美属萨摩亚",
VI:"美属维尔京群岛", PRI:"波多黎各(美)", AW:"阿鲁巴", CW:"库拉索", SX:"荷属圣马丁", BQ:"荷兰加勒比区",
BL:"圣巴泰勒米", MF:"法属圣马丁", PM:"圣皮埃尔和密克隆", GL:"格陵兰", FO:"法罗群岛", GI:"直布罗陀",
AX:"奥兰群岛", SH:"圣赫勒拿", FK:"马尔维纳斯群岛", GS:"南乔治亚", TF:"法属南部领地", HM:"赫德岛和麦克唐纳群岛",
UM:"美国本土外小岛屿", IO:"英属印度洋领地", BV:"布韦岛", CX:"圣诞岛", CC:"科科斯群岛", NF:"诺福克岛",
PN:"皮特凯恩群岛", CK:"库克群岛", NU:"纽埃", TK:"托克劳", WF:"瓦利斯和富图纳",
A1:"匿名代理", A2:"卫星提供商", O1:"其他国家/地区", XX:"未知国家/地区", T1:"中转区域"
};
const CITY_CN = {
"Beijing":"北京市","Shanghai":"上海市","Guangzhou":"广州市","Shenzhen":"深圳市","Tianjin":"天津市","Chongqing":"重庆市",
"Hong Kong":"香港","Hongkong":"香港","HongKong":"香港","Kowloon":"九龙","Macau":"澳门","Macao":"澳门","Taipei":"台北市",
"New Taipei":"新北市","Taichung":"台中市","Kaohsiung":"高雄市","Tainan":"台南市","Hsinchu":"新竹市","Keelung":"基隆市",
"Dongguan":"东莞市","Foshan":"佛山市","Zhongshan":"中山市","Zhuhai":"珠海市","Huizhou":"惠州市","Jiangmen":"江门市",
"Shantou":"汕头市","Zhaoqing":"肇庆市","Zhanjiang":"湛江市","Maoming":"茂名市","Meizhou":"梅州市","Shaoguan":"韶关市",
"Qingyuan":"清远市","Yangjiang":"阳江市","Jieyang":"揭阳市","Chaozhou":"潮州市","Heyuan":"河源市","Shanwei":"汕尾市",
"Yunfu":"云浮市",
"Hangzhou":"杭州市","Nanjing":"南京市","Suzhou":"苏州市","Ningbo":"宁波市","Wuxi":"无锡市","Wenzhou":"温州市",
"Changzhou":"常州市","Shaoxing":"绍兴市","Jiaxing":"嘉兴市","Xuzhou":"徐州市","Huzhou":"湖州市","Jinhua":"金华市",
"Taizhou":"台州市","Zhenjiang":"镇江市","Lishui":"丽水市","Yancheng":"盐城市","Yangzhou":"扬州市","Huaian":"淮安市",
"Lianyungang":"连云港市","Suqian":"宿迁市","Zhoushan":"舟山市","Quzhou":"衢州市","Nantong":"南通市",
"Chengdu":"成都市","Mianyang":"绵阳市","Yibin":"宜宾市","Nanchong":"南充市","Deyang":"德阳市","Leshan":"乐山市",
"Luzhou":"泸州市","Dazhou":"达州市","Meishan":"眉山市","Suining":"遂宁市","Guang'an":"广安市","Panzhihua":"攀枝花市",
"Ziyang":"资阳市","Neijiang":"内江市","Wanzhou":"万州区","Fuling":"涪陵区",
"Wuhan":"武汉市","Yichang":"宜昌市","Xiangyang":"襄阳市","Huangshi":"黄石市","Jingzhou":"荆州市","Shiyan":"十堰市",
"Xiaogan":"孝感市","Huanggang":"黄冈市","Jingmen":"荆门市","Ezhou":"鄂州市","Xianning":"咸宁市","Suizhou":"随州市",
"Enshi":"恩施土家族苗族自治州","Enshi City":"恩施市",
"Changsha":"长沙市","Zhuzhou":"株洲市","Xiangtan":"湘潭市","Hengyang":"衡阳市","Yueyang":"岳阳市","Changde":"常德市",
"Shaoyang":"邵阳市","Zhangjiajie":"张家界市","Yiyang":"益阳市","Chenzhou":"郴州市","Yongzhou":"永州市","Huaihua":"怀化市",
"Loudi":"娄底市","Xiangxi":"湘西土家族苗族自治州",
"Jinan":"济南市","Qingdao":"青岛市","Yantai":"烟台市","Weifang":"潍坊市","Linyi":"临沂市","Jining":"济宁市",
"Zibo":"淄博市","Weihai":"威海市","Rizhao":"日照市","Dongying":"东营市","Taian":"泰安市","Binzhou":"滨州市",
"Dezhou":"德州市","Liaocheng":"聊城市","Heze":"菏泽市","Zaozhuang":"枣庄市",
"Zhengzhou":"郑州市","Luoyang":"洛阳市","Nanyang":"南阳市","Xuchang":"许昌市","Xinxiang":"新乡市","Kaifeng":"开封市",
"Anyang":"安阳市","Pingdingshan":"平顶山市","Shangqiu":"商丘市","Jiaozuo":"焦作市","Zhoukou":"周口市","Xinyang":"信阳市",
"Luohe":"漯河市","Puyang":"濮阳市","Hebi":"鹤壁市","Sanmenxia":"三门峡市","Zhumadian":"驻马店市",
"Shijiazhuang":"石家庄市","Tangshan":"唐山市","Qinhuangdao":"秦皇岛市","Handan":"邯郸市","Baoding":"保定市","Langfang":"廊坊市",
"Cangzhou":"沧州市","Hengshui":"衡水市","Xingtai":"邢台市","Chengde":"承德市","Zhangjiakou":"张家口市",
"Xiamen":"厦门市","Fuzhou":"福州市","Quanzhou":"泉州市","Putian":"莆田市","Zhangzhou":"漳州市","Longyan":"龙岩市",
"Sanming":"三明市","Nanping":"南平市","Ningde":"宁德市",
"Hefei":"合肥市","Wuhu":"芜湖市","Huangshan":"黄山市","Ma'anshan":"马鞍山市","Maanshan":"马鞍山市",
"Bengbu":"蚌埠市","Anhui Bozhou":"亳州市","Suzhou (Anhui)":"宿州市","Anqing":"安庆市","Xuancheng":"宣城市",
"Huainan":"淮南市","Huaibei":"淮北市","Chuzhou":"滁州市","Liuan":"六安市","Chizhou":"池州市","Tongling":"铜陵市",
"Fuyang":"阜阳市","Bozhou":"亳州市","Lu'an":"六安市",
"Nanchang":"南昌市","Jiujiang":"九江市","Ganzhou":"赣州市","Shangrao":"上饶市","Pingxiang":"萍乡市","Xinyu":"新余市",
"Jian":"吉安市","Ji'an":"吉安市","Yichun":"宜春市","Fuzhou":"抚州市","Jingdezhen":"景德镇市","Yingtan":"鹰潭市",
"Xi'an":"西安市","Xian":"西安市","Baoji":"宝鸡市","Xianyang":"咸阳市","Weinan":"渭南市","Yan'an":"延安市",
"Yulin":"榆林市","Hanzhong":"汉中市","Ankang":"安康市","Shangluo":"商洛市","Tongchuan":"铜川市",
"Taiyuan":"太原市","Datong":"大同市","Yuncheng":"运城市","Changzhi":"长治市","Jincheng":"晋城市","Jinzhong":"晋中市",
"Linfen":"临汾市","Lvliang":"吕梁市","Luliang":"吕梁市","Shuozhou":"朔州市","Xinzhou":"忻州市","Yangquan":"阳泉市",
"Shenyang":"沈阳市","Dalian":"大连市","Harbin":"哈尔滨市","Changchun":"长春市","Anshan":"鞍山市","Fushun":"抚顺市",
"Jilin":"吉林市","Yanji":"延吉市","Daqing":"大庆市","Qiqihar":"齐齐哈尔市","Jinzhou":"锦州市","Mudanjiang":"牡丹江市",
"Yingkou":"营口市","Benxi":"本溪市","Dandong":"丹东市","Fuxin":"阜新市","Liaoyang":"辽阳市","Panjin":"盘锦市",
"Tieling":"铁岭市","Chaoyang":"朝阳市","Huludao":"葫芦岛市","Tonghua":"通化市","Baicheng":"白城市","Siping":"四平市",
"Baishan":"白山市","Songyuan":"松原市","Jiamusi":"佳木斯市","Hegang":"鹤岗市","Shuangyashan":"双鸭山市","Jixi":"鸡西市",
"Qitaihe":"七台河市","Suihua":"绥化市","Heihe":"黑河市","Daxing'anling":"大兴安岭地区",
"Kunming":"昆明市","Dali":"大理白族自治州","Lijiang":"丽江市","Qujing":"曲靖市","Yuxi":"玉溪市","Baoshan":"保山市",
"Zhaotong":"昭通市","Puer":"普洱市","Lincang":"临沧市","Chuxiong":"楚雄彝族自治州","Honghe":"红河哈尼族彝族自治州",
"Wenshan":"文山壮族苗族自治州","Xishuangbanna":"西双版纳傣族自治州","Dehong":"德宏傣族景颇族自治州",
"Nujiang":"怒江傈僳族自治州","Diqing":"迪庆藏族自治州",
"Guiyang":"贵阳市","Zunyi":"遵义市","Liupanshui":"六盘水市","Anshun":"安顺市","Bijie":"毕节市",
"Tongren":"铜仁市","Qiannan":"黔南布依族苗族自治州","Qiandongnan":"黔东南苗族侗族自治州",
"Qianxinan":"黔西南布依族苗族自治州",
"Nanning":"南宁市","Guilin":"桂林市","Liuzhou":"柳州市","Beihai":"北海市","Yulin":"玉林市","Wuzhou":"梧州市",
"Qinzhou":"钦州市","Guigang":"贵港市","Fangchenggang":"防城港市","Baise":"百色市","Hezhou":"贺州市",
"Hechi":"河池市","Laibin":"来宾市","Chongzuo":"崇左市",
"Haikou":"海口市","Sanya":"三亚市","Sansha":"三沙市","Danzhou":"儋州市",
"Lanzhou":"兰州市","Tianshui":"天水市","Jiuquan":"酒泉市","Jiayuguan":"嘉峪关市","Zhangye":"张掖市","Jinchang":"金昌市",
"Baiyin":"白银市","Qingyang":"庆阳市","Pingliang":"平凉市","Dingxi":"定西市","Longnan":"陇南市","Linxia":"临夏回族自治州",
"Gannan":"甘南藏族自治州","Wuwei":"武威市",
"Xining":"西宁市","Haidong":"海东市","Haibei":"海北藏族自治州","Huangnan":"黄南藏族自治州",
"Hainan":"海南藏族自治州","Guoluo":"果洛藏族自治州","Yushu":"玉树藏族自治州","Haixi":"海西蒙古族藏族自治州",
"Yinchuan":"银川市","Shizuishan":"石嘴山市","Wuzhong":"吴忠市","Guyuan":"固原市","Zhongwei":"中卫市",
"Hohhot":"呼和浩特市","Baotou":"包头市","Ordos":"鄂尔多斯市","Chifeng":"赤峰市","Tongliao":"通辽市",
"Hulunbuir":"呼伦贝尔市","Ulanqab":"乌兰察布市","Bayannur":"巴彦淖尔市","Xing'an":"兴安盟",
"Xilingol":"锡林郭勒盟","Alxa":"阿拉善盟","Wuhai":"乌海市",
"Urumqi":"乌鲁木齐市","Kashgar":"喀什地区","Karamay":"克拉玛依市","Turpan":"吐鲁番市","Hami":"哈密市",
"Changji":"昌吉回族自治州","Bortala":"博尔塔拉蒙古自治州","Bayingol":"巴音郭楞蒙古自治州",
"Aksu":"阿克苏地区","Kizilsu":"克孜勒苏柯尔克孜自治州","Hotan":"和田地区",
"Yili":"伊犁哈萨克自治州","Tacheng":"塔城地区","Altay":"阿勒泰地区","Shihezi":"石河子市",
"Lhasa":"拉萨市","Shigatse":"日喀则市","Chamdo":"昌都市","Nyingchi":"林芝市","Shannan":"山南市",
"Nagqu":"那曲市","Ngari":"阿里地区",
"Aba":"阿坝藏族羌族自治州","Ngawa":"阿坝藏族羌族自治州","Garzê":"甘孜藏族自治州","Garze":"甘孜藏族自治州",
"Liangshan":"凉山彝族自治州",
"Tokyo":"东京","Osaka":"大阪","Kyoto":"京都","Yokohama":"横滨","Nagoya":"名古屋","Sapporo":"札幌","Fukuoka":"福冈",
"Kobe":"神户","Hiroshima":"广岛","Sendai":"仙台","Kitakyushu":"北九州","Chiba":"千叶","Saitama":"埼玉",
"Seoul":"首尔","Busan":"釜山","Incheon":"仁川","Daegu":"大邱","Gwangju":"光州","Daejeon":"大田","Ulsan":"蔚山",
"Singapore":"新加坡市",
"Kuala Lumpur":"吉隆坡","Johor Bahru":"新山","Ipoh":"怡保","Penang":"槟城","George Town":"乔治市","Malacca":"马六甲",
"Jakarta":"雅加达","Surabaya":"泗水","Bandung":"万隆","Medan":"棉兰","Bali":"巴厘岛","Denpasar":"登巴萨",
"Bangkok":"曼谷","Chiang Mai":"清迈","Pattaya":"芭堤雅","Phuket":"普吉岛","Hat Yai":"合艾",
"Hanoi":"河内","Ho Chi Minh City":"胡志明市","Da Nang":"岘港","Hai Phong":"海防","Can Tho":"芹苴",
"Manila":"马尼拉","Cebu":"宿务","Davao":"达沃",
"Mumbai":"孟买","Delhi":"新德里","Bangalore":"班加罗尔","Hyderabad":"海得拉巴","Chennai":"金奈","Kolkata":"加尔各答",
"Pune":"浦那","Ahmedabad":"艾哈迈达巴德",
"Karachi":"卡拉奇","Lahore":"拉合尔","Islamabad":"伊斯兰堡",
"Dhaka":"达卡","Kathmandu":"加德满都","Colombo":"科伦坡","Yangon":"仰光","Vientiane":"万象","Phnom Penh":"金边",
"Bandar Seri Begawan":"斯里巴加湾市","Ulaanbaatar":"乌兰巴托",
"Dubai":"迪拜","Abu Dhabi":"阿布扎比","Riyadh":"利雅得","Jeddah":"吉达","Doha":"多哈","Kuwait City":"科威特城",
"Muscat":"马斯喀特","Amman":"安曼","Beirut":"贝鲁特","Damascus":"大马士革","Sana'a":"萨那","Jerusalem":"耶路撒冷",
"Tel Aviv":"特拉维夫","Istanbul":"伊斯坦布尔","Ankara":"安卡拉","Baku":"巴库","Tbilisi":"第比利斯","Yerevan":"埃里温",
"Astana":"阿斯塔纳","Almaty":"阿拉木图","Tashkent":"塔什干","Bishkek":"比什凯克","Dushanbe":"杜尚别","Ashgabat":"阿什哈巴德",
"London":"伦敦","Manchester":"曼彻斯特","Birmingham":"伯明翰","Liverpool":"利物浦","Glasgow":"格拉斯哥","Edinburgh":"爱丁堡",
"Leeds":"利兹","Bristol":"布里斯托尔","Sheffield":"谢菲尔德","Newcastle":"纽卡斯尔","Belfast":"贝尔法斯特","Dublin":"都柏林",
"Paris":"巴黎","Marseille":"马赛","Lyon":"里昂","Toulouse":"图卢兹","Nice":"尼斯","Bordeaux":"波尔多","Lille":"里尔",
"Berlin":"柏林","Munich":"慕尼黑","Hamburg":"汉堡","Frankfurt":"法兰克福","Cologne":"科隆","Stuttgart":"斯图加特",
"Dusseldorf":"杜塞尔多夫","Leipzig":"莱比锡","Dresden":"德累斯顿","Bonn":"波恩",
"Rome":"罗马","Milan":"米兰","Naples":"那不勒斯","Turin":"都灵","Florence":"佛罗伦萨","Venice":"威尼斯","Palermo":"巴勒莫",
"Madrid":"马德里","Barcelona":"巴塞罗那","Valencia":"瓦伦西亚","Seville":"塞维利亚","Bilbao":"毕尔巴鄂","Malaga":"马拉加",
"Amsterdam":"阿姆斯特丹","Rotterdam":"鹿特丹","The Hague":"海牙","Utrecht":"乌得勒支","Eindhoven":"埃因霍温",
"Brussels":"布鲁塞尔","Antwerp":"安特卫普","Ghent":"根特","Luxembourg":"卢森堡市",
"Vienna":"维也纳","Salzburg":"萨尔茨堡","Graz":"格拉茨","Zurich":"苏黎世","Geneva":"日内瓦","Basel":"巴塞尔",
"Stockholm":"斯德哥尔摩","Gothenburg":"哥德堡","Oslo":"奥斯陆","Bergen":"卑尔根","Copenhagen":"哥本哈根","Aarhus":"奥胡斯",
"Helsinki":"赫尔辛基","Tampere":"坦佩雷","Reykjavik":"雷克雅未克","Warsaw":"华沙","Krakow":"克拉科夫",
"Prague":"布拉格","Bratislava":"布拉迪斯拉发","Budapest":"布达佩斯","Ljubljana":"卢布尔雅那","Zagreb":"萨格勒布",
"Belgrade":"贝尔格莱德","Bucharest":"布加勒斯特","Sofia":"索非亚","Athens":"雅典","Thessaloniki":"塞萨洛尼基",
"Lisbon":"里斯本","Porto":"波尔图","Nicosia":"尼科西亚","Valletta":"瓦莱塔","Moscow":"莫斯科","Saint Petersburg":"圣彼得堡",
"Novosibirsk":"新西伯利亚","Yekaterinburg":"叶卡捷琳堡","Kazan":"喀山","Sochi":"索契","Vladivostok":"符拉迪沃斯托克(海参崴)",
"Kiev":"基辅","Kyiv":"基辅","Kharkiv":"哈尔科夫","Odessa":"敖德萨","Minsk":"明斯克","Chisinau":"基希讷乌",
"Tiraspol":"蒂拉斯波尔","Sukhumi":"苏呼米","Tskhinvali":"茨欣瓦利","Pristina":"普里什蒂纳","Skopje":"斯科普里",
"Tirana":"地拉那","Podgorica":"波德戈里察","Sarajevo":"萨拉热窝","Banja Luka":"巴尼亚卢卡","Mostar":"莫斯塔尔",
"Riga":"里加","Tallinn":"塔林","Vilnius":"维尔纽斯",
"Cairo":"开罗","Alexandria":"亚历山大","Luxor":"卢克索","Cape Town":"开普敦","Johannesburg":"约翰内斯堡","Pretoria":"比勒陀利亚",
"Durban":"德班","Port Elizabeth":"伊丽莎白港","Nairobi":"内罗毕","Mombasa":"蒙巴萨","Lagos":"拉各斯","Abuja":"阿布贾",
"Casablanca":"卡萨布兰卡","Rabat":"拉巴特","Marrakech":"马拉喀什","Algiers":"阿尔及尔","Tunis":"突尼斯市",
"Tripoli":"的黎波里","Khartoum":"喀土穆","Addis Ababa":"亚的斯亚贝巴","Dar es Salaam":"达累斯萨拉姆","Kampala":"坎帕拉",
"Kigali":"基加利","Luanda":"罗安达","Maputo":"马普托","Harare":"哈拉雷","Lusaka":"卢萨卡","Abidjan":"阿比让",
"Accra":"阿克拉","Dakar":"达喀尔","Douala":"杜阿拉","Bamako":"巴马科","Lomé":"洛美","Brazzaville":"布拉柴维尔",
"Kinshasa":"金沙萨","Libreville":"利伯维尔","Yaounde":"雅温得","Yaoundé":"雅温得",
"New York":"纽约","Los Angeles":"洛杉矶","Chicago":"芝加哥","Houston":"休斯顿","Phoenix":"菲尼克斯","Philadelphia":"费城",
"San Antonio":"圣安东尼奥","San Diego":"圣迭戈","Dallas":"达拉斯","San Jose":"圣何塞","Austin":"奥斯汀",
"Jacksonville":"杰克逊维尔","Fort Worth":"沃斯堡","Columbus":"哥伦布","Charlotte":"夏洛特","Indianapolis":"印第安纳波利斯",
"San Francisco":"旧金山","Seattle":"西雅图","Denver":"丹佛","Washington":"华盛顿","Washington D.C.":"华盛顿特区",
"Boston":"波士顿","El Paso":"埃尔帕索","Nashville":"纳什维尔","Detroit":"底特律","Oklahoma City":"俄克拉荷马城",
"Portland":"波特兰","Las Vegas":"拉斯维加斯","Memphis":"孟菲斯","Louisville":"路易斯维尔","Baltimore":"巴尔的摩",
"Milwaukee":"密尔沃基","Albuquerque":"阿尔伯克基","Tucson":"图森","Fresno":"弗雷斯诺","Sacramento":"萨克拉门托",
"Miami":"迈阿密","Atlanta":"亚特兰大","Minneapolis":"明尼阿波利斯","Tampa":"坦帕","New Orleans":"新奥尔良",
"Honolulu":"火奴鲁鲁(檀香山)","Anchorage":"安克雷奇",
"Toronto":"多伦多","Vancouver":"温哥华","Montreal":"蒙特利尔","Calgary":"卡尔加里","Edmonton":"埃德蒙顿","Ottawa":"渥太华",
"Winnipeg":"温尼伯","Quebec City":"魁北克市","Hamilton":"汉密尔顿",
"Sydney":"悉尼","Melbourne":"墨尔本","Brisbane":"布里斯班","Perth":"珀斯","Adelaide":"阿德莱德","Gold Coast":"黄金海岸",
"Canberra":"堪培拉","Newcastle":"纽卡斯尔","Wollongong":"伍伦贡","Auckland":"奥克兰","Wellington":"惠灵顿",
"Christchurch":"基督城","Hamilton (NZ)":"哈密尔顿(NZ)","Dunedin":"达尼丁",
"Sao Paulo":"圣保罗","São Paulo":"圣保罗","Rio de Janeiro":"里约热内卢","Brasilia":"巴西利亚","Salvador":"萨尔瓦多",
"Fortaleza":"福塔莱萨","Belo Horizonte":"贝洛奥里藏特","Manaus":"马瑙斯","Curitiba":"库里蒂巴","Recife":"累西腓",
"Buenos Aires":"布宜诺斯艾利斯","Cordoba":"科尔多瓦","Rosario":"罗萨里奥","Mendoza":"门多萨","Santiago":"圣地亚哥",
"Lima":"利马","Cusco":"库斯科","Bogota":"波哥大","Bogotá":"波哥大","Medellin":"麦德林","Cali":"卡利","Barranquilla":"巴兰基亚",
"Caracas":"加拉加斯","Quito":"基多","Guayaquil":"瓜亚基尔","Asuncion":"亚松森","Asunción":"亚松森",
"Montevideo":"蒙得维的亚","La Paz":"拉巴斯","Sucre":"苏克雷","Santa Cruz":"圣克鲁斯","Panama City":"巴拿马城",
"San Jose":"圣何塞(哥斯达黎加)","San José":"圣何塞(哥斯达黎加)","Managua":"马那瓜","Tegucigalpa":"特古西加尔巴",
"San Salvador":"圣萨尔瓦多","Guatemala City":"危地马拉城","Belmopan":"贝尔莫潘","Havana":"哈瓦那",
"Santo Domingo":"圣多明各","Mexico City":"墨西哥城","Guadalajara":"瓜达拉哈拉","Monterrey":"蒙特雷","Puebla":"普埃布拉",
"Tijuana":"蒂华纳","Cancun":"坎昆","Cancún":"坎昆","Acapulco":"阿卡普尔科","Merida":"梅里达","León":"莱昂",
"Kingston":"金斯顿","Port-au-Prince":"太子港","San Juan":"圣胡安","Port of Spain":"西班牙港","Paramaribo":"帕拉马里博",
"Cayenne":"卡宴","Santiago de Cuba":"圣地亚哥-德古巴","Havana Province":"哈瓦那省","Nassau":"拿骚",
"Bridgetown":"布里奇敦","Roseau":"罗索","Castries":"卡斯特里","Kingstown":"金斯敦","St. George's":"圣乔治",
"St. John's":"圣约翰","Basseterre":"巴斯特尔",
"Suva":"苏瓦","Port Moresby":"莫尔兹比港","Nadi":"楠迪","Apia":"阿皮亚","Nuku'alofa":"努库阿洛法","Funafuti":"富纳富提",
"Tarawa":"塔拉瓦","Majuro":"马朱罗","Palikir":"帕利基尔","Ngerulmud":"恩吉鲁穆德","Yaren":"亚伦区","Hagatna":"阿加尼亚",
"Pago Pago":"帕果帕果","Charlotte Amalie":"夏洛特阿马利亚","Oranjestad":"奥拉涅斯塔德","Willemstad":"威廉斯塔德",
"Philipsburg":"菲利普斯堡","Kralendijk":"克拉伦代克","Gustavia":"古斯塔维亚","Marigot":"马里戈",
"Saint-Pierre":"圣皮埃尔","Nuuk":"努克","Torshavn":"托尔斯港","Thorshavn":"托尔斯港","Gibraltar":"直布罗陀",
"Mariehamn":"玛丽港","Jamestown":"詹姆斯敦","Stanley":"斯坦利",
"Adamstown":"亚当斯敦","Avarua":"阿瓦鲁阿","Alofi":"阿洛菲","Fakaofo":"法考福","Mata-Utu":"马塔乌图",
"Papeete":"帕皮提","Noumea":"努美阿","Port Vila":"维拉港","Honiara":"霍尼亚拉"
};
const PATH_FUNCTION = {
"/":"访问主页(Clash 配置生成器)",
"/index.html":"访问主页(Clash 配置生成器)",
"/api/visit":"API: 查询访问者信息",
"/favicon.ico":"请求站点图标",
"/robots.txt":"请求 robots.txt"
};
function toCnCountry(raw) {
if (!raw) return "未知国家";
const s = raw.toString().trim();
if (!s) return "未知国家";
if (s.length === 2 && COUNTRY_CN[s]) return COUNTRY_CN[s];
if (s.length === 2) {
const up = s.toUpperCase();
if (COUNTRY_CN[up]) return COUNTRY_CN[up];
}
const EN_COUNTRY = {
"China":"中国","United States":"美国","Japan":"日本","South Korea":"韩国","Korea, Republic of":"韩国",
"Singapore":"新加坡","Hong Kong":"中国香港","Taiwan":"中国台湾","Macau":"中国澳门",
"United Kingdom":"英国","Great Britain":"英国","England":"英格兰","Scotland":"苏格兰","Wales":"威尔士",
"Germany":"德国","France":"法国","Russia":"俄罗斯","Russian Federation":"俄罗斯",
"Australia":"澳大利亚","Canada":"加拿大","India":"印度","Brazil":"巴西","Italy":"意大利","Spain":"西班牙",
"Mexico":"墨西哥","Indonesia":"印度尼西亚","Thailand":"泰国","Vietnam":"越南","Malaysia":"马来西亚",
"Philippines":"菲律宾","Turkey":"土耳其","Saudi Arabia":"沙特阿拉伯","United Arab Emirates":"阿联酋",
"Israel":"以色列","South Africa":"南非","Egypt":"埃及","Argentina":"阿根廷","Netherlands":"荷兰",
"Sweden":"瑞典","Switzerland":"瑞士","Belgium":"比利时","Austria":"奥地利","Norway":"挪威","Denmark":"丹麦",
"Finland":"芬兰","Poland":"波兰","Ireland":"爱尔兰","Portugal":"葡萄牙","Greece":"希腊","New Zealand":"新西兰",
"Pakistan":"巴基斯坦","Bangladesh":"孟加拉国","Nigeria":"尼日利亚","Ukraine":"乌克兰","Colombia":"哥伦比亚",
"Chile":"智利","Peru":"秘鲁","Iran":"伊朗","Iraq":"伊拉克","Qatar":"卡塔尔","Kuwait":"科威特",
"Oman":"阿曼","Jordan":"约旦","Lebanon":"黎巴嫩","Syria":"叙利亚","Yemen":"也门","Afghanistan":"阿富汗",
"Nepal":"尼泊尔","Sri Lanka":"斯里兰卡","Myanmar":"缅甸","Burma":"缅甸","Cambodia":"柬埔寨","Laos":"老挝",
"Brunei":"文莱","Mongolia":"蒙古","Kazakhstan":"哈萨克斯坦","Uzbekistan":"乌兹别克斯坦",
"Turkmenistan":"土库曼斯坦","Kyrgyzstan":"吉尔吉斯斯坦","Tajikistan":"塔吉克斯坦",
"Azerbaijan":"阿塞拜疆","Armenia":"亚美尼亚","Georgia":"格鲁吉亚","Belarus":"白俄罗斯",
"Moldova":"摩尔多瓦","Romania":"罗马尼亚","Bulgaria":"保加利亚","Croatia":"克罗地亚",
"Slovenia":"斯洛文尼亚","Serbia":"塞尔维亚","Montenegro":"黑山","North Macedonia":"北马其顿",
"Albania":"阿尔巴尼亚","Bosnia and Herzegovina":"波黑","Kosovo":"科索沃",
"Estonia":"爱沙尼亚","Latvia":"拉脱维亚","Lithuania":"立陶宛","Czech Republic":"捷克共和国","Czechia":"捷克共和国",
"Slovakia":"斯洛伐克","Hungary":"匈牙利","Luxembourg":"卢森堡","Monaco":"摩纳哥",
"Liechtenstein":"列支敦士登","Andorra":"安道尔","San Marino":"圣马力诺","Vatican":"梵蒂冈",
"Vatican City":"梵蒂冈","Malta":"马耳他","Iceland":"冰岛","Cyprus":"塞浦路斯",
"Morocco":"摩洛哥","Algeria":"阿尔及利亚","Tunisia":"突尼斯","Libya":"利比亚","Sudan":"苏丹",
"South Sudan":"南苏丹","Ethiopia":"埃塞俄比亚","Somalia":"索马里","Kenya":"肯尼亚","Tanzania":"坦桑尼亚",
"Uganda":"乌干达","Rwanda":"卢旺达","Burundi":"布隆迪","Mozambique":"莫桑比克",
"Zimbabwe":"津巴布韦","Zambia":"赞比亚","Malawi":"马拉维","Angola":"安哥拉","Namibia":"纳米比亚",
"Botswana":"博茨瓦纳","Lesotho":"莱索托","Eswatini":"斯威士兰","Gambia":"冈比亚","Senegal":"塞内加尔",
"Mauritania":"毛里塔尼亚","Mali":"马里","Burkina Faso":"布基纳法索","Niger":"尼日尔","Chad":"乍得",
"Central African Republic":"中非","Cameroon":"喀麦隆","Equatorial Guinea":"赤道几内亚",
"Gabon":"加蓬","Republic of the Congo":"刚果共和国","DR Congo":"刚果民主共和国",
"Democratic Republic of the Congo":"刚果民主共和国","Guinea":"几内亚","Sierra Leone":"塞拉利昂",
"Liberia":"利比里亚","Cote d'Ivoire":"科特迪瓦","Ivory Coast":"科特迪瓦","Ghana":"加纳","Togo":"多哥",
"Benin":"贝宁","Eritrea":"厄立特里亚","Djibouti":"吉布提","Comoros":"科摩罗","Mauritius":"毛里求斯",
"Seychelles":"塞舌尔","Cape Verde":"佛得角","Reunion":"留尼汪","Mayotte":"马约特",
"Western Sahara":"西撒哈拉","Venezuela":"委内瑞拉","Ecuador":"厄瓜多尔","Bolivia":"玻利维亚",
"Paraguay":"巴拉圭","Uruguay":"乌拉圭","Guyana":"圭亚那","Suriname":"苏里南","French Guiana":"法属圭亚那",
"Cuba":"古巴","Jamaica":"牙买加","Haiti":"海地","Dominican Republic":"多米尼加","Puerto Rico":"波多黎各",
"Trinidad and Tobago":"特立尼达和多巴哥","Panama":"巴拿马","Costa Rica":"哥斯达黎加",
"Nicaragua":"尼加拉瓜","Honduras":"洪都拉斯","El Salvador":"萨尔瓦多","Guatemala":"危地马拉",
"Belize":"伯利兹","Bahamas":"巴哈马","Barbados":"巴巴多斯","Fiji":"斐济",
"Papua New Guinea":"巴布亚新几内亚","Solomon Islands":"所罗门群岛","Vanuatu":"瓦努阿图",
"New Caledonia":"新喀里多尼亚","French Polynesia":"法属波利尼西亚","Samoa":"萨摩亚","Tonga":"汤加",
"Tuvalu":"图瓦卢","Kiribati":"基里巴斯","Marshall Islands":"马绍尔群岛",
"Micronesia":"密克罗尼西亚","Palau":"帕劳","Nauru":"瑙鲁","Guam":"关岛",
"American Samoa":"美属萨摩亚","Greenland":"格陵兰","Faroe Islands":"法罗群岛","Gibraltar":"直布罗陀"
};
if (EN_COUNTRY[s]) return EN_COUNTRY[s];
const lower = s.toLowerCase();
if (lower.includes("china") && !lower.includes("taiwan") && !lower.includes("hong") && !lower.includes("macau")) return "中国";
if (lower.includes("chinese")) return "中国";
return s;
}
function toCnCity(raw) {
if (!raw) return "未知地区";
const s = raw.toString().trim();
if (!s) return "未知地区";
if (CITY_CN[s]) return CITY_CN[s];
const normalized = s.split(" ").map(w => w ? w[0].toUpperCase() + w.slice(1).toLowerCase() : "").join(" ");
if (CITY_CN[normalized]) return CITY_CN[normalized];
const noApos = s.replace(/['\u2019]/g, "");
if (CITY_CN[noApos]) return CITY_CN[noApos];
if (CITY_CN[noApos.replace(/\s*\(.*\)\s*$/,"")]) return CITY_CN[noApos.replace(/\s*\(.*\)\s*$/,"")];
return s;
}
function toFunctionName(path) {
if (!path) return "未知功能";
const p = path.toString();
if (PATH_FUNCTION[p]) return PATH_FUNCTION[p];
const qIdx = p.indexOf("?");
if (qIdx > 0) {
const noQuery = p.slice(0, qIdx);
if (PATH_FUNCTION[noQuery]) return PATH_FUNCTION[noQuery];
}
const clean = (qIdx > 0 ? p.slice(0, qIdx) : p);
if (clean.startsWith("/api/")) return "API 接口: " + clean;
if (clean.startsWith("/static/") || clean.includes(".")) return "资源请求: " + clean;
return "访问: " + clean;
}
const country = toCnCountry(request.cf?.country || "未知国家");
const city = toCnCity(request.cf?.city || request.cf?.region || "未知地区");
const nowIso = new Date().toISOString();
const SECURITY_HEADERS = {
"X-Content-Type-Options": "nosniff",
"X-Frame-Options": "SAMEORIGIN",
"Referrer-Policy": "strict-origin-when-cross-origin",
"Permissions-Policy": "camera=(), microphone=(), geolocation=(), interest-cohort=()",
"Strict-Transport-Security": "max-age=31536000; includeSubDomains"
};
function withSecurityHeaders(headersObj) {
return Object.assign({}, SECURITY_HEADERS, headersObj || {});
}
function methodNotAllowedResponse(allowMethods) {
return new Response("Method Not Allowed", {
status: 405,
headers: withSecurityHeaders({
"Allow": allowMethods,
"Content-Type": "text/plain;charset=UTF-8"
})
});
}
const SHARED_HEAD_META = `<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<meta name="theme-color" content="#f8fafc">
<script>(function(){try{var t=localStorage.getItem('theme');if(t==='dark'||t==='light'){document.documentElement.setAttribute('data-theme',t);var m=document.querySelector('meta[name="theme-color"]');if(m)m.setAttribute('content',t==='dark'?'#0f172a':'#f8fafc');}}catch(e){}})();</script>`;
const SHARED_TRACK_SCRIPT = `<script>
function trackAction(actionName, extra) {
try {
const name = (actionName || "").toString().slice(0, 150);
if (!name) return;
const payload = { action: name };
if (extra !== undefined && extra !== null) {
const e = String(extra).slice(0, 200);
if (e) payload.extra = e;
}
const jsonStr = JSON.stringify(payload);
if (navigator.sendBeacon) {
try {
const blob = new Blob([jsonStr], { type: "application/json" });
if (!navigator.sendBeacon("/api/track", blob)) {
fetch("/api/track", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: jsonStr,
keepalive: true,
credentials: "same-origin"
}).catch(function(){});
}
} catch (be) {
fetch("/api/track", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: jsonStr,
keepalive: true,
credentials: "same-origin"
}).catch(function(){});
}
} else {
fetch("/api/track", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: jsonStr,
keepalive: true,
credentials: "same-origin"
}).catch(function(){});
}
} catch (_) { }
}
</script>`;
async function recordVisit(path, actionDirect) {
if (env && env.DB) {
d1Heartbeat(env.DB, clientIp).catch(function (e) { console.warn("D1 在线心跳失败:", e); });
}
if (!env || !env.PAGE_VISITS) return 0;
try {
const safePath = (path || "").toString().slice(0, 200);
const functionName = (actionDirect && typeof actionDirect === "string" && actionDirect.trim())
? ("功能操作: " + actionDirect.trim()).slice(0, 200)
: toFunctionName(safePath).slice(0, 200);
const TTL_30_DAYS = 30 * 24 * 60 * 60;
const TTL_DEDUPE_10_MIN = 10 * 60;
const dedupeKey = `dedupe:${clientIp}:${functionName}`;
if (await env.PAGE_VISITS.get(dedupeKey)) return 0;
await env.PAGE_VISITS.put(dedupeKey, "1", { expirationTtl: TTL_DEDUPE_10_MIN })
.catch(dedupeErr => console.warn("写操作去重标记失败:", dedupeErr));
const newLog = { ip: clientIp, country, city, time: nowIso, path: functionName };
let logs = [];
const existingLogsStr = await env.PAGE_VISITS.get("visit_logs_list");
if (existingLogsStr) {
try { logs = JSON.parse(existingLogsStr) || []; } catch (_) { logs = []; }
}
logs.unshift(newLog);
if (logs.length > 100) logs = logs.slice(0, 100);
await env.PAGE_VISITS.put("visit_logs_list", JSON.stringify(logs), { expirationTtl: TTL_30_DAYS });
return 0;
} catch (e) {
console.error("KV 写入异常:", e);
return 0;
}
}
if (url.pathname === "/api/track") {
if (request.method !== "POST") return methodNotAllowedResponse("POST");
const TRACK_RATE_LIMIT = 10;
const TRACK_RATE_WINDOW = 60;
if (env && env.PAGE_VISITS && clientIp) {
try {
const rlKey = "rl:track:" + clientIp;
const rlRaw = await env.PAGE_VISITS.get(rlKey);
const rlCount = rlRaw ? parseInt(rlRaw, 10) : 0;
if (rlCount >= TRACK_RATE_LIMIT) {
return new Response(JSON.stringify({ ok: false, error: "请求过于频繁,请稍后再试" }), {
status: 429,
headers: withSecurityHeaders({
"Content-Type": "application/json;charset=UTF-8",
"Retry-After": String(TRACK_RATE_WINDOW)
})
});
}
await env.PAGE_VISITS.put(rlKey, String(rlCount + 1), { expirationTtl: TRACK_RATE_WINDOW });
} catch (_) { }
}
const ct = (request.headers.get("Content-Type") || "").toLowerCase();
let action = "";
let extra = "";
if (ct.indexOf("application/json") >= 0) {
try {
const j = await request.json();
action = (j?.action || "").toString();
extra = (j?.extra || "").toString();
} catch (_) { action = ""; }
} else {
try {
const formData = await request.formData();
action = (formData.get("action") || "").toString();
extra = (formData.get("extra") || "").toString();
} catch (_) {
const text = await request.text().catch(() => "");
if (text) {
try {
const sp = new URLSearchParams(text);
action = sp.get("action") || "";
extra = sp.get("extra") || "";
} catch (_) {}
}
}
}
if (!action) {
return new Response(JSON.stringify({ ok: false, error: "缺少 action 字段" }), {
status: 400,
headers: withSecurityHeaders({ "Content-Type": "application/json;charset=UTF-8" })
});
}
const fullAction = extra ? (action + " | 详情: " + extra) : action;
ctx.waitUntil(recordVisit("/api/track", fullAction));
return new Response(JSON.stringify({ ok: true, received: fullAction }), {
status: 200,
headers: withSecurityHeaders({
"Content-Type": "application/json;charset=UTF-8",
"Cache-Control": "no-store"
})
});
}
if (url.pathname === "/api/visit") {
if (request.method !== "GET") return methodNotAllowedResponse("GET");
let visitCount = 0;
let onlineCount = 0;
let counterReady = false;
ctx.waitUntil(recordVisit("/api/visit"));
if (env && env.DB) {
counterReady = true;
try {
const stats = await d1TrackAndGetStats(env.DB, clientIp);
visitCount = stats.visitCount;
onlineCount = stats.onlineCount;
} catch (statsErr) {
console.warn("D1 访客统计失败:", statsErr);
counterReady = false;
}
}
return new Response(JSON.stringify({
ip: clientIp,
country: country,
city: city,
visitCount: visitCount,
onlineCount: onlineCount,
counterReady: counterReady
}), {
headers: withSecurityHeaders({ "Content-Type": "application/json;charset=UTF-8" })
});
}
if (url.pathname === "/api/geo-lookup") {
if (request.method !== "GET") return methodNotAllowedResponse("GET");
const rawHosts = (url.searchParams.get("host") || "").split(",").map(s => s.trim()).filter(Boolean);
const rawIps = (url.searchParams.get("ip") || "").split(",").map(s => s.trim()).filter(Boolean);
const targets0 = rawHosts.length ? rawHosts : rawIps;
ctx.waitUntil(recordVisit("/api/geo-lookup", "节点地理查询:" + targets0.length + " 个目标"));
if (!targets0.length) {
return new Response(JSON.stringify({ ok: false, error: "缺少 host 或 ip 参数", results: [] }), {
status: 400,
headers: withSecurityHeaders({ "Content-Type": "application/json;charset=UTF-8" })
});
}
const targets = Array.from(new Set(targets0)).slice(0, 20);
const results = new Array(targets.length);
let cursor = 0;
async function geoWorker() {
while (cursor < targets.length) {
const i = cursor++;
const t = targets[i];
try {
const r = await lookupGeoTarget(t, COUNTRY_CN);
results[i] = { host: t, ip: r.ip, label: r.label };
} catch (e) {
console.warn("[GeoLookup] 查询失败:", t, e && e.message);
results[i] = { host: t, ip: "", label: "通用" };
}
}
}
await Promise.all(Array.from({ length: 8 }, geoWorker));
return new Response(JSON.stringify({ ok: true, results }), {
headers: withSecurityHeaders({
"Content-Type": "application/json;charset=UTF-8",
"Cache-Control": "public, max-age=3600"
})
});
}
await recordVisit("/");
const html = `<!DOCTYPE html>
<html lang="zh-CN">
<head>
${SHARED_HEAD_META}
<title> OpenClash(.yaml)规则文件一键生成工具 </title>
<style>
:root {
--bg-base: #f3f2f1;
--body-bg: linear-gradient(180deg, #faf9f8 0%, #f3f2f1 45%, #edebe9 100%);
--surface: #ffffff;
--container-bg: rgba(255, 255, 255, 0.72);
--primary: #0078d4;
--primary-hover: #106ebe;
--primary-pressed: #005a9e;
--primary-soft: #deecf9;
--primary-soft-border: rgba(0, 120, 212, 0.25);
--secondary-deep: #323130;
--card-border: #e1e1e1;
--card-border-hover: #c8c6c4;
--input-border: #8a8886;
--text-strong: #323130;
--text-main: #605e5c;
--text-muted: #a19f9d;
--input-bg: #ffffff;
--input-ring: rgba(0, 120, 212, 0.3);
--output-bg: #faf9f8;
--output-color: #323130;
--option-bg: #ffffff;
--node-card-bg: #ffffff;
--btn-add-node-bg: #ffffff;
--mode-btn-bg: #ffffff;
--btn-sub-color: #323130;
--quick-link-color: #0078d4;
--mode-desc-color: #004578;
--mode-desc-bg: #deecf9;
--mode-desc-border: rgba(0, 120, 212, 0.25);
--success: #107c10;
--success-soft: #dff6dd;
--warning: #9d5d00;
--warning-soft: #fff4ce;
--danger: #c50f1f;
--danger-soft: #fde7e9;
--ghost-pressed-bg: #f0f0f0;
--shadow-1: 0 1.6px 3.6px rgba(0, 0, 0, 0.13), 0 0.3px 0.9px rgba(0, 0, 0, 0.1);
--shadow-2: 0 3.2px 7.2px rgba(0, 0, 0, 0.13), 0 0.6px 1.8px rgba(0, 0, 0, 0.1);
--shadow-3: 0 6.4px 14.4px rgba(0, 0, 0, 0.13), 0 1.2px 3.6px rgba(0, 0, 0, 0.1);
--shadow-hover: 0 6.4px 14.4px rgba(0, 0, 0, 0.18), 0 1.2px 3.6px rgba(0, 0, 0, 0.14);
}
body {
font-family: 'Segoe UI Variable Display', 'Segoe UI', 'Segoe UI Web', -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
padding: 24px;
background: var(--body-bg);
background-attachment: fixed;
color: var(--text-main);
margin: 0;
min-height: 100vh;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
}
.container {
max-width: 1000px;
margin: 0 auto;
background: var(--container-bg);
-webkit-backdrop-filter: blur(32px) saturate(150%);
backdrop-filter: blur(32px) saturate(150%);
padding: 28px;
border-radius: 8px;
border: 1px solid var(--card-border);
box-shadow: var(--shadow-2);
transition: box-shadow 200ms ease-out, border-color 200ms ease-out;
}
.container:hover {
box-shadow: var(--shadow-hover);
border-color: var(--card-border-hover);
}
@media (max-width: 1024px) {
body { padding: 18px; }
.container { padding: 22px; border-radius: 8px; }
.hero h2 { font-size: 21px; }
}
@media (max-width: 640px) {
body { padding: 12px; }
.container { padding: 16px; border-radius: 8px; box-shadow: var(--shadow-2); }
.hero { padding-top: 6px; }
.hero h2 { font-size: 18px; margin-bottom: 14px; }
.hero-logo { height: 22px; width: 22px; vertical-align: -4px; }
.hero-sub { font-size: 12px; }
.ip-stats-badge { font-size: 11px; padding: 5px 10px; flex-wrap: wrap; justify-content: center; row-gap: 2px; }
.download-btn-link.home-link { width: 100%; justify-content: center; }
.download-btn-link { padding: 10px 12px; font-size: 12px; }
.mode-btn-group { flex-direction: column; }
.mode-btn { width: 100%; min-height: 48px; }
.row { flex-direction: column !important; gap: 8px !important; }
.row > div { width: 100% !important; flex: none !important; }
.section-title { font-size: 14px; }
label { font-size: 12px; }
table { font-size: 12px; }
th, td { padding: 8px 6px; }
}
@media (max-width: 380px) {
body { padding: 8px; }
.container { padding: 12px; }
.hero h2 { font-size: 16px; }
.step-card { padding: 14px 12px 14px; border-radius: 8px; }
.step-title { font-size: 14px; }
.ip-stats-badge { font-size: 10px; padding: 4px 8px; }
}
@media (pointer: coarse) {
.download-btn-link { min-height: 40px; }
.btn-main, .btn-clear, .btn-lookup { min-height: 48px; }
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; }
}
.hero { text-align: center; padding: 10px 8px 4px; }
.hero h2 {
margin: 0 0 20px;
color: var(--text-strong);
font-size: 26px;
font-weight: 600;
letter-spacing: -0.01em;
}
.hero-sub { color: var(--text-muted); font-size: 13px; white-space: nowrap; }
.hero-logo {
height: 30px;
width: 30px;
vertical-align: -6px;
border-radius: 50%;
margin-right: 2px;
}
html[data-theme="dark"] .hero-logo {
box-shadow:
0 0 0 1.5px rgba(76, 194, 255, 0.6),
0 0 12px rgba(0, 120, 212, 0.5),
0 0 28px rgba(0, 120, 212, 0.22);
}
@media (prefers-color-scheme: dark) {
html:not([data-theme="light"]) .hero-logo {
box-shadow:
0 0 0 1.5px rgba(76, 194, 255, 0.6),
0 0 12px rgba(0, 120, 212, 0.5),
0 0 28px rgba(0, 120, 212, 0.22);
}
}
.hero-tools { display: flex; justify-content: center; align-items: center; gap: 12px; flex-wrap: wrap; }
.step-card {
background: var(--surface);
border: 1px solid var(--card-border);
border-radius: 8px;
padding: 18px 20px 20px;
margin-top: 22px;
box-shadow: var(--shadow-1);
transition: box-shadow 200ms ease-out, transform 200ms ease-out, border-color 200ms ease-out;
}
.step-card:hover {
box-shadow: var(--shadow-hover);
border-color: var(--card-border-hover);
transform: translateY(-4px);
}
.step-title {
display: flex;
align-items: center;
gap: 10px;
font-weight: 600;
font-size: 15px;
color: var(--text-strong);
margin-bottom: 14px;
}
.step-badge {
width: 26px;
height: 26px;
flex: 0 0 auto;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: var(--primary);
color: #ffffff;
font-size: 13px;
box-shadow: var(--shadow-1);
}
.ip-stats-badge {
background: var(--primary-soft);
border: 1px solid var(--primary-soft-border);
color: var(--mode-desc-color);
padding: 6px 14px;
border-radius: 4px;
font-size: 12px;
display: inline-flex;
align-items: center;
gap: 8px;
font-weight: 600;
box-shadow: none;
}
.ip-stats-badge strong { color: var(--text-strong); }
#userIp {
cursor: pointer;
-webkit-user-select: all;
user-select: all;
border-bottom: 1px dashed currentColor;
padding-bottom: 1px;
transition: color 150ms ease-out;
}
#userIp:hover { color: var(--primary); }
#userIp.ip-masked::after {
content: " 👁️🗨️";
font-size: 11px;
opacity: 0.8;
vertical-align: middle;
}
#userIp.ip-revealed::after {
content: " 🙈";
font-size: 11px;
opacity: 0.8;
vertical-align: middle;
}
.github-link { color: var(--text-muted); display: inline-flex; align-items: center; justify-content: center; text-decoration: none; transition: color 150ms ease-out; }
.github-link:hover { color: var(--primary); }
.quick-links-bar { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 16px; margin-bottom: 14px; justify-content: center; align-items: center; }
.quick-links-right { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 150px), 1fr)); gap: 8px; flex: 1 1 auto; min-width: 0; justify-content: center; }
.download-btn-link.home-link { background: var(--primary); color: #ffffff; border-color: transparent; font-weight: 600; }
.download-btn-link.home-link:hover { background: var(--primary-hover); color: #ffffff; border-color: transparent; box-shadow: var(--shadow-hover); }
.download-btn-link.home-link:active { background: var(--primary-pressed); box-shadow: none; }
.section-title {
font-weight: 600;
font-size: 15px;
color: var(--text-strong);
border-left: none;
border-radius: 2px;
padding-left: 0;
margin: 0;
}
.download-btn-link {